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]>
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]>
Vertical (top-to-bottom) text used to reuse the horizontal line stack —
every line landed on the same baseline and overlapped. The layout now
stacks each character in its own 1.4h cell down the column, one column
per paragraph (1.2h apart), with word-boundary column wrap against the
defined width (which measures COLUMN LENGTH for vertical flow — the
horizontal pre-wrap is disabled there). Justification anchors place the
column block from wrap-accurate metrics: the pre-pass simulates the
render's word wrap exactly, so right/centre/bottom-attached blocks stay
put as columns grow. Layouts also report pre-rotation local_bounds so
frames and fills can box what was actually drawn.
Width grips route through one shared helper, flow_grip_axis(): the flow
axis (rotated baseline, or down the column) plus an attachment-side
factor — a right-attached text forms LEFT of its insertion, so its grip
sits there too; centre attachments use ±half. MTEXT's grip and drag
projection both use it, covering all 9 attachments × both flows × any
rotation from a single table.
Co-Authored-By: Claude Fable 5 <[email protected]>
Every angle row went through the linear formatter, so switching LUNITS
to Architectural showed a block's Rotation as feet-and-inches — and the
string wouldn't parse back, wedging the field until it was cleared and
retyped. Only the decimal/fractional LUNITS survived by accident.
New edit_angle row helper formats angles with format_angle
(AUNITS/AUPREC: decimal degrees, DMS, grads or radians), applied to all
17 angle rows (rotations, obliquing, arc start/end, hatch pattern
angle, multileader text rotation). parse_angle_deg converts every
AUNITS display form back to degrees ("30°"/"30d", DMS 30°15'20",
33.33g, 0.52r) and parse_f64 falls back to it, so the displayed value
round-trips through an edit.
Closes#297
Co-Authored-By: Claude Fable 5 <[email protected]>
Re-evaluate AcDbField-driven MTEXT — date/login/document-property fields, table
formulas and object properties — against the current system instead of showing
the frozen last-save cache. The evaluation engine lives in `acadrust::fields`;
OCS supplies only the environment (system clock, OS login, env vars) through a
FieldContext implementation, and hooks the resolved text into MTEXT rendering,
falling back to the cached value for anything it can't evaluate.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
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]>
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]>
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]>
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]>
Bump acadrust to a46cb8b (spline knot parameterization + mtext line-space
style now stored) and populate rows the builders left blank:
Entity-local: spline Knot Parameterization + closed-spline Area (shoelace),
mtext Line space style, hatch Spacing (pattern-line offset), leader Text
offset (annotation offset), region Area + Perimeter (wire-loop approx).
Doc-dependent (resolved in app/properties.rs where the document is
reachable): insert Block Unit + Unit factor (block record units vs
INSUNITS), underlay Name + Path (definition object), leader Text style /
vertical placement / overall scale and tolerance Text style (dimension
style), multileader Max points + segment angles (mleader style).
Rows needing an annotation-scale / sheet-set subsystem or an internal
PDF/DWF layer parser are intentionally left blank.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.
Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Groundwork for making the Properties panel conform to the per-entity group/row
spec (PROPERTIES.md). The panel already renders a Vec of sections, but each
entity could only express a single geometry section.
Change PropertyEditable::geometry_properties (and the EntityTypeOps enum
dispatch) to return Vec<PropSection>, update the two impl macros, and flatten
in properties_sectioned so the panel shows [General] + the entity's groups.
Every entity builder is wrapped to return its existing section as a one-element
Vec — pure mechanical change, no rows or titles altered, panel output identical.
Enables Phase 2: rewriting each entity's groups/rows to match the spec.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Move the 30 flat modules under src/scene/ into six folders by role:
convert (entity→geometry), text (fonts/shaping), model (render models),
pick (hit-test/selection/grips/xclip), view (camera/transform/render),
cache (block + property caches). pipeline/ is unchanged.
Full path migration — all `scene::X` references become
`scene::<group>::X`, no compatibility re-exports. render.rs items that
were `pub(super)` (parent was scene) become `pub(in crate::scene)` to
keep their original scene-wide visibility.
No behavior change; build and tests are green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Grip markers drew visibly offset from the entity on large-coordinate
(UTM-scale) drawings: every Grippable impl cast the f64 entity coordinates
to f32 BEFORE the world-offset subtraction, and at 1e7 magnitude an f32
holds ~0.9 drawing units of rounding error. The wires subtract the offset
in f64 first (precision-preserving), so the grips landed up to ~1 unit
away from the geometry they belong to.
GripDef.world is now DVec3: producers fill it straight from the f64
entity data, refresh_selected_grips subtracts the world offset in f64,
and only the small offset-local result is cast to f32 for screen-space
projection (grips_to_screen / find_hit_grip) and drag state.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The rendered MText preview is now interactive: drag across glyphs to select
a character range and apply toolbar formatting (bold, italic, underline,
overline, strike, case, alignment) to exactly those characters.
The text layout gains an opt-in pass that emits one world-space box per
visible character carrying its reading-order index; the editor maps those
boxes into the preview frame, hit-tests pointer drags against them, and
highlights the selection. Formatting splices the matching inline codes into
the raw value at the selected byte range (mapped via a code-aware visible-
character scanner). With no preview selection the Edit-box path is unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two free helpers in app/commands.rs lived as big match-on-EntityType
blocks: massprop_entity (Circle/Arc/Line/LwPolyline/Ellipse area +
perimeter + centroid, ~140 lines) and entity_text_content /
replace_entity_text (Text/MText/AttributeDefinition/AttributeEntity).
Move each to its own trait in entities/traits.rs and implement
per-entity:
MassPropsCalc — Circle, Arc, Line, LwPolyline, Ellipse
TextContent — Text, MText, AttributeDefinition, AttributeEntity
EntityTypeOps gets dispatch shims that route to the trait via the
existing macro, so callsites just call entity.mass_props() /
entity.text_content() / entity.replace_text(search, rep).
The remaining free helpers in commands.rs (entity_list_details,
entity_extra_info, entity_type_name, build_data_extraction_csv) are
single-command formatting helpers tightly coupled to LIST / EXTRACT /
text-format output. Distributing them as per-entity methods would
inflate every entity file with command-side formatting concerns
without making the command-side any cleaner — left in place.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- MultiLeader: stop double-scaling when ctx.text_height (WCS-baked) is set;
use it as-is and only multiply by scale_factor + anno on the fallback path.
ctx.text_width gets the same treatment.
- Text / Attribute: width_factor and oblique_angle are entity-authoritative.
The previous entity*style multiplication / addition mis-rendered any text
whose TXSTYLE had a non-default width_factor or oblique_angle (RR 0.6,
STYLE2 15°, Romans/ARIEL/A32 0.65-0.8 in the sample file).
- MText: honour TXSTYLE is_backward (negative width_factor mirror) and
is_upside_down (180° rotation). measure_mtext_chars uses positive
magnitude so anchor math stays correct under backward text.
- Tolerance: use tol.dimension_gap when set instead of the hard-coded
0.35 * height heuristic.
- Table: cell text height / font / alignment resolve through
CellContent -> CellStyle -> TableStyle.{title|header|data}_row_style
-> compiled-in defaults. Row classification respects the suppressed
flags. Cell + content rotation applied.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
PolyfaceMesh and PolygonMesh were wireframe-only. They now render with
the same flat-color solid fill as Face3D entities.
- Add fill_tris: Vec<[f32;3]> to TruckEntity and WireModel
- PolyfaceMesh: fan-triangulate each face into fill_tris
- PolygonMesh: triangulate each grid quad into fill_tris
- tessellate.rs Lines branch: apply world_offset to fill_tris
- Face3DGpu::from_wires: collect fill_tris from all wires in addition
to Face3D key_vertices quads
- upload_face3d: pass all_wires so fill_tris are batched into one draw call
- Grip points in paper space now use the 2-D linear to_px transform
(tx/ty/ortho_size) instead of the 3-D view_proj matrix, matching the
coordinate system used by PaperCanvas
- world_offset is no longer subtracted from paper-space entity grip
positions — paper-space coordinates are already unoffset (same rule
as wire tessellation in wires_for_block)
- MTEXT v_offset corrected for all attachment points: CXF glyphs sit
on the baseline and extend upward, so Top attachment needs v_offset=-h,
Bottom needs (n_lines-1)*line_h, Middle is the midpoint of the two
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Line spacing: DXF code 44 (line_spacing_factor) is a multiplier on
AutoCAD's default baseline-to-baseline distance of 5/3 × text_height.
The renderer was using factor × 1.0 instead of factor × 1.667, making
all MTEXT appear cramped.
Blank paragraphs: split_mtext_lines was filtering out empty strings, so
\\P\\P (double paragraph break used to separate sections) was silently
collapsed. Empty lines in the middle of the block are now preserved;
only leading/trailing blank lines are stripped.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- fix: polyline arc center sign was inverted (- → +) causing arcs to render on the
wrong side in LwPolyline and Polyline2D
- fix: store glyph strokes in local space with f64 origin so world_offset subtraction
uses f64 precision; prevents blocky text at large UTM coordinates
- fix: Face3D fill darkened to 45% so edge wires are visually distinct from fill
- fix: wire batch vertex buffer chunked to 256 MB GPU limit (was panic on large files)
- fix: gpu_face3d_edges changed from Option<WireGpu> to Vec<WireGpu> to support chunks
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Text/MText values now correctly handle DXF special-character sequences:
- %%d → °, %%p → ±, %%c → ⌀, %%nnn → Unicode scalar (resolved in tessellation)
- %%u / %%o toggle underline and overline strokes for TEXT entities
MText decoration codes (\L/\l, \O/\o, \K/\k) were previously consumed
as argument-bearing codes, silently eating the decorated text. Fixed
strip_mtext_codes to keep them as zero-width markers; tessellate_text_ex
now emits actual line strokes (underline, overline, strikethrough).
measure_mtext_chars and word_wrap skip markers so line widths are correct.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>