Commit Graph
1989 Commits
Author SHA1 Message Date
Hakan Seven 7ef5b431e1 Merge pull request #698 from gianlucafiore/fix/697
Fix TCOUNT numbering and add placement options
2026-08-08 03:07:09 +03:00
Hakan SevenandClaude Opus 5 7307ea3136 fix(dim): measure a two-line angle between the lines that define it
A two-line angular dimension stores two lines, not two rays:
first_point→second_point is one, angle_vertex→definition_point the other,
and the angle is the one they make where they cross. The renderer read
angle_vertex as the centre and the other two points as the rays from it,
which measures something else entirely — a ten degree angle came out as
two hundred and seventy, and a shallow one as very nearly a full circle.

The lines are now intersected for the real vertex, and the arc point
picks which of the four angles at that crossing is meant. Each line
offers its direction and its reverse, and the sweep is tried from either
line: taking only the first→second order leaves out half the angles, and
the half left out is the one wanted whenever the lines are nearly
parallel. Parallel lines cross nowhere, so only the extension lines are
drawn.

A zero sweep was also being turned into a full turn, because the wrap
that pushes a negative sweep forwards ran on zero too. Two rays pointing
the same way enclose no angle and now draw none.

Checked against the three angular dimensions in a drawing that carries
its own baked arcs: the vertex and sweep computed from the stored line
endpoints reproduce the baked centre and angle to the decimal — 7.48,
10.61 and 2.29 degrees — from entirely separate inputs.

This is the path taken when a dimension has no baked block: one OCS drew
itself, or one whose block was dropped by an edit. Both paths now agree.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 02:59:34 +03:00
Hakan SevenandClaude Opus 5 db43c21d55 fix(annotative): scale text by its own annotativeness, not its style's
An annotative object is drawn at the annotation scale only for the scales
it has a representation for. Text was being scaled whenever its text
style carried the annotative flag, whether or not the text itself had any
representation at all.

A drawing of twenty floors here has 1144 pieces of text on a style
someone flagged annotative and no object contexts anywhere. At 1:100 they
were drawn a hundred times too tall — a 411-unit height reaching 41067 —
while every other style in the same drawing stayed put.

`is_annotative` already said the right thing, and said why: a text style
can be turned annotative long after text was drawn in it, and that text
keeps its stored height until it is explicitly converted. The scale
resolver contradicted it. Text now scales on its own annotativeness
alone: an object context, a legacy XDATA marker, or the entity's flag.

Dimensions, leaders, tolerances and tables are unchanged — there the
style is the standard signal and still counts on its own.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 02:45:59 +03:00
Hakan SevenandClaude Opus 5 2ea3bca093 feat(properties): stop offering a height the style has fixed
A text style with a non-zero height fixes the size of everything drawn in
it — the CAD that writes the file skips the height prompt for such a
style, and shows the height without letting it be changed. OCS offered
the field either way, so a drawing could be edited into disagreeing with
its own style.

The height row is now read-only when the style fixes it, for TEXT, MTEXT
and both attribute kinds. `num_prop` already draws a numeric row either
way, so the value still reads the same, just grey.

The panel builders are handed an entity, not the document it came from,
and widening that trait would touch every entity type. The lookup rides
beside the unit context instead, seeded from the same place, since it
answers the same kind of question: something the drawing settles that a
row needs in order to render itself.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 02:45:46 +03:00
Hakan SevenandClaude Opus 5 188d1a9ccb fix(dim): draw a dimension from the picture it carries
A DIMENSION references the block "that contains the entities that make up
the dimension picture", and that block is the picture: AutoCAD requires
it and draws it, BricsCAD draws it when present and only renders from the
dimension variables when it is missing. OCS re-derived the picture from
DIMVARS every time and ignored the block entirely.

That works while a style agrees with what the drawing actually drew. A
Revit-exported schedule here does not: its style stores DIMTXT already in
drawing units beside a DIMSCALE meant to scale it, so the text came out
three hundred times too tall, and each dimension carries a DIMFXL
override that is likewise already in drawing units, so the extension
lines ran nearly a million units past the points they measure.

