Commit Graph
199 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 3be9d8925e fix(point): match AutoCAD PDMODE glyph geometry
The mode-4 tick was drawn as a full vertical line centred on the point;
AutoCAD draws it rising from the point upward by PDSIZE/2. The '+' and
'×' arms were half size — AutoCAD extends them to the full PDSIZE so the
cross pokes out past an enclosing circle/square (which sit at PDSIZE/2).
Ground-truthed every PDMODE combination against ezdxf's point renderer.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 17:07:27 +03:00
Hakan SevenandClaude Opus 4.8 50f6196594 fix(render): flag 2D overlay fills so they show in every view mode
A SOLID's filled quad and a dimension/MultiLeader text background are flat 2-D
overlays that should draw in wireframe and shaded alike, but face3d_gpu
classified fills by `!fill_tris_low.is_empty()` — and a 2-D fill at UTM scale
carries a double-single low residual for precision just like a real 3-D mesh
face. So the arrowhead fill and the "5;8" cyan text background were routed to
the 3-D buffer and only appeared in shaded mode, vanishing back to nothing on
return to wireframe.

WireModel now carries an explicit `fill_is_3d`: true only for a real surface
(PolyfaceMesh / PolygonMesh face, set in the tessellator), false for every flat
overlay fill. face3d_gpu classifies on the flag, so 2-D fills stay in the
always-visible buffer while keeping their low residual (and UTM precision).

Verified on budweiser: the `_BoxFilled` arrow SOLID and the dim '5;8' MTEXT
background both tessellate with fill_is_3d = false.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 15:15:24 +03:00
Hakan SevenandClaude Opus 4.8 015973babb fix(solid): fill 2D SOLIDs so block-nested ones aren't hollow
A 2D SOLID's `to_truck` emitted only its 4 perimeter edges; its fill came from
a solid-fill HatchModel the scene injects for top-level SOLIDs. That injection
never runs for a SOLID nested in a block, so a filled arrowhead like the
`_BoxFilled` dimension arrow rendered as a hollow box outline.

`to_truck` now also emits the two fill triangles (visual quad p0→p1→p3→p2, the
second degenerate for a triangle SOLID). Block expansion carries fill_tris, so
the arrowhead — and any SOLID inside a block — renders filled; the dimension /
MultiLeader / mesh 2-D fill pass already routes these to the flat fill batch.
Top-level SOLIDs also keep their HatchModel fill; the duplicate is invisible for
the opaque solids these are.

Verified on budweiser dim 3728: the `_BoxFilled` arrow SOLID now tessellates to
fill_tris (2 triangles) instead of an empty fill.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 14:15:13 +03:00
Hakan SevenandClaude Opus 4.8 b655bd232b feat(ole): render the OLE2FRAME embedded presentation bitmap
An OLE2FRAME drew only a rectangle-with-X placeholder. AutoCAD shows the OLE
object's cached presentation image, which the DWG stores as a BMP embedded in
the OLE data blob (a BITMAPFILEHEADER "BM" + DIB).

`ImageModel::from_ole2frame` scans the blob for a self-consistent embedded BMP
(validating the DIB-header size, so a metafile/other-format OLE cleanly falls
back), decodes it with the `image` crate, and builds the textured quad at the
frame rectangle with a per-corner double-single split so it stays precise at UTM
scale. The scene's image collection now feeds OLE2FRAME entities through it
alongside RasterImage. The placeholder's diagonal X is dropped — only the frame
border is drawn now, so it no longer crosses over the bitmap.

Verified on budweiser handle 3088: the 32×32 embedded bitmap decodes to RGBA
with the expected colours (yellow upper-left, blue centre, red lower-right),
upright and positioned at the frame corners.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 13:33:46 +03:00
Hakan SevenandClaude Opus 4.8 db896ec632 fix(spline): interpolate fit-point splines with end tangents, grip via fit points
A fit-point SPLINE (no control vertices) was drawn with Catmull-Rom through its
fit points, which ignores the stored begin/end tangents and uses local slopes.
An open curve then peeled away from the true shape at its ends — budweiser's
handle 2735 lost the tangent-driven overhang at its top end.

