The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.
A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sync current main and companion codec changes before preserving unsupported frame rows, rendering in the entity plane, and sharing final layout metrics with placement preview.
The last four truck crates are gone from the manifest. What they were doing
falls into three parts, and each now has one answer instead of two.
NURBS evaluation. truck was carrying B-spline curves and surfaces for the
SPLINE entity, hatch boundaries, BLEND's endpoint frames, the spline preview
and ACIS spline-surface faces. The kernel now has both, over one de Boor
written across however many coordinates a control point holds — so a plane
curve and a space curve cannot drift apart, and a surface is the same
algorithm applied twice. The rational and the polynomial cases stop being
separate types: weights absent means polynomial.
The entity conversion. Every LINE, ARC, ELLIPSE, SPLINE and polyline was
built into truck topology purely so it could be sampled back into points.
They are sampled through `entities::curve` now, which is where each one's
geometry is already defined once and what EXTRUDE and REVOLVE read — so a
circle drawn on screen and a circle handed to the Model tab come from the
same definition. That retires four of TruckObject's variants and the module
is renamed for what it does.
SWEEP and LOFT. Both only ever produced a mesh, so both are built from point
lists: a band of quads per span, and a lid where a profile closes. Lofting
profiles of different densities resamples them by distance rather than by
index, so a circle lofted to a square no longer twists.
Two things worth noting for anyone reading the old comments. The tolerance
globals that lived in the tessellation module were never about truck at all —
they are the per-frame chord height, and they move to `curve_tol`. And the
rule that a profile handed over as `Lines` silently broke EXTRUDE and REVOLVE
no longer holds: those read the curve directly and never look at this channel.
The two `automation` test failures are unchanged from before this and are not
caused by it.
Co-Authored-By: Claude Opus 5 <[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]>
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]>
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]>
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]>
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]>
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]>
Text elements previously rendered as hollow wireframe outlines and sometimes
fell back to the wrong system font due to case-sensitive exact matching. Also,
the viewport background grid drew on top of solid entities.
Case-insensitive and partial-name fallback matching (e.g., mapping "arialn"
to "Arial Narrow") is now added to `sysfont::canonical_family_name`, ensuring
text resolves to the correct installed system font.
TrueType font contours are now triangulated into solid fills (`fill_tris`)
using `lyon_tessellation` in `ttf_glyph.rs`. To prevent the GPU from misclassifying
this 2D text as a 3D mesh (which broke flat shading), text tessellation splits
the output into separate outline and fill `WireModel`s.
The background grid is extracted from `SelectionCanvas` into `GridCanvas` and
moved beneath the 3D viewport to ensure proper layering. Finally, `DepthBiasState`
is enabled on all face/fill pipelines to apply a polygon offset, perfectly
resolving Z-fighting between coplanar wireframe lines and solid faces.
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]>
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <[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
- 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]>
Replace raw text dump with proper boxed feature-control frame:
- parse_gdt_rows(): splits %%v cells and ^J row separators
- substitute_gdt_codes(): strips {\Fgdt;X} patterns, maps symbol chars
to 3-letter ASCII labels (POS, PRP, PAR, SYM, CIR, CYL, …)
- tessellate_tolerance(): outer rectangle, row separators, column
dividers, and CXF-tessellated cell text — all as Vec<Vec<[f32;2]>>
(TruckObject::Text polyline path)
- Column widths computed per cell content; text centred in each cell
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- mline.rs: MLine renders center spine + two parallel offset lines at
±scale/2 using the vertex miter directions; vertex grips; scale,
closed and style-name properties
- tolerance.rs: Tolerance renders the GDT text string via font
tessellation at insertion_point with rotation from direction vector;
insertion grip and position properties
- properties.rs: Dimension entity's style_name EditText is upgraded to
a Choice dropdown when document.dim_styles is non-empty, enabling
point-and-click DimStyle assignment without a separate panel
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>