Neither is guesswork about units now. The block holds the geometry the
drawing was authored with, so it is drawn: the contents are already in
world space and the block's base point is the origin, and the identity
instance that carries them exists only to reuse the scene graph's
ByBlock / layer-0 colour resolution. Every wire answers to the dimension,
so a pick still selects the dimension rather than one of its pieces.
Re-deriving stays as the fallback for a dimension with no block.

Annotative dimensions keep the old path: their several representations
are separate blocks and choosing between them is what the annotation
machinery already does, while the doctrine above assumes one picture.

A picture is only good while its inputs hold, so it is dropped when they
change: editing a dimension's properties, and saving the dimension style
it is drawn with. Entity edits already dropped it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 02:44:30 +03:00
gianlucafiore b704a43872 Fix TCOUNT numbering and placement options 2026-08-07 19:50:11 -03:00
Hakan SevenandClaude Opus 5 039f703d7f fix(input): stop drafting-aid keys from cancelling the command
F8 reached ORTHO as an ordinary command, and dispatching a command tears
down whatever is running first: the active command dropped, its preview
cleared, its base point forgotten. So pressing F8 partway through a LINE
abandoned the line, and during a MOVE it took the dragged ghost with it.

A function key gets pressed mid-command — that is what it is for.
Toggles that only flip a flag now skip the teardown: ORTHO, GRID, SNAP,
POLAR, and the drafting-settings popup. None of them starts a command,
opens a document or reads geometry, so there is nothing for the teardown
to protect.

F7, F9 and F10 came through the same path and were broken the same way;
they are fixed with F8. F3, F11 and F12 already had messages of their
own and were never affected.

Closes #677

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 00:48:25 +03:00
Hakan SevenandClaude Opus 5 eb513ac5f8 fix(stretch): let the selection stage behave like every other one
STRETCH ended itself when a crossing window caught nothing, so a missed
aim meant restarting the command to try again. A missed aim is not a
decision to stop: say what happened and ask for the corner afresh, the
way a selection that picks nothing leaves MOVE still asking.

Enter now ends the selection stage too. With objects already picked the
window is only there to narrow them, so there is nothing left to choose
— Enter takes a window containing all of them, which stretches every
vertex, which is the whole selection moving. That is what a
pick-selected stretch means. With nothing picked there is nothing to
end, so Enter still cancels. The prompt says so.

Not changed: with a prior selection, a window that misses every vertex
still goes on to ask for the base point. The command holds wires, not
entities, and the entity rules do not follow the wires — a small window
inside a circle catches its centre, and so the whole circle, while no
tessellated vertex is inside. Guarding on the wires would refuse that
stretch.

Closes #676

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 00:42:16 +03:00
Hakan SevenandClaude Opus 5 8e09a312aa fix(dim): let the dimension line say which distance is measured
DIMLINEAR chose its measurement axis from the two extension line origins
alone, taking the longer of their x and y differences. Every line tilted
less than 45° therefore came out horizontal and stayed horizontal, no
matter where the dimension line was dragged.

The third pick is what answers the question. A linear dimension reports
the run or the rise; pulling the dimension line clear of the points above
or below asks for the horizontal distance, pulling it out to the side
asks for the vertical one.

"Clear of the points" is measured against their extent, not their
midpoint. A dimension line dragged straight up from near one end of a
long pair is still above them even though it is a long way from the
middle, and a midpoint test would call that sideways. When the location
is inside the pair on both axes it has not said anything yet, so the
longer side still decides.

Nothing else needed changing: the measurement already projects onto the
rotation axis, and the block builder already derives the dimension line
and both extension lines from it.