A fit spline is the C² cubic that passes through every fit point clamped to the
stored end tangents. Replace Catmull-Rom (kept only for closed splines, whose
wrap the clamped solve doesn't model) with `fit_spline_polyline`: chord-length
parameterisation (honouring knot_parameterization), a tridiagonal Thomas solve
for the knot slopes with clamped-tangent or natural end rows, and Hermite
evaluation. Verified on 2735: the curve passes through all five fit points and
its ends follow begin_tangent/end_tangent (dir·tan = 0.997).

Also fixes grip-selection: grips()/apply_grip() only ever looked at
control_points, so a fit spline (0 CV) showed no grips and couldn't be
grip-selected or edited. Both now fall back to fit_points when there are no
control points — dragging a grip moves the fit point and the curve re-fits on
the next tessellation, keeping the end tangents. The mirror path already
transformed fit_points; move/rotate/scale run through acadrust.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 12:46:09 +03:00
Hakan SevenandClaude Opus 4.8 0ce150411a feat(solid3d): draw ISOLINES on curved faces in wireframe
A cylinder's wireframe showed only its two rim circles, so from the side it
read as two disconnected ellipses rather than a cylinder. AutoCAD draws
ISOLINES — a fixed set of longitudinal lines on each curved face — so the
surface reads from any angle.

`collect_isolines` walks the cone-surface faces (cylinder and cone), recovers
each face's height and angular extent with the same logic `tess_cone_face`
uses, and emits `header.isolines` (default 4) longitudinal segments from the
bottom rim to the top rim. They are appended to the feature-edge line list, so
they ride the existing body transform, double-single split, and top-level /
per-INSTANCE re-split for free.

Being part of `edge_verts`, they inherit the edge pass's gating: drawn in the
wireframe modes and in shaded-with-edges, hidden in plain shaded — matching
where AutoCAD shows isolines. `isolines` is threaded from the header through
the tessellation entry points alongside `facet_res`.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 19:45:42 +03:00
Hakan SevenandClaude Opus 4.8 292e23f44f fix(mtext): size \S fractions to fill and persist paragraph alignment
Two layout fixes on the same MTEXT (budweiser2018.dwg handle 2609 / 8782).

Fraction size: `\H0.7x;\S1/2;\H1.429x;` is how a 70 %-scaled fraction is
spelled, so the 0.7 already *is* the fraction's size — the earlier constants
shrank each half again on top of it and buried the stack inside one line.
A half is now near the run height and the stack stands about twice that: the
numerator rises past the cap and the denominator sits on the baseline, the way
a fraction is meant to grow past its line rather than be squeezed into it.

Alignment persistence: a `\p...;` block stays in force until the next `\p`, so
a paragraph opened by a bare `\P` / `\N` inherits it. The parser records only
the literal codes, so the second line of a centred or right column
(`\pxqc;col2\P≡`) fell back to the left edge while its heading stayed aligned.
Layout now carries a paragraph's whole property block forward to any paragraph
that sets none of its own — measured, the column symbols land on the same
centre / right edge as their headings.

Kept in the layout adapter rather than the parser: acadrust faithfully records
which paragraphs carry an explicit `\p`, so persistence is a rendering
interpretation, and the literal record stays available to an editor.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 18:26:29 +03:00
Hakan SevenandClaude Opus 4.8 7e1cd6842b feat(mtext): lay out \N columns and stack \S fractions
Two things an MTEXT can ask for that the layout ignored, both found on the
same drawing (budweiser2018.dwg handles 8782 and 2609).

Columns: `column_data` was read but only ever reached the properties panel,
so a three-column MTEXT ran as one stacked block. Each line now carries the
column it belongs to, the row counter restarts per column, and wrapping
measures against the column rather than the block — measuring against the
block would let a line run across the gutter and over its neighbour.
Paragraph alignment already worked, which is what made the bug read as
"columns are there but misplaced": `\pxqr;` pushed col3 to the right edge
either way.

Only explicit `\N` breaks move to the next column. A column that fills up
does not spill into the next on its own — `heights` / `auto_height` are not
read, so a column runs as long as its content.

Stacking: `\S1/2;` was flattened to the literal "1/2", which is why the
line then overflowed its 7.26-wide box and dropped `°C ª` onto a second
row. The halves now share one horizontal slot, drawn at a shrunk height
with the numerator lifted, so the slot is as wide as the wider half. `^`
stacks without a rule; `#` stays inline `num/den`, which is what a slanted
fraction reads as and needs no second baseline.

The fraction rule rides a run-less stroke group: a group carrying a
`GlyphRun` renders as SDF quads with its strokes suppressed, a run-less one
keeps them, and the rule is plain geometry rather than a glyph.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 16:48:10 +03:00
Hakan SevenandClaude Opus 4.8 4f1e710b35 fix(pick): catch a wide polyline on its band, not just its centreline
A wide polyline draws a solid band, but hit-testing only ever saw the
centreline 8 px out, so a donut could only be selected by clicking the
phantom circle inside the disc it paints. `self.hatches` — the map the
hatch pick channel walks — is filled from HATCH and SOLID alone, so the
band `wide_fills` produces was drawn (via `synced_hatch_models`) and never
picked.

The band rides in on `pick_tris`, the same channel a thickness wall uses:
`wide_fills` already computes it, so it costs one tessellation and inherits
the block transform. It ear clips rather than fans — an arc band is an
annular sector, concave on its inner edge — reusing `triangulate_planar`.

Wiring `pick_tris` through `Lines` alone was not enough: a plain wide
polyline leaves `to_truck` as `Contour`, and a PLINEGEN=0 one as
`SegmentedLines`. Both now carry it.

This needs the wire's cullable box to cover the band, and it did not.
`bounding_box()` boxes a polyline's stored vertices, knowing nothing of the
width painted around them: a donut's two vertices sit on one horizontal
line, so the box had zero height and rejected every click on the disc
before any triangle was tested. Every site that stamps the entity box onto
a wire now goes through `set_wire_aabb`, which widens it over `pick_tris`
(a no-op for the entities that have none, which is nearly all), and the
block cache chains `pick_tris` into its own box for the same reason.

That box was already too small for the centreline itself — an arc leaves
the vertex box — so a donut was unselectable at its top and bottom at
enough zoom. Widening fixes that too.

Verified end-to-end: tessellate a width-0.5 donut on a 0.05 centreline
radius, then `click_hit` five points 20 px clear of the centreline — all
land, and a click outside the disc still misses.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 15:22:56 +03:00
Hakan SevenandClaude Opus 4.8 07d7888b94 fix(render): halve polyline width — wide bands drew twice as wide
DXF codes 43 / 40 / 41 store a polyline band's full width, and
`polyline_segment_fill` offsets ±hw about the centreline, so the stored
value has to be halved. Both `wide_fills` passed it in whole and drew
every wide polyline at double width.

The codebase was writing the spec value and reading it as something else:
`make_donut` stores `outer_r - inner_r`, a full width, which `wide_fills`
then treated as a half-width. So OCS mis-drew even its own donuts.

Seen on budweiser2018.dwg handle 2579, a donut of width 2.2 on a 1.1
centreline radius: it should fill out to radius 2.2 and instead reached
3.3 — a disc 1.5× its real size, overflowing the square it sits in.

Affects every wide polyline, so TRACE and PEDIT-widened runs come back to
true size too. The writers already stored full width and are left alone,
as is the property panel, which shows the file's own value.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 15:00:06 +03:00
Hakan SevenandClaude Opus 4.8 3f060a570a fix(pick): hit-test lineweight width and thickness walls
Selection tested a flat 8 px radius around each wire's centreline and
nothing else, so two things the user can see were not pickable.

A wire renders as a band `line_weight_px` wide, so pick now widens to that
rendered half-width via `pick_tolerance_px`, mirroring the wire shader's
`select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for
every standard weight (the widest, 2.11 mm, renders 7.97 px half-width —
just under the 8 px threshold); this keeps the two sides from drifting
apart if the display boost in `lineweight_to_px` ever changes.

Entities extruded by a DXF thickness draw their wall as four edges with
nothing in between, so the interior was a hole the cursor fell through.
WireModel gains `pick_tris`: triangles hit-testing treats as solid and the
renderer never uploads. Kept apart from `fill_tris` because that channel
reaches the GPU, and a wall that rendered shaded would change how every
thickness drawing looks. Ranked below `fill_tris` — those are drawn, these
are not, so where they overlap the drawn face is what the user means.

Walls are dropped when a wire is projected into a floating viewport:
`points` are reprojected and clipped into paper coords there, so cloned
model-space triangles would hit-test at a model-scale offset. Extruded
entities stay selectable by their edges, which do get projected.

The wall loop runs on every hover that misses everything else, so it
rejects on the wire's AABB first — an extruded circle is ~128 triangles
and a drawing full of thickness would otherwise project every wall on
each mouse move.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 14:58:55 +03:00
Hakan SevenandClaude Opus 4.8 5bd683cf4e feat(mline): GPU-dash multiline elements with shared A-type alignment
MLINE dashed elements were fully CPU-expanded (apply_along) — one wire per
dash — to keep the parallel lines in phase. Move pure-dash elements onto the
GPU dash shader and end-align every element to the multiline's centre-line so
they share one interior phase.

- wire_distances now accumulates arc-length from the double-single (high+low)
  points, so the dash phase stays precise at UTM coordinates. f32-high-only
  deltas quantised ~0.1 there and drifted the parallel lines apart — the
  original reason MLINE dashes were CPU-side.
- New WireModel.dash_align_end carries a shared begin/end dash length. The
  shader uses it as align_end for every parallel element (the interior phase
  depends on align_end, not the wire's own length) while align_total stays the
  element's own length, so each still ends on a dash at its own endpoint.
- MLINE tessellation derives that length once from the centre-line, routes
  pure dash/space/dot elements to the GPU pattern and text-bearing elements to
  apply_along.
- apply_along gained an "A"-type mode (shared reference length): a solid
  begin/end dash and a phased interior, so its dashes — and embedded text —
  line up with the GPU-dashed sibling elements instead of tiling from start.

Pure-dash MLINEs now cost one WireModel + pattern per element instead of N
segments; single (non-MLINE) entities keep the from-start tiling unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 02:59:11 +03:00
Hakan SevenandClaude Opus 4.8 48e6d0d553 feat(dgn-ls): dash walls from start + fix wall connector and cap identity
DET pipe walls now carry the stroke pattern (dash_from_start), which
tiles from each wall's own start vertex and skips the A-type end
alignment that was force-lighting a solid dash under the end symbol.

Two rendering fixes on top:
- Dedup the host polyline's vertices before offsetting the walls. Some
  DGN pipe polylines carry trailing duplicate end vertices; a zero-length
  segment gave offset_xy a degenerate normal, leaving that vertex
  un-offset and folding the wall down to the centre line. The two walls'
  fold segments met at the centre and drew a spurious line linking them.
- Re-key the symbol (end circle / half-ellipse) wires to the host handle
  so the whole pipe picks and selects as one entity instead of the
  symbols acting as separate phantom entities.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 01:49:18 +03:00
Hakan SevenandClaude Opus 4.8 0e18b5bb33 feat(mline): render multilines from their MLINESTYLE + complex linetypes
Multilines were drawn with a fixed ±scale/2 two-line guess in the entity
colour, ignoring the MLINESTYLE entirely, so a drawing's custom style (per-
element offsets, colours and linetypes) was lost — a red/yellow multiline
rendered as a white box.

- mline.rs: resolve the MLINESTYLE (by style_handle, then name) and build one
  parallel line per element from its offset x scale + justification shift.
- tessellate.rs: special-case MLINE (like Leader) to emit one WireModel per
  element in the element's own colour, CPU-dashing every element through
  apply_along so the parallel lines stay in phase.
- linetypes.rs: render a drawing's OWN complex linetypes (embedded text/shape),
  document definition first and the bundled .lin catalog only as a fallback, so
  a file-defined text linetype like PLYN shows its text.
- Run the dash/glyph layout in a local frame (f64 origin subtracted, re-split to
  double-single after) so spacing stays precise at UTM coordinates, and pass
  tracking = 1.0 so embedded text keeps its letter spacing.

Cargo.lock pins acadrust to the MLINE->MLINESTYLE handle-link reader fix.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-16 18:44:15 +03:00
Hakan SevenandClaude Opus 4.8 e9976270f6 style(tolerance): drop an unneeded mut on the cell-run closure
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-15 22:55:51 +03:00
Hakan SevenandClaude Opus 4.8 6e2195d62f fix(mtext): anchor a top-aligned line by the ink it actually draws
A symbol font's glyphs straddle the cap box by design — the diameter sign's
stroke overhangs it top and bottom — so anchoring a top-aligned line by the
nominal cap height alone hung it above where the drawing puts it, by the size
of the overhang.

Measure how far the first line's glyphs reach above the baseline and anchor by
that when it exceeds the cap. The fold is one-directional, so ordinary letters
(which stop at the cap) keep their exact previous offset.

The test pins the benchmark's own case: without this, the glyph draws from
0.7372 instead of the insertion point — matching the offset measured against
the file's own LINE endpoints for handle 1087.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-15 22:55:51 +03:00
Hakan SevenandClaude Opus 4.8 c10e8dae44 fix(tolerance): draw the feature control frame properly
A tolerance drew one row of ASCII soup — "RUN | ANGtol(M) | ..." — about
a tenth of its proper size. Six defects, each independent.

Rows were split on the two-character string "^J". A DWG stores a raw
newline, and the text-DXF reader rewrites the on-disk ^J to one before we
see it, so that split never fired on either mainstream path and every row
collapsed into the first. It now normalises all four spellings the break
arrives in — including the \P our own DXF writer emits, which made
open-DWG → save-DXF → reopen asymmetric.

Symbols were replaced with ASCII labels from a table that was itself
wrong: it read r as circular runout, n as angularity, p as position. The
font's own encoding says concentricity, diameter, projected tolerance
zone, and the benchmark's pre-exploded reference agrees. That table was
an inversion of acadrust's, which is self-contradictory — it binds n to
two different meanings. Do not re-derive from it.

The frame is a proportion of the character height: a compartment spans
-h..+h about its row's centreline, so it is 2h tall and the margin falls
out of that. It was built as height + 2*DIMGAP instead, which is a
different entity's geometry — DIMGAP sizes the dimension line's break and
the rectangle around a basic dimension, where it genuinely applies twice
per axis. Even fed a self-consistent style that can only ever produce
1.5h, so it was wrong in kind rather than mis-fed.

Height came from the entity's own field, which a DWG never sets — the
reader carries only the style's handle, leaving a small non-zero default
that a "> 0" test happily used. It now resolves the style by handle first
and name second: a DWG records the handle and defaults the name, a DXF
the reverse, so matching on the name alone would have picked the wrong
style for every DWG-read tolerance. An entity may also override single
style variables on itself, and this one halves its character height that
way; ignoring that drew the frame several times too large.

Rows stacked upward, every row inherited the widest row's columns, and
one full-width border boxed them all — none of which showed while the
parse only ever produced a single row. The insertion point sits on the
first row's centreline; anchoring the frame's bottom-left there offset it
by its own height, and by an amount that changed with the row count.

Cell text ran together because the glyph run asked for no letter spacing
while the width measurement assumed the font's own, so the characters
crowded inside correctly-sized boxes. Compartments are also measured by
what they draw now: the pen advance stops one letter-space past the last
glyph, and counting that made every one of them too wide.

Symbols come from a bundled stroke font, restored from this repository's
own history: it shipped QCAD's gdt.cxf until the text engine moved from
CXF to LFF, which removed every .cxf and left nothing in their place —
the ASCII labels date from exactly there. Converted to LFF, keyed by the
letter its \Fgdt;X escape carries, and registered under that name like
any other font, so a switch to it resolves through the ordinary lookup:
an MTEXT carrying the escape, or a text style named gdt.shx, reaches it
too, with no code of their own. The geometry is public domain by explicit
waiver; provenance is in the file's header.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-15 22:22:07 +03:00
Ryan SchultzandClaude Opus 4.8 f93f0beffc fix(dim): carry rounded fraction up into inches/feet in architectural format
A 3ft object measuring 35.9999" (float noise) printed "2'-11 1"" because the
architectural and fractional formatters split feet/inches/fraction *before*
rounding. A fraction that rounded up to a full unit (16/16 -> 1/1) reduced to a
bare "1" and leaked out with no carry into the inches or feet.

Round on the fraction grid first using integer ticks, then decompose, so carries
propagate: 35.99" now reads 3'-0" instead of 2'-11 1". Adds regression tests.

Fixes #380

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-14 07:31:09 -05:00
Hakan SevenandClaude Opus 4.8 d005cc4ab1 feat(props): make dim-line colour editable for leaders and dimensions
The "Dim line color" row on Leader and Dimension entities was
read-only: an earlier attempt wired it to Leader.override_color, which
acadrust never serialises, so the pick was lost on save. Store it
instead as a standard ACAD_DSTYLE per-object dimension-style override
(DXF code 176, an ACI index) through the existing dim_override codec,
so it round-trips through both DWG and DXF like the other dim
overrides. RGB picks collapse to the nearest ACI, matching the rest of
the dim-colour stack (dimension styles are index-only through the file
layer). The renderer prefers the override over the style's DIMCLRD.

The write branch is guarded to leaders and dimensions so a mixed
selection cannot stamp the override onto other entity types.

Bumps acadrust to c9fe982, which carries parsed XDATA onto dimensions
on DXF read (it was dropping common.extended_data), so the dimension
override persists on DXF save as well as DWG.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 17:31:09 +03:00
Hakan SevenandClaude Opus 4.8 fb8e2aeec1 feat(properties): make MText columns editable
The Columns row becomes a No columns / Static / Dynamic picker, with
editable column count / width / gutter rows that are live only when
columns are on. Switching a plain MText to columns seeds a consistent
count / width / gutter (dynamic defaults to auto-height) so the stored
layout isn't half-defined; the rows then refine it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 01:14:15 +03:00
Hakan SevenandClaude Opus 4.8 7cf1bf014b feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.

XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.

Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.

Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 00:28:42 +03:00
Hakan SevenandClaude Opus 4.8 1b037a68aa feat(annotative): scale text/dims/tables/blocks at the annotation scale + editable property pickers
Render: annotative-ness is resolved centrally in scene::annotative::is_annotative
(per-object context dictionary, legacy XDATA, or annotative style) so the bake and
the properties panel agree. Text, dimensions, tables and blocks now display at the
current annotation scale in model space. An annotative block scales as one uniform
unit about its insertion point — its internal geometry and attributes are carried by
that scale instead of being scaled individually, fixing the block-attribute size
regression. Tables scale their column/row/margin geometry.

Properties: the handle- and flag-backed rows are now editable pickers — MLEADER
multileader style / text style / arrowhead / leader linetype, General Material, and
Plot style (named plot-style mode only; the color-dependent mode stays read-only,
which is correct). A picked name is resolved back to its handle in the update loop,
where the document is in scope. Annotative Yes/No and the annotation-scale row are
shown per object.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 23:16:44 +03:00
Hakan SevenandClaude Opus 4.8 e3d5edf9b7 feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:

- Text: combined Justify (horizontal x vertical) dropdown; Position and
  Text-alignment rows become editable/grayed by justification; drop the
  Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
  (back-solves the factor) and Line space style; Defined width/height
  gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
  group (fold landing rows into Leaders), gate Text vs Block on content
  type, full 9-value attachment label set, resolve handle-backed rows
  (style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
  style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
  vertical text pos, overall scale); Current Vertex navigator; combined
  Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
  transparency by name. Add shared num_prop helper for conditionally
  editable numeric rows.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 18:01:44 +03:00
Hakan SevenandClaude Opus 4.8 b61bc0fdc0 fix(mleader): straight leaders, real landing + connection-anchored text (#347)
Render MultiLeader labels from the stored context fields instead of
re-deriving them:

- Draw each leader line with the line's own path type (code 170) rather
  than the MultiLeader's overall/style type. A spline-style leader can
  carry straight lines; splining a straight two-point run through
  Catmull-Rom bowed it, so the leader looked wavy with a dirty elbow.
- Drop the stray landing segment that ran from the dogleg end up to the
  text's top-left insertion, leaving just the horizontal dogleg.
- Anchor the horizontal-leader text so its bottom line sits at the
  connection point (the box then spans connection.y → text_location.y,
  matching the writer's content_base_point + landing_gap geometry).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 13:59:42 +03:00
Hakan SevenandClaude Opus 4.8 62bd360e46 refactor(precision): widen command transform scalars and defaults to f64 (#311)
Completes the f64 migration on the CPU data path: the only f32 left now
is the GPU/render/screen layer, which is architecturally required (wgpu
vertex buffers and iced are f32; f64 vertices would double memory).

- EntityTransform::Rotate.angle_rad and Scale.factor are now f64 (the
  transform was already applied in f64 around the DVec3 center, so this
  only preserves the typed angle/factor). apply_standard_transform and
  apply_standard_scale take f64.
- CmdResult::ExtrudeEntity.height and AlignSelected.{angle_rad,scale}
  are f64; the EXTRUDE height parses as f64.
- defaults.rs (last-used construction values: circle radius/diameter,
  rotate angle, scale factor, offset/fillet/chamfer distance, array
  spacing/counts/angle) moved from Cell<f32> to Cell<f64>. Array counts
  round-trip through u32 (exact below 2^53).
- Consumers in circle/array/fillet/offset/rotate/scale/align/solid3d and
  dimension.rs rotate_point/scale_point follow. `as f32` is kept only
  where a value feeds a WireModel rubber-band/ghost (the preview path is
  screen-space f32).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 00:34:38 +03:00
Hakan SevenandClaude Opus 4.8 8ed10ceb2a feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.

Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard

Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
  through the whole text pipeline; the resolved style font name is stamped
  so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector

Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 20:18:50 +03:00
Hakan SevenandClaude Opus 4.8 d37dabb0fb fix(mtext): show preview glyphs and make Enter break the line
Two defects in the in-app MTEXT editor, both surfaced once the preview
text became visible:

1. Invisible preview text. After the SDF text migration, tessellate emits
   glyphs as SDF quads on `text_verts` and leaves the polyline `points`
   empty, but the editor preview draws on a 2D canvas that can't run the
   SDF shader — so only the selection box showed. Add a `force_text_strokes`
   parameter to tessellate: when set, TEXT/MTEXT run-groups also emit their
   glyph outline strokes as points. The preview passes true (real stroke
   font, aligned with glyph_boxes); every other caller passes false and
   keeps SDF-only text.

2. Enter did not start a new line. mtext_type inserted a literal newline,
   but the MTEXT layout only breaks on the `\P` code (a raw 0x0A is a break
   only to the caret logic). Normalise every literal newline — from Enter or
   a multi-line paste — to `\P`, so the line breaks and the saved value
   stays standard.

Closes #308

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 10:38:41 +03:00
Hakan SevenandClaude Opus 4.8 b5106c82b5 feat(hatch): editable gradient colors + background color
Gradient Type (One/Two color), Color 1 and Color 2 editable via a generic field-keyed colour picker path. Background colour editable and stored as HATCHBACKGROUNDCOLOR extended data (group 1071 true colour), rendered in OCS as a solid backdrop drawn behind the pattern. Hide Origin X/Y for gradients (pattern-only concept) and drop the redundant gradient rows from the pattern branch.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 23:35:27 +03:00
Hakan SevenandClaude Opus 4.8 22059e138c feat(hatch): editable pattern props + origin grip
Merge duplicate Type into pattern_type; Type/Associative/Island detection as dropdown/toggle; Angle/Scale/Spacing/Origin X-Y editable; Color via picker. Reorder Pattern group, drop redundant Color/Transparency/Layer override and the Misc section. Wire the pattern origin (per-line base_point) into the render (x0/y0 relative to world_origin) so it affects dashed/offset patterns; add a circle origin grip at the hatch centroid (new GripShape::Circle).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 22:48:33 +03:00
Hakan SevenandClaude Opus 4.8 954a2e8b43 refactor(properties): rework ellipse geometry panel
Single Geometry section: Start/Center/End points, Major/Minor radius (editable; minor derives ratio), Radius ratio, Start/End angle editable. Major/Minor axis vectors, parameters, Length and Normal kept read-only at the end.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 22:48:25 +03:00
Hakan SevenandClaude Opus 4.8 b808fc2f86 refactor(properties): editable circle metrics, RO normals
Circle: Diameter/Circumference/Area now editable (each derives radius); Normal X/Y/Z read-only. Arc: reorder to Start/Center/End first; Normal X/Y/Z read-only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 20:39:31 +03:00
Hakan SevenandClaude Opus 4.8 b3c8c0a2f1 feat(properties): navigate polyline vertices from the panel
The Current Vertex row is now a ◀ / ▶ stepper (new PropValue::Stepper)
showing "i / N". Stepping it moves the panel's focus through the
polyline's vertices — Vertex X/Y and the per-vertex start/end widths show
and edit the focused vertex instead of always the first — and the focused
vertex's grip is drawn hot (filled) in the drawing so it's visible while
navigating. Focus wraps around and resets to the first vertex when the
selection changes. Adds Area and Length to the LwPolyline Geometry group
(Polyline2D already had them). Applies to LwPolyline and Polyline2D.

The focused vertex is threaded to the per-entity property builder/editor
through a dispatch thread-local (like the curve-tolerance override), so
the PropertyEditable trait signatures are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 17:53:05 +03:00
Hakan SevenandClaude Opus 4.8 9b45f0588d feat(polyline): apply the full width to each side of the centreline
Draw the polyline width bands with the stored width offset to each side
of the centreline rather than half of it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 15:56:56 +03:00
Hakan SevenandClaude Opus 4.8 bd04420e20 fix(ellipse): stop both axes ballooning when the minor grip passes the major
Dragging the minor-axis grip past the major length re-derived the
perpendicular from the just-swapped major axis on every mouse move, so
the major flipped 90° and the swap re-fired each frame — both axes grew
without bound once the minor reached the major.

Decide which axis the cursor is dragging by projection (not by a perp of
the current major), keep the non-dragged axis fixed, and store the longer
one as the major so the ratio stays <= 1. The swap is now stable across
the circle: the minor holds the old major length while the dragged axis
grows smoothly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 10:59:27 +03:00
Hakan SevenandClaude Opus 4.8 5c15091e74 refactor(text): SDF for all text kinds; remove the legacy stroke text path
SDF is the only text renderer now — the stroke path and its LOD ladder are
gone, and the remaining text kinds render as SDF glyph quads too.

- ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as
  SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype
  text already SDF.
- Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take
  the SDF path unconditionally everywhere: tessellate Text arm (per-group by
  GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table,
  mleader.
- Delete the text LOD ladder: tess.rs baseline/greek block and the
  block_cache greek/baseline path (emit_greeked_text / emit_text_baseline),
  plus the now-dead helpers text_baseline_points / text_greek_obb_tris /
  text_obb_corners_native / mtext_line_count and the LocalWire
  text_height_local / text_obb_local fields. All text is crisp at every zoom;
  text entities are exempt from the sub-pixel stub cull.
- Fix a latent bug: block-internal SDF text was culled because its LocalWire
  aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into
  aabb_local and exempt text_verts-carrying wires from the sub-pixel cull.
- Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind,
  MTextRunKind::Tab).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 21:07:00 +03:00
Hakan SevenandClaude Opus 4.8 67ce45d40b feat(text): SDF for synthesized-table cell text + guard table pick box
An in-app-created table (no baked *T### block) synthesised its cell text via
layout_mtext as stroke polylines — the last text path still rendering strokes
while SDF is the default (imported/baked tables already render SDF via the
recursive Text arm).

- tessellate_table: when SDF is on, build each cell's glyph quads from the
  run-carrying layout and collect them into one text-carrying wire (per-vertex
  cell colour, glyph-bounds AABB); the stroke/fill accumulation is the SDF-off
  fallback. Mirrors the MLEADER SDF branch.
- tess.rs synthesized-table path: guard the whole-table AABB clobber on
  (!points.is_empty() || !fill_tris.is_empty()) — matching the dim / mleader /
  baked-block paths — so the empty SDF-text wire keeps its tight glyph-box AABB
  and the pick box hugs the text instead of the whole table. Found by the
  adversarial review pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 18:37:17 +03:00
Hakan SevenandClaude Opus 4.8 d9a2c8245c feat(text): SDF for tolerance + complex-linetype text; per-group model; pick cycle
Extend SDF text to the remaining text kinds and make the Text arm decide SDF
per group instead of per entity type.

- Text arm: a stroke group renders as SDF glyph quads when it carries a
  GlyphRun (strokes suppressed); a run-less group keeps its strokes. TEXT /
  MTEXT / dim / block / mleader are unchanged (all run-groups). A composite
  Text object can now mix geometry + text: its run-less groups stay strokes and
  its run-groups become SDF. The glyph-bounds AABB is hoisted so both the
  bins-empty and bins-non-empty paths stamp it on the text wire.
- Tolerance: refactored to emit the feature-control frame (border, separators,
  dividers) as run-less stroke groups and each cell's text as a run-group, so
  the frame stays crisp geometry while the values/symbols render as SDF text.
- Complex linetypes: embedded LtSeg::Text renders as SDF glyph quads on an
  extra text wire (glyph-bounds AABB) instead of stroke polylines.
- click_hits_all: include SDF text boxes so selection cycling steps through
  text too, ranked after real geometry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 18:22:08 +03:00
Hakan SevenandClaude Opus 4.8 cd7306f530 feat(text): MTEXT background fill / mask + text frame, editable in Properties
Render the MTEXT background (never drawn before — the flags were only shown
read-only in the panel) and make it editable.

- Text arm emits an opaque fill_tris rect behind the glyphs: flag 0x01 uses
  the entity background colour, 0x02 uses the canvas colour (a wipeout-style
  mask). Box = glyph bounds padded by (background_scale - 1) × text height.
- Text frame (flag 0x10) draws a closed rectangle in the text colour around
  the same box.
- Threaded `bg_color` into `tessellate()` (+ all callers and the
  MultiLeaderTess trait) so the mask can use the drawing-window colour.
- Properties: "Background mask" is a Off / Fill / Mask dropdown, "Background
  color" is its own colour picker (independent of the entity colour — new
  PropBgColor* messages; picking a colour switches to Fill mode), and "Text
  frame" is a checkbox toggle. Edits re-tessellate so the change shows live.

Renders via the face3d fill path, so the fill shows with FILLMODE on (same as
MLEADER background fill); the frame is a stroke and always shows.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 17:41:16 +03:00
Hakan SevenandClaude Opus 4.8 110683f419 refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).

- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
  cached by the tess memo, cloned on hit, and transformed by the block
  expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
  the strokes; render gathers text_verts from the viewport wire set
  (`gather_text_verts`, cached on wire content id) — the old collector is
  removed. Covers model, dimension, table, block-internal and paper-sheet
  text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
  SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
  entity_aabb which mis-places the box for MTEXT; composite paths no longer
  clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
  highlight, keyed on selection_generation) mirroring the selected-wire
  xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 17:01:36 +03:00
Hakan SevenandClaude Opus 4.8 1709d41e4f refactor(text): parse single-line ATTRIB text via acadrust
Route single-line attribute text through the same acad_text_encode path
TEXT uses (acadrust parse_plain_text, re-encoded for the stroke
tessellator). Multi-line attributes already flow through layout_mtext
(acadrust). No visible change — attribute text with %% codes rendered
correctly before via the local tokenizer; this shares the one parser.
TOLERANCE cells and complex-linetype text still use the local tokenizer
(specialized paths that rarely carry %% codes).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 01:15:36 +03:00
Hakan SevenandClaude Opus 4.8 d528a3e2a5 feat(text): parse TEXT via acadrust + extend SDF text to MTEXT
Two related steps for the text pipeline:

- TEXT parsing now goes through acadrust's parse_plain_text (like MTEXT
  uses parse_mtext). text::acad_text_encode parses the value's %% codes —
  including %%u/%%o underline/overline and %%nnn — and re-encodes to the
  stroke tessellator's \L/\O + resolved-Unicode grammar, so OCS no longer
  tokenizes TEXT %% codes itself. (ATTRIB/TOLERANCE/DIMENSION still use the
  local tokenizer for now.) Bumps the acadrust pin to the commit that
  completes the TEXT/MTEXT control-code set.

- SDF text (OCS_TEXT_SDF) now covers MTEXT and every text-producing
  entity, not just top-level TEXT. TextStroke carries a GlyphRun (layout
  params); the collector walks convert()'s TruckObject::Text runs and
  applies annotation scale exactly as tessellate does for strokes.
  layout_glyph_quads shares lff::tokenize_run so %% specials resolve and
  \L/\O decorations are skipped (decoration LINES in SDF are still TODO).

Verified: stroke rendering unchanged with the flag off; SDF glyphs render
correctly for TEXT and MTEXT with OCS_TEXT_SDF=1. 154 lib tests green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 00:53:19 +03:00
Hakan SevenandClaude Opus 4.8 b88e5e4ad5 refactor(mtext): parse MTEXT via acadrust, drop OCS's own parser
OCS maintained a second MTEXT inline-code parser that duplicated
acadrust's `mtext_format::parse_mtext`. Replace it with an adapter
(`adapt_mtext_paragraphs`) that maps acadrust's structured MTextDocument
onto the existing layout types (`MTextLine`/`RunState`); the layout
engine (`layout_mtext`, `mtext_line_count`, and their callers) is
unchanged. Removed the hand-rolled `parse_mtext_paragraphs*` and its
helpers.

Validated during migration by a differential test (adapter vs the old
parser matched byte-for-byte over a 20-case corpus) and confirmed
rendering identically in the app; the differential test is replaced with
adapter-only unit tests. Relies on acadrust's new `MTextScalar` to tell
absolute `\H` from relative `\Hx` (bumped the acadrust pin).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 00:04:11 +03:00
Hakan SevenandClaude Opus 4.8 896b6c385c feat(text): render TEXT as SDF quads behind OCS_TEXT_SDF flag
Wires the SDF glyph atlas into the render loop end-to-end for top-level
TEXT entities, gated by the OCS_TEXT_SDF env var so the default path is
unchanged. Verified rendering on a real drawing: glyph quads land exactly
over the existing stroke text (position, size, rotation), confirming the
atlas → shader → pipeline → screen path and the RTE / UV math.

- text::text_run_placement — extracted from to_truck (behaviour-preserved)
  so the stroke and SDF-quad paths compute an identical run placement.
- sdf_atlas::text_atlas — process-wide GlyphAtlas (Mutex/OnceLock), shared
  by the collector (bakes glyphs) and the GPU upload (reads texels).
- Scene::sdf_text_vertices — collects per-glyph TextVertex for the frame's
  TEXT entities via layout_glyph_quads + push_glyph_vertices. Empty unless
  OCS_TEXT_SDF is set.
- ViewportData.text_verts + Pipeline text_pipeline/atlas/vbuf, upload_text
  (rebuilds the R8 atlas texture when it grows), and a text render pass
  drawn over the wires.

Still additive over the stroke text; only top-level TEXT is wired (MTEXT,
attributes, and block-internal text are next), and the collector re-runs
per frame (vertex caching by geometry epoch is a follow-up).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 23:02:34 +03:00
Hakan SevenandClaude Opus 4.8 67bcdb1731 fix(mesh): ear-clip concave mesh faces instead of fan-triangulating
PolyfaceMesh and SubD Mesh faces were fan-triangulated from vertex 0, which
is only valid for convex faces. A concave face (e.g. an L-shaped / notched
n-gon) fanned into triangles that spill outside the outline — spurious
"triangles that shouldn't be there" that covered neighbouring geometry and
its edges. Replace the fan with an ear-clipping triangulator that projects
the planar face to 2D, respects its winding, and clips only real ears, so
concave faces triangulate to exactly their own area. Convex faces (and the
cylinder-cap n-gons) are unchanged in result. Falls back to a fan for a
degenerate / self-intersecting polygon.

Verified on Annotation-2D-Mesh-Solid-BIM.dwg (concave 5-/6-gon mesh faces)
and by a unit test: a concave L-hexagon triangulates to n-2 triangles whose
total area equals the polygon's, with no overshoot.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 23:01:59 +03:00
Hakan SevenandClaude Opus 4.8 e515fa3156 feat(spline): draw fit-point splines via Catmull-Rom interpolation
A spline stored by fit points (DWG R2013+ scenario 2) has no control points
or knots, so the renderer — which built its curve from control points —
produced nothing and the spline was invisible. When a spline has fewer than
two control points but at least two fit points, interpolate a smooth
Catmull-Rom curve through the fit points and emit it as a dense polyline.
Open ends use reflected phantom points; closed/periodic splines wrap.

Pairs with the acadrust bump (e8e422a) that decodes the R2013+ fit-point
scenario correctly — together they make the fit-point spline in
Annotation-2D-Mesh-Solid-BIM.dwg render.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 19:52:56 +03:00
Hakan SevenandClaude Opus 4.8 e78e137664 fix(paper): keep model 3D solids off the paper-space sheet
The paper layout sheet pass reused the full model mesh set, so every
top-level 3D solid piled onto the paper origin — the sheet camera works
in paper coordinates, not model space. Guard `meshes` on
`inst.paper_sheet` the same way wires/hatches/images already are: the
sheet shows only paper-native content; the model's solids render inside
the floating content viewports (model camera + per-viewport scissor).

Also name 3DSOLID/REGION/BODY/SURFACE/MESH/SOLID/MULTILEADER in dxf_name
(they fell through to "ENTITY"), and add a SELHANDLES diagnostic that
reports the active space plus a per-type / per-block selection breakdown.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 23:29:18 +03:00
Hakan SevenandClaude Opus 4.8 726984e3a5 feat(properties): show Thickness in the General group
Thickness (DXF 39) is a General-group property in the palette, but it is
stored per-entity, so it was previously shown (inconsistently) only in the
Geometry section of a few entities. Surface it in General for every entity
type that carries an extrusion thickness (arc, circle, line, lwpolyline,
point, polyface mesh, 2D polyline, shape, 2D solid, text) via
entity_thickness / set_entity_thickness, routed through apply_common_prop.

Removes the now-duplicate Thickness rows (and dead apply arms) from the
arc/circle/shape/solid Geometry sections so it is never shown twice.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 16:51:05 +03:00
Hakan SevenandClaude Opus 4.8 ab0a4f790c fix(insert): place block attributes by their ATTDEF geometry, follow the block (#255)
Inserting a block piled every attribute at the origin: each AttributeEntity
was built from just tag + value with default geometry. Carry the full
AttributeDefinition through the attreq flow and build attributes with
AttributeEntity::from_definition, then apply the block's insertion transform
so each lands in WCS at its proper position / alignment / rotation.

Attributes are stored in world space, so moving the block must move them
too. The MOVE command already did (acadrust translate_insert); apply_grip
(grip-drag) and the Position X/Y/Z property edit now translate the
attributes by the same world delta so they no longer stay behind.

Rotate/scale/mirror of a block still don't carry the attributes — acadrust's
transform_insert has no attribute pass — tracked separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 16:47:39 +03:00
Hakan SevenandClaude Opus 4.8 79a5ac6fed fix(scene): correct MIRROR of TEXT under MIRRTEXT (commit + preview)
Root cause: the glyph tessellator advanced the pen by advance*width_factor
while the transform already scales the pen position by the width factor, so
a backward (negative width factor) run kept its word order and grew on the
same side of the anchor instead of mirroring across it. Advance in unscaled
glyph units to match text_local_bounds; the transform applies the factor once.

On top of that:
- The renderer honours the entity's own text-generation flags (DXF 71: bit 2
  backward / bit 4 upside-down) XOR the text style's flags, and signs the
  horizontal anchor by the width factor so Center/Right backward text mirrors
  about its anchor.
- MIRRTEXT on: a MIRROR toggles both group-71 bits so the reflected rotation
  renders a true glyph mirror (backward-upright for a vertical axis, flipped
  for a horizontal one).
- MIRRTEXT off: the text flips left<->right and lands mirror-symmetric to the
  source (right-reading) instead of hugging the axis.
- The MIRROR preview re-derives text ghosts to match the commit; mirrored()
  now reflects the double-single residual too.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 12:42:55 +03:00
Hakan SevenandClaude Opus 4.8 2bc5c2a686 fix(commands): JUSTIFYTEXT sets vertical alignment for single-line TEXT
The Text branch only set horizontal_alignment, so TL/BL/MC etc. never
moved the text box vertically. Map the T*/M*/B* codes to Top/Middle/Bottom
and re-sync the alignment point. (#246)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 09:51:34 +03:00