Closes #669

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 00:34:03 +03:00
Hakan SevenandClaude Opus 5 1a753e821c refactor(snap): find both tracking directions in one pass
Perpendicular tracking arrived as a second full scan of the drawing's
segments, run next to the edge-direction scan on every acquisition. The
two ask different questions of the same segments — does this one end
here, does this one pass through here — and the scan is the expensive
part, so they now ask together and the drawing is walked once.

Two things fall out of sharing the walk. The perpendicular path picks up
the non-finite guard the edge path already had, so a NaN vertex can no
longer reach the projection. And a perpendicular is now skipped when the
edge rays already cover its direction: at a square corner one segment's
perpendicular is the other segment's own ray, and two rays down the same
line are two identical-looking things for the cursor to choose between.

Behaviour is otherwise unchanged — same tolerance, same near-parallel
dedup, same cap of six each, same skipping of round wires.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 00:23:53 +03:00
Hakan Seven 2920c4deee Merge pull request #696 from gianlucafiore/fix/695-perpendicular-tracking
Add perpendicular object snap tracking
2026-08-08 00:18:43 +03:00
Hakan SevenandClaude Opus 5 a4698c5b6b fix(units): leave the sheet viewport alone when converting
DWGUNITS reframed every viewport by the conversion factor, on the
reasoning that a viewport's framing is measured in model units and has to
follow them. That holds for a window onto the model; it does not hold for
a layout's sheet viewport, whose view is the paper itself, in paper
units. Scaling it left the sheet the size it was while the view of it
moved by a thousand.

Only a paper-space viewport can be a sheet — the model-space scale
reached everything else, and what it reached frames the model by
definition. Among the rest, the layout names its sheet outright, and a
file arriving without that link still gives itself away by sitting at the
paper origin, where only the sheet sits.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 00:11:57 +03:00
Hakan SevenandClaude Opus 5 62dfbfe2be fix(paper): give the sheet back its desk and its edge
A paper layout painted its whole canvas in the page colour, so the sheet
had no visible edge — zooming out never reached one, because the surround
was the same colour as the page. Floating viewports then painted
themselves black over it.

Both come from the same idea, that a viewport's canvas is a colour it can
be handed. What lies behind a viewport depends on what the viewport is:

- The full-canvas sheet shows the desk. `paper_bg_color` is the page, and
  the page is a bounded rectangle `paper_sheet_fill` already draws; using
  it for the canvas too erased the boundary between them. The desk is now
  its own colour, defined once in the scene and read by both the widget
  and the renderer, so the two cannot drift apart.
- A floating viewport sits on that sheet and must paint no canvas at all.
  Handing it a transparent one does not work: the background shader
  returns an opaque colour whatever alpha it is given, so alpha zero came
  out black. Skipping the pass is the only way to ask for see-through, so
  `skip_background` now suppresses both the clear and the pass. It is part
  of the frame signature too — without that, a cached frame would keep
  showing the canvas the flag was just turned off for.

Shading and fog still blend toward what is behind a floating viewport,
which is the page, so the uniform keeps that colour even though nothing
paints it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 00:11:47 +03:00
Hakan SevenandClaude Opus 5 6cbec232bb feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.

parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.

parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.

ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.

The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.

The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 23:46:49 +03:00
gianlucafiore b59fa03b53 Add perpendicular object snap tracking 2026-08-07 15:14:02 -03:00
Hakan SevenandClaude Opus 5 523b85f42b fix(units): make the units selector reach what it names
The status-bar units button wrote INSUNITS, which nothing on screen reads.
INSUNITS only tells the drawing how to scale content arriving from
elsewhere, so picking a unit relabelled the drawing and changed nothing
visible — the selector looked broken because the variable it moved was
invisible.

Point the button at LUNITS instead, the setting that decides how every
coordinate and distance is written, and give the three questions the word
"units" runs together a place each:

- The button offers the five length formats, each shown beside a sample of
  itself, so Engineering and Architectural are told apart by sight.
- UNITS / DDUNITS opens a real dialog: length and angle format and
  precision, the direction angle zero points, whether positive angles run
  clockwise, and the insertion unit. Its sample comes from the same
  formatters the drawing uses, so it cannot drift from the readout.
- DWGUNITS converts. Model geometry, viewport framing, the header's length
  defaults and each dimension style's scale all move together in one undo
  step; paper space is left alone, since the sheet is still the same sheet.
  Locked layers are converted too — a half-converted drawing would be at
  two scales at once.

The coordinate readout formatted at four decimal places whatever the
drawing asked for; it now goes through the same format_length the
properties panel uses. That made the angle formats reachable in turn, so
degrees-minutes-seconds now carries the d/'/" marks that say which
convention it is, and surveyor's units are written at all.

Typed input is still plain decimal: a length the readout writes as
5'-6 1/2" cannot yet be typed back. ANGBASE and ANGDIR are stored but not
yet applied to displayed directions.

Closes #668

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 19:59:09 +03:00
Hakan SevenandClaude Opus 5 cfc7c06c6c fix(scale): keep the standard scales when the first one is written
A drawing that owns no annotation scales is shown the standard set, which
`scale_list` substitutes whenever the drawing's own list is empty. The
first thing to need a real scale object — an annotative dimension, a pick
from the status bar, a viewport scale — wrote just the one it wanted. The
drawing then had a list of its own, of exactly one entry, so the
substitution stopped and every other scale disappeared from the picker.

Bring the whole set in before writing that first scale. It is the set the
drawing was already being shown, so nothing appears that was not on offer
a moment earlier, and the drawing ends up owning its scales the way one
that shipped with a list does.

The seeding itself already existed for the scale manager, which stages it
on open; it simply was not reachable from the path that writes a scale.

Closes #666
Closes #683

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 18:19:41 +03:00
Hakan SevenandClaude Opus 5 26b4f29a13 feat(options): offer the file association in Options
Registering as the .dwg/.dxf handler was reachable only by typing
FILEASSOC, which is also the only thing that installs the icons the
desktop shows — so a user with no reason to know the command had no way
to arrive at either.

The checkbox makes the same two calls the command does, so the stored
setting and the actual registration cannot drift apart. A registration
the system refuses puts the checkbox back rather than leaving it showing
a state that is not true.

The line under it says the icons come with it, since that is what sends
people looking for this in the first place.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 17:51:06 +03:00
Hakan SevenandClaude Opus 5 7c8271c716 fix(icon): draw every icon from the logo file
Two places still showed the old mark after the logo was redrawn.

The window icon was built by drawing the letterforms stroke by stroke in
code, background colour and all, which made it the one icon in the
application not coming from assets/logo.svg. It now rasterises that file,
fitted to the square without distorting whatever aspect the logo has. It
returns nothing when the logo cannot be rendered, so the window falls back
to the platform default instead of to something invented here -- which is
how it ended up stale in the first place.

The icon installed into the icon theme was only ever written from inside
the desktop-entry write, behind that write's own "nothing changed" early
return. The entry's text does not change when the logo does, so once it
had been written the icon was never refreshed again. The two change for
different reasons; the icon is now checked first and on its own terms. It
already compares bytes, so an unchanged logo still costs only a read.

Worth knowing on Wayland: the compositor ignores the window icon entirely
and takes the dock icon from the desktop entry, so there the installed
theme file is the only one that matters -- and it refreshes when the file
associations are next set up, not at launch.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 17:37:59 +03:00
Hakan SevenandClaude Opus 5 36e328f7d7 feat(layers): add LAYTRANS to translate layers onto a standard
Drawings arrive from elsewhere carrying someone else's layer names, and
folding them into a set of house layers meant renaming and merging by
hand, one layer at a time.

LAYTRANS reads a layer set out of another drawing and moves this one onto
it. Given a file it maps every name the two already share and translates
at once, which is all most incoming drawings need. Bare, it opens a
translator: the drawing's layers on one side, the loaded set on the
other, mappings built up between them and applied only on Translate, so a
mapping can be reconsidered and the whole translation is a single undo
step. Mappings can be saved and read back, objects can be forced to
ByLayer so the target layer's properties are what actually shows, and the
result can be written out as a log.

The move itself is the one LAYMRG already made for a single pair, so
LAYMRG now goes through it too rather than keeping its own copy.

Three things fell out for free or nearly so. A standards file is a
drawing, so .dws and .dwt need nothing but a place in the file filter.
The entity list is flat and holds block definitions, so objects inside
blocks travel with everything else instead of being stranded on a layer
that no longer exists. And a target layer that this drawing already has
is left alone: the drawing has an opinion about it, and restyling it
would reach past what was asked for -- only newly created targets take
the standard's properties.

Mappings are saved beside the drawing rather than inside a DWS. That is
where AutoCAD keeps them, but putting them there would mean inventing a
place in the drawing that nothing else reads back.

Closes #624

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 16:51:15 +03:00
Hakan SevenandClaude Opus 5 f7590b1aa8 feat(plugin-api): let add-ons ask for a visual style by name
Removing the binary wireframe event left add-ons with no way to set a
visual style except by spelling out a command string. Give them the
intent back, in the vocabulary everything else now speaks.

The name is carried as text rather than the render-mode type: this crate
is dependency-free by default and only pulls acadrust in behind the host
feature, which is not a weight to add to the manifest contract for one
enum. The host resolves it against the same list its ribbon, its picker
and its commands read, so an add-on can offer exactly the styles the
application does and no others. An unknown name says so and lists the
ones that exist, from that same list.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 14:29:01 +03:00
Hakan SevenandClaude Opus 5 b5a1146966 feat(view): one list of visual styles behind every way of choosing one
The ribbon offered four styles and dispatched each one's id as a command.
Two of those ids were not visual-style commands: "Shaded" carried SOLID,
which draws a 2D filled polygon, so choosing it started a draw command;
"Hidden" carried HIDDEN, which matched nothing at all, so it did nothing.
The intent had been for the button to fire its tool's event, but the
dropdown never reads one -- it dispatches the id.

Behind that sat two generations of the same feature. The render-mode
picker offered the seven styles a viewport can actually be drawn in;
everything else -- the ribbon, VSCURRENT, SHADEMODE, VISUALSTYLES and a
handful of bare verbs -- went through a binary wireframe-or-shaded flag
that could only approximate them, reporting "Hidden (shown shaded)" and
"X-Ray (shown as wireframe)" when asked for something it had no way to
draw. Four descriptions of one choice, each drifting on its own.

There is one list now. Each style names itself once -- mode, label, icon,
and the command that applies it -- and the ribbon, the picker, the
VISUALSTYLES verb and the interactive prompt all read from it, down to
the line that lists the choices, so what is offered cannot disagree with
what works. Keywords are the render modes' own names, since there is one
set of styles left to name.

The binary path is gone rather than kept alongside: its message, its
module event, the tool definitions that produced it, the ribbon's
special-cased highlight arms, the bare style verbs and the older keyword
spellings. X-Ray goes with it -- no render mode draws one, and it was
already coming out as a plain wireframe. Three files that had been left
holding a single icon constant each fold into the list.

Note: ModuleEvent::SetWireframe leaves the plugin API with it.

Closes #621

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 14:15:59 +03:00
Hakan SevenandClaude Opus 5 5c5c7606a1 feat(select): offer the selection modes at "Select objects"
Only Previous and Last were on offer, so Window, Crossing, Fence, the two
polygons and All could not be reached at all -- and with no keyboard to
type them into, the buttons were the only way in.

Window and Crossing fix the sense of the next box. Dragging derives that
from the direction the corner travels and would overwrite it the moment
the corner moved, so the derivation stands down while a keyword holds it.
Add and Remove decide what a pick does for the rest of the round: Remove
subtracts the way Shift already does, rather than through PICKADD, which
decides whether a pick *replaces* the set and so would have wiped
everything gathered so far. Both are dropped when the next command
starts, since neither outlives the selection that asked for them.

Fence and the polygons are picked point by point, which TRIM and EXTEND
already ask for. That gesture -- the points, the rubber-banded dashed
preview, whether the path closes -- moves into one place both bind to,
and the two preview builders duplicated in trim.rs go with it.

A fence is not the polygon test with crossing set: that closes the point
list and counts whatever lies inside, so a line drawn past a row of
objects would take everything behind it too. It gets a hit test that only
counts a real intersection, and skips the area-based geometry entirely.

The lasso gathered its hits from wires, hatches, block hatches, meshes and
block meshes inline; the new modes need the same list, and a second copy
would drift. Both now come through `Scene::path_hit_handles`.

Closes #596

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 13:32:11 +03:00
Hakan SevenandClaude Opus 5 22890a9411 ci: keep the issue-welcome run in Actions history
The job deleted its own run once the comment was posted, so a failure to
post left nothing behind to look at. Keep the run, and drop the
`actions: write` permission it needed only in order to delete itself.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 12:02:16 +03:00
Hakan SevenandClaude Opus 5 ca152de1c0 refactor(text): let acadrust carry the rotation through a transform
The merged fix added the rotation update on this side, matching only
`EntityTransform::Rotate`. That left `Affine` -- which several entity
paths and the UCS world transform emit -- still dropping the angle, and
it sat against the contract this crate already relies on: MIRROR is
delegated precisely because acadrust's entity-aware paths "handle
direction flags, stored-angle conventions and bulges themselves", and
`mirror_text` did exactly that for the same field.

Moved upstream (acadrust 9ecaffc), where one helper carries the angle for
every entity that keeps one. That covers rotation, affine and reflection
in a single place, and closes the same gap for SHAPE and the ATTRIB pair,
which a rotation left facing the old way too -- attributes noticeably so,
since INSERT already turned itself while its attributes did not.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 11:48:53 +03:00
Hakan Seven 05f875b1ff Merge pull request #692 from gianlucafiore/fix/690-text-rotation
Fix text rotation transforms
2026-08-07 11:40:12 +03:00
Hakan SevenandClaude Opus 5 0af6b470ed refactor(snap): measure typed distance along the guide that is drawn
Extension answered "which acquired ray is this snap on?" twice, from two
different tests: the overlay picked its guide by off-ray distance in
screen pixels, while typed-distance entry re-searched the tracking set in
world space. Two tests can disagree, and the ways they disagree are both
bad -- a guide on screen that typing ignores, or a distance running off
along a ray the user cannot see. The second search could also come up
empty on its own, since only four acquisitions are kept and the oldest is
dropped as new ones arrive.

Ask once. The search that draws the guide now also reports the endpoint it
drew from, the snap carries it, and typed distance reads the ray straight
off that -- the direction is just the way from the endpoint to the snap,
which sits beyond it by construction. The screen-space test stays the only
one, which is what the guide wanted anyway: it stays scale-independent
where a world test would not.

Also correct the tracking-point doc: it claims XZ with Y=0, but
`edge_dirs_at` builds every direction from X/Y with Z zeroed and the rays
follow it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 11:29:15 +03:00
Hakan SevenandClaude Opus 5 b8f9727a27 fix(grip): stop double-offsetting the UCS grip snap marker
The extension anchors needed the same pane-to-canvas lift the snap
marker already got, but the block that added it was placed next to the
one that was already there rather than merged into it, so `screen` took
the tile origin twice. A single full-canvas tile has no origin to add, so
it only shows once the viewport is split. Fold the anchors into the
existing block.

Also require the ray to pass through the snapped point before offering it
as a distance reference. Only four acquisitions are kept and the oldest is
dropped as new ones arrive, so the ray that produced a snap can be gone by
the time a distance is typed; picking the nearest survivor unconditionally
sent the point off along an unrelated ray instead of falling back to plain
point input. `extension_snap` projects onto the ray in exact f64, so the
tolerance only has to clear rounding, scaled with the coordinate to hold at
UTM magnitudes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 11:22:04 +03:00
Hakan Seven 687bc2079d Merge pull request #691 from gianlucafiore/fix/grip-direct-distance
Support tracking and direct distance input for grip editing
2026-08-07 11:16:49 +03:00
Hakan SevenandClaude Opus 5 e5974a8895 fix(setvar): refresh geometry the variable is baked into
LTSCALE scales the dash pattern baked into every wire, and PDMODE /
PDSIZE decide the point glyph built at tessellation time. Each has its
own command that invalidates for exactly that reason -- and PDMODE /
PDSIZE are also reached from the point style dialog and the viewport
setting, both of which invalidate too. Only SETVAR, the fourth way in,
set the header and stopped there, so the drawing kept rendering the old
value until some unrelated edit happened to rebuild it.

Invalidate the same way each variable's own command does, alongside the
TEXTFILL case already handled here.

The rest of the SETVAR list needs nothing: FILLMODE is read at render
time and hashed into viewport state, CELTSCALE and TEXTSIZE only seed
new entities, SPLFRAME is never read outside this file, and the others
are behaviour rather than geometry.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 11:10:42 +03:00
gianlucafiore 4dbe9775eb Fix text rotation transforms 2026-08-07 00:16:48 -03:00
gianlucafiore 62457be90e Support tracking and direct distance input for grip editing 2026-08-06 23:35:58 -03:00
gianlucafiore 6752df6ea4 Fix linetype refresh and circle rendering 2026-08-06 23:35:16 +03:00
Hakan SevenandClaude Opus 5 30b8974b73 fix(solid3d): pick up the SAT record-splitting fix
A drawing whose bodies are built only of planes opened without error but
drew nothing, while the solids still selected. Pre-7.0 SAT writes strings
as a length and the characters, so one may contain the record
terminator; ODA's builder emits `8 STEP Id#` inside a dxid-attrib. The
parser split there, every later record answered to the wrong index, and
faces resolved their surface pointer to a coedge or a vertex, so nothing
meshed.

Fixed upstream in acadrust 41149d7. All 22 solids in the reported drawing
now parse to exactly the record count their header declares, and all 22
mesh.

Closes #670

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 23:27:18 +03:00
Hakan SevenandClaude Opus 5 5c7aeca321 fix(solid3d): close torn cylinder walls from truck revolutions
A revolution comes back from rsweep as three 120-degree segment faces,
but every hole was attached to the first of them. A window wider than a
segment cannot sit on one face, so that face turned invalid and its
whole third of the tube vanished -- the pump's cylinders lost 120
degrees of wall each and the solid read as open where they met the body.
One of them shed a segment with no hole involved at all, so chasing the
individual causes would not have covered it.

Measure the result instead: after a cone face whose boundary closes the
revolution is triangulated, check that the meshed vertices actually span
it, and hand the face to the parametric sampler when they do not. That
catches a tear whatever produced it, while genuinely partial cones and
every non-cone surface pass straight through -- the tee's pierced run
cylinder still meshes through the kernel.

Also bring truck's chord tolerance in line with the detailed LOD. Truck
emits a single mesh rather than an LOD ladder, so a tenth of the radius
left a wide pipe as a hexagonal prism whose flats sank nine units inside
the true surface, pulling the wall away from the caps and planar faces
that meet it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 22:29:41 +03:00
Hakan SevenandClaude Opus 5 2a1a8f4b94 fix(solid3d): read cone radius from the major axis
The cone-surface record's trailing real is a parameter scale, not
geometry. It equals the radius on many bodies, so cylinders generally
came out right, but where the two diverge the lateral surface was built
at the wrong size while the flat caps stayed on their own rims -- a
gauge dial drew as a ring around its own face, its band at 43.2 against
caps at 34.56.

The major axis is the radius vector at the reference cross-section, so
its length is the radius. Route every read through one helper and keep
the record's field only as a fallback for a degenerate major axis, so
the band, the silhouette generators, the isolines and the coaxial-rim
check all size themselves the same way. Taper is unchanged: the major
axis is still the h=0 radius that radius + h*tan(a) extends.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 22:00:22 +03:00
Hakan SevenandClaude Opus 5 01d8b87db1 fix(solid3d): trim sphere caps and pierced cone legs
Sphere faces drew as complete balls. ACIS parametrises a sphere about
any pole, so a vessel's dished head is commonly bounded by a circle that
is no line of latitude in that frame, and a theta/phi bounding box of
such a boundary covers nearly the whole ball. Derive the window
analytically instead: when a face's loops are circles lying on the
sphere, re-pole the frame onto their common axis, where each seam is an
exact latitude at acos(d / R). One ring bounds a cap, two bound a zone;
the material side comes from the boundary winding, reusing the helper
the torus path already had.

Separately, the longitude wrap flag was resetting the colatitude window
to the whole meridian. A cap centred on the pole covers every longitude
while still ending at its seam, so that reset grew every cap back into a
ball. Trim the two angles independently and rename the flag theta_full
so the split reads at each use, including the silhouette generator,
which skipped the colatitude test the same way.

Cone faces took their height span from the first boundary loop alone. A
lateral face closed off by one rim and one pierce curve -- a pipe branch
meeting its run -- carries its two ends on different loops, so the tube
was sized to whichever came first and the leg above the junction went
undrawn. Read every loop.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 21:44:49 +03:00
Karim Jerbi 8f009d0058 feat(insert): add docked block palette
Add a searchable block palette with previews, file import, collision-safe nested definitions, and dependency/table-record import. Keep palette thumbnails current across drawing switches and block-definition edits.
2026-08-06 11:02:52 +03:00
Hakan Seven 67a767e23b fix(view): pause hover during navigation 2026-08-06 00:21:44 +03:00
Hakan Seven afcf7baead fix(solid3d): honor torus trim winding 2026-08-06 00:07:25 +03:00
Hakan Seven c45daeffb6 feat(render): add scene environment pipeline 2026-08-05 23:42:18 +03:00
Hakan Seven b667eeeb05 refactor(render): unify scene rendering paths
Route model, paper, viewport and nested block content through one traversal while preserving leaf renderers. Carry decoded material state into mesh shading and harden solid geometry conversion.
2026-08-05 21:44:59 +03:00
Hakan Seven afc94dce13 feat(qselect): expand filter workflow
Refs #511
2026-08-05 13:40:53 +03:00
Hakan Seven e07f4f959b fix(view): preserve projection framing
Use full 3D bounds to derive a safe perspective eye distance without changing the camera target, orientation, or field of view.

Refs #502
2026-08-05 12:47:46 +03:00
Hakan Seven 5a0c286a10 chore(release): prepare v0.9.3 v0.9.3 2026-08-05 01:03:51 +03:00
Hakan Seven deaecdfb1e fix(ribbon): mask tools on start
Refs #494

Refs #507
2026-08-05 00:52:52 +03:00
Hakan Seven b92b8df997 fix(trim): handle lasso and box selection
Keep selection gestures in the active trim flow so crossed segments are modified instead of selected.\n\nRefs #487
2026-08-05 00:26:29 +03:00
Hakan Seven 474bfcc7db fix(input): restore arrow key navigation
Route editor and command-line arrows by active context and recall successfully dispatched commands.\n\nRefs #485
2026-08-05 00:08:35 +03:00
Hakan Seven 657d023fb7 feat(ucs): integrate coordinate systems
Route coordinate input, geometry creation, transforms, properties,\nviews, and viewport persistence through a shared working plane.\n\nRefs #484
2026-08-04 23:17:54 +03:00
Hakan Seven b1d55c6a7e feat(view): add visual style previews
- preview render modes on hover without committing
- distinguish 2D and 3D wireframe fill and depth behavior

Refs #483
2026-08-04 21:23:25 +03:00