Commit Graph
132 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 3c5aa4453a fix(dimension): bake↔live consistency, edits, baseline/continue, grips
Batch 1-3 of the dimension audit (19 issues). The dominant class was the
baked *D block diverging from the live render: a reloaded dimension renders
from that block, so on save+reopen it silently changed.

Bake now matches the live render:
- text value/format via the shared style-resolved path (DIMDEC/DIMLFAC/
  DIMPOST/units/<>), correct height + rotation ladder, suppression
- angular dims bake their swept arc (no longer collapse to a V)
- diameter draws edge-to-edge through the centre (live + bake), centre mark
- radius/linear/aligned bake arrowheads
- group-10 (base.definition_point) synced from the edited geometry so an
  edited dim no longer shifts on reload

Editing / properties:
- aligned no longer corrupts ext_line_rotation when a coordinate is edited
- DIMTEDIT pins the moved text (text_user_positioned)
- angle properties shown/edited in degrees
- non-linear text respects the user-positioned gate

Commands / geometry:
- DIMBASELINE stacks outward (not toward the points) and its preview no
  longer tilts over non-level points
- DIMCONTINUE/DIMBASELINE inherit the base dim's text rotation (UCS chains)
- angular extension lines reach the arc (no gap)
- text grip sits on the text for auto-placed dims instead of the origin

Refs #181. (acadrust DXF round-trip + QDIM/DIMBREAK/DIMJOGLINE/DIMSPACE/
DIMEDIT to follow.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 17:47:50 +03:00
Hakan SevenandClaude Opus 4.8 2672807c13 fix(polyline): keep wide-polyline fills precise at UTM coordinates
The solid fill band of a wide LwPolyline / Polyline2D was built from
absolute WCS vertex coordinates cast straight to f32, then handed to a
HatchModel with world_origin = [0, 0]. At UTM-scale coordinates (~1e6)
f32 resolves to ~0.06, so the half-width offset collapsed and the
relative-to-eye fill shader cancelled `boundary - eye` to noise — the
band rendered as a string of squares. (Exploding the polyline drops the
width, hence no band and a clean line, which is why that "fixed" it.)

Build the band relative to a world_origin (the first vertex) in f64 and
store the small f32 offsets, mirroring how baked hatch fills keep
precision. Same class of bug as the on-create hatch-fill fix.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 10:55:28 +03:00
Hakan SevenandClaude Opus 4.8 dee5a05652 fix(text): address PR #180 review
Follow-up cleanups on the merged TTF-fill work:

- Drop the unconditional resolve_text_style log line — it fired for every
  text style and spammed ~7k stderr lines opening a single real drawing.
- Drop the per-glyph tessellation-error eprintln; a malformed font would
  otherwise log once per character. The glyph still falls back silently.
- Tessellate glyph fills with the nonzero winding rule (lyon defaults to
  even-odd, which mis-fills TrueType outlines whose contours overlap).
- Make the sysfont prefix fallback deterministic (sorted) and require ≥3
  chars so a 1–2 letter request can't grab an arbitrary family.
- Remove the unrelated windows-gnu --exclude-all-symbols linker flag that
  slipped into .cargo/config.toml.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 00:27:40 +03:00
Karim Jerbi 627761b629 fix(text): render TTF glyphs as solid fills, fix font fallback, correct Z-order
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.
2026-06-25 18:17:01 +01:00
Hakan SevenandClaude Opus 4.8 7086a79c79 fix(dimension): render diameter text and shrink point markers (#139)
Two issues in a file whose dimensions carry baked *D### blocks.

Missing diameter text: the baked MText values are "\U+220520" /
"\U+220510" — the \U+2205 (⌀) escape immediately followed by the literal
digits "20"/"10". The MText parser read up to six hex digits for a \U+
escape, so it greedily consumed "220520" = 0x220520, which is past the
maximum code point, char::from_u32 returned None, and the whole text
dropped. A \U+ escape is exactly four hex digits; read four so the symbol
decodes and the trailing digits stay literal. (Angular "90\U+00B0" only
worked because its escape sits at the end of the string.)

Node artifacts: PDMODE=0 points were drawn as a fixed 0.1-world-unit "+",
which in a small drawing turned the 31 dimension def-points on the
Defpoints layer into visible crosses. Size the dot from world-per-pixel
(~1 px, zoom-adaptive via the tessellation cache key) to match the near-
invisible dot other CAD apps draw.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 18:22:35 +03:00
Hakan SevenandClaude Opus 4.8 ef8d5b2610 refactor: remove the last world_offset zero-locals
Clean up the remaining `let [ox,oy,oz] = [0,0,0]` bindings and their
zero-valued add/subtract arithmetic across scene/mod.rs (mesh LOD resplit,
model AABB, wide-polyline fills, wipeout boundary, hatch/solid models,
insert/entity AABB), table, multileader and xclip. Coordinates now read as
the plain absolute values they are. No world_offset reference remains.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 18:55:00 +03:00
Hakan SevenandClaude Opus 4.8 29415cac20 refactor: drop residual zero-subtraction from world_offset removal
In the files where ox/oy/oz came solely from the now-zero offset
destructure (tessellate, block_cache, dimension, leader, text_support),
remove the `let [ox,oy,oz] = [0,0,0]` binding and the `- ox` arithmetic so
the coordinate math reads as the plain absolute value it now is.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 18:38:23 +03:00
Hakan SevenandClaude Opus 4.8 56d507fd88 refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.

- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
  to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
  tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
  entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
  helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
  multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.

Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 18:35:02 +03:00
Hakan SevenandClaude Opus 4.8 dd1f2e49bb fix(render): UTM-precise UCS icon + fallback outlines (hatch etc.)
Two UTM-scale rendering defects.

- UCS icon jittered: it projects only axis DIRECTIONS but went through the
  full view_proj, whose ~5.7e6 translation cancels catastrophically in f32.
  Feed it the rotation-only view_proj_rte instead (translation dropped).

- Fallback outline geometry (hatch boundary, viewport/insert/ole2frame
  frames, Solid3D edge wires) built absolute f32 points with an empty
  points_low, so at UTM the outline sat ~0.5 m off its own fill (the hatch
  fill renders precisely via the world_origin-relative HatchModel pipeline).
  FallbackGeometry now carries absolute f64 points + key vertices; the
  tessellate dispatcher splits them into the double-single high/low pair the
  relative-to-eye wire pass needs, so outlines land exactly on their fills.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 10:09:19 +03:00
Hakan SevenandClaude Opus 4.8 4f7a98a2ad feat(f64): command layer compiles on DVec3
Whole command/snap/grip/cursor chain now builds on f64 DVec3 end to end,
so picked points, snaps and grip drags stay precise at UTM-scale
coordinates instead of quantizing to ~0.5 m through f32.

- All ~80 command files migrated: draw (line/circle/arc/ellipse/shapes
  in full f64; others via point methods), modify, annotate, insert, view,
  inquiry, layers, groups, clipboard, layout.
- Entity apply_grip impls updated for GripApply::{Absolute,Translate}(DVec3).
- Construction sites (EntityTransform/CmdResult/DynAnchor) emit DVec3;
  wire-preview boundaries cast to f32 only at the GPU edge.
- Modify/annotate commands keep internal Vec3 geometry where precision is
  non-critical, casting at the DVec3 boundary; key draw commands store f64.

Builds clean (no warnings). Visual verification of crosshair/snap/grip at
extreme UTM zoom pending.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 03:51:50 +03:00
Hakan SevenandClaude Opus 4.8 3da71698b1 wip(f64): convert command layer to DVec3 (in progress)
Migrate the command/snap/grip/cursor chain from f32 Vec3 to f64 DVec3 so
picked points, snaps and grip drags stay precise at UTM-scale coordinates
(f32 quantizes to ~0.5 m there).

Done so far:
- command/mod.rs: EntityTransform, CmdResult points, CadCommand point
  methods, TangentObject already DVec3 (radius f64).
- snap: SnapResult.world DVec3; world_to_screen/try_pt DVec3.
- GripApply -> DVec3; grip find/state (GripEdit) f64; camera
  pick_on_plane_f64 / pick_on_target_plane_f64; scene paper_to_model_f64.
- update.rs click + mouse-move + grip-drag pipelines route precise DVec3
  to on_point/on_preview_wires/on_entity_pick; ortho/polar cast at the
  UcsXform boundary only.
- cmd_result dispatch, transform helpers, dimension/arc/circle/multileader
  apply_grip, circle/arc/ellipse/shapes draw commands, array modify.

Tree does NOT compile yet (~236 errors remain across modify/annotate/
insert command files + entity apply_grip impls); committed at user request
to checkpoint progress.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 03:32:11 +03:00
Hakan SevenandClaude Opus 4.7 cca1086696 refactor(snap): widen key_vertices to f64 (world_offset removal P4)
WireModel/LocalWire/BatchEntry key_vertices are now Vec<[f64; 3]>;
tessellation keeps them in f64 (no f32 cast at offset subtraction),
producers (dimension, leader, multileader) build them from the acadrust
f64 source, the block-cache transform keeps f64, and the face3d Face3D
quad path splits them into the double-single (high, low) pair. Consumers
(snap endpoint/midpoint, xclip) read via f32 at the leaf. Still
offset-relative (inert); endpoint/midpoint snap is no longer f32-capped.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 22:23:56 +03:00
Hakan SevenandClaude Opus 4.7 cd1d3bb619 refactor(snap): producers emit f64 snap points (world_offset removal P3)
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]>
2026-06-23 22:10:29 +03:00
Hakan SevenandClaude Opus 4.7 a7a1fac09a refactor(snap): widen snap_pts to f64 (world_offset removal P2)
WireModel/LocalWire/TruckEntity snap buffers and the producers/consumers
now carry snap candidates as DVec3 (offset_snap_pts, dimension/multileader
producers, block-cache transform, pick/xclip, snap module). Still
offset-relative, so behaviour is unchanged — this just lets the snap
transport survive the upcoming switch to absolute coordinates without an
extra f32 quantization. The snap module still resolves in f32 (as_vec3 at
entry), matching today's precision.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 18:52:42 +03:00
Hakan SevenandClaude Opus 4.7 c0f3d0de5e feat(render): double-single fills for the face3d pipeline
Table / PolyfaceMesh / greek-text / mleader-background fills go through
the face3d pipeline; their vertices were single f32 and quantized at UTM
scale. WireModel now carries a fill_tris_low residual (filled by the
tessellate Lines path), Face3DVertex gains position_low (location 3), and
face3d.wgsl reconstructs the eye-relative position from the pair. 3DFACE
quads built from key_vertices use a zero residual (no low source).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.7 97d6993768 refactor(wire): add WireModel.points_low parallel buffer (empty default)
Foundation for double-single positions: WireModel now carries an
index-paired f32 residual buffer (`points_low`) next to the existing
`points` high half. Empty means "all-zero residual", so every existing
constructor (interactive draw, previews) is unchanged in behaviour;
tessellation from CAD f64 will fill it in the next steps so vertex
precision survives at UTM-scale coordinates.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.8 5291a3fb82 fix(text): keep text anchors in f64 and honour space advance
Single-line TEXT and ATTRIB/ATTDEF cast the insertion/alignment point to
f32 before computing the stroke origin. At large coordinates (UTM ~4.5M)
the f32 ulp is ~0.5, snapping text baselines onto a coarse grid so
adjacent rows collapse onto each other and overlap. Keep the anchor in
f64 throughout.

Horizontal alignment now uses the pen advance box [0, advance] instead of
the inked min_x/max_x, so leading/trailing spaces keep their width and
space-aligned columns no longer shift. text_local_bounds returns
TextLocalBounds { ink_min, ink_max, advance }.

TABLE laid out its whole grid in an f32 world frame; switch to a local
f32 frame plus an f64 base added at the widening step, fixing cell-text
baseline snap and the i32 overflow in the border-dedup keys.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 18:47:45 +03:00
Hakan SevenandClaude Opus 4.8 33314a5889 feat(ucs): orient text-bearing annotations to the active UCS
New annotations now place their text and reading geometry along the
active UCS X axis (identity = world), so they read square to the user's
coordinate system instead of world-horizontal:

- TEXT / MTEXT: rotation defaults to the UCS X angle
  (DocumentTab::ucs_rotation_angle, model space only).
- Linear dimensions: measure along the UCS axis the span is nearest, and
  bake the UCS X angle as the text rotation so the value reads square
  even under a style that forces horizontal text. DIMCONTINUE / DIMBASELINE
  derive their axis from the base dim's rotation (any angle), not a
  world H/V test.
- LEADER: the MText annotation rotates to the UCS, and the hookline runs
  along the leader's horizontal_direction (set to UCS X), used by the
  renderer instead of world horizontal.
- MLEADER: context.text_direction = UCS X drives both the text rotation
  and, in the renderer, the text side and dogleg/landing direction.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 18:18:35 +03:00
Hakan SevenandClaude Opus 4.8 de1f1c0e24 fix(3d): move and paste ACIS solids to the right place
Solids were missing from the entity transform dispatch, so move / rotate /
scale / mirror were silently no-ops on a 3D solid (only its wires shifted)
and a pasted solid stayed at its original location. Add a Transformable impl
for Solid3D/Region/Body/Surface (delegating to acadrust, which now composes
the move into the ACIS body placement) and register them in the dispatcher.

Also make cross-drawing paste land at the cursor: the clipboard centroid is
measured in the source drawing's offset-relative frame, so correct the paste
translation by the source/target world_offset difference (zero within one
drawing). Bumps acadrust for the solid-geometry transform fixes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 02:46:56 +03:00
Hakan SevenandClaude Opus 4.8 31ed911714 fix(3d): place and move ACIS solids and tilted-normal blocks correctly
Bump acadrust to pick up real ACIS body transforms (so moving/copying/
rotating a 3D solid relocates its geometry) and OCS-aware INSERT placement.

- body_transform: read the SAT transform record from its first 13 float
  tokens instead of by raw index, which skipped the matrix because the
  record leads with a book-keeping pointer — so the body placement was
  silently ignored and solids never honored their stored transform.
- Insert grips: the grip lives in world space but insert_point is stored in
  the block's OCS, so grip display and grip-drag now round-trip through the
  OCS. A block whose extrusion normal isn't +Z no longer shows its grip in
  the wrong place or drags along the wrong axes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 01:52:53 +03:00
Hakan SevenandClaude Opus 4.8 490c0f6cf9 feat(properties): show polyline length and area
Line, Circle and Arc reported their lengths/quantities in the Geometry
section, but polylines (and rectangles, which are closed polylines) only
listed vertex count, closed flag and elevation. Add read-only Length and
Area rows: length is the true path length with arc segments measured by
their arc length, area treats the polyline as closed with a circular
segment correction per bulge.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 21:14:13 +03:00
Hakan SevenandClaude Opus 4.8 2a260be31d fix(dim): restore DIMTAD=Outside placement, route all DIMTAD values
The previous DIMTAD fix made every non-below value use the text-up side,
which broke DIMTAD=2 (Outside) — that should sit on the side farthest from
the measured points. Pass the DIMTAD value through instead of a below flag:
above (1) / JIS (3) / centred (0) use the text-up side, below (4) the
opposite, and outside (2) the away-from-object side. Tests cover above-up
and outside-away. (#144)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 17:38:57 +03:00
Hakan SevenandClaude Opus 4.8 ac5d92c0b4 fix(dim): place DIMTAD "above" text by text-up side, not object side
Dimension text vertical placement chose the perpendicular side away from the
measured points, so "Above" (DIMTAD 1) flipped to below whenever the
dimension line sat on the opposite side of the geometry. Place it on the side
the text top points to instead — the perpendicular of the dimension line
normalised to read left-to-right — independent of the object. (#144)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 17:32:56 +03:00
Hakan SevenandClaude Opus 4.8 23844c137b fix(dim): keep dim line continuous when text sits above/below it
The dim-line break test used text_half + DIMGAP as its vertical
threshold, but DIMTAD 1/4 places the text exactly text_half + DIMGAP off
the line. The two terms cancel at the same DIMSCALE-scaled value, leaving
the comparison on the knife's edge — so a gap flickered into the line
depending on the DIMGAP/DIMSCALE combination. Drop DIMGAP from the
vertical threshold so above/below text stays strictly clear and the line
stays whole; centred text (DIMTAD 0) still breaks for the DIMTFILL
background. The horizontal half-width keeps DIMGAP so a real break clears
the text. (#94)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 21:09:56 +03:00
Hakan SevenandClaude Opus 4.8 96ec672d5b feat(points): point style commands + DDPTYPE dialog, viewport-aware size
Points could only render as a 1px dot — no way to change PDMODE/PDSIZE.

- PDMODE / PDSIZE commands set the header value and rebuild the glyphs.
- DDPTYPE opens a Point Style dialog: a 5x4 grid of rendered glyphs
  (shape x enclosure), a size field, and relative/absolute radios.
- Relative (<=0) PDSIZE now sizes from the current world-per-pixel so the
  marker stays a roughly constant on-screen size across zoom, rebuilding
  on zoom via the existing camera-generation cache key. Absolute (>0)
  scales with the world as before. Switching to absolute with a zero size
  seeds a positive value from the on-screen size so it stays representable.

Part of the #100 extensibility epic (point style surfaced in #106).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 11:13:12 +03:00
Hakan SevenandClaude Opus 4.8 a384a163f3 fix(dim): scale DIMGAP by DIMSCALE for text placement
The text-to-line gap used a raw DIMGAP while text height already scaled
by DIMSCALE, so dimension text was misplaced whenever DIMSCALE != 1.
Thread dim_scale into dimension_text_pos_f64 and text_fill_rect and scale
DIMGAP the same way as DIMTXT.

Closes #94

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 05:32:31 +03:00
Hakan SevenandClaude Opus 4.8 3666c7fa69 refactor(modules): rename home module to draw, restore first-tab order
The Home tab was already titled "Draw"; rename the module to match:
folder modules/home → modules/draw, HomeModule → DrawModule, id "home"
→ "draw". The inner draw/ command folder is kept (modules::draw::draw).

Update build.rs PRIORITY ("home" → "draw") so the tab returns to the
front of the ribbon; registry.rs regenerates accordingly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 11:49:14 +03:00
Hakan SevenandClaude Opus 4.8 7fe7e0230d refactor(scene): group modules into role-based subfolders
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]>
2026-06-16 11:33:46 +03:00
Hakan SevenandClaude Opus 4.8 cd048165a9 feat(fonts): system TrueType support with shaping and fallback
Add a TrueType/OpenType render path alongside the LFF stroke engine so
users can pick fonts installed on their machine for text and MText.

- Discover system families via fontdb (sysfont) and list them in the
  text-style picker.
- Extract glyph outlines with ttf-parser, flattened into wire strokes
  normalized to the same 9-unit cap-height space as LFF glyphs.
- Shape runs with cosmic-text (ligatures, Arabic joining, kerning, bidi)
  plus automatic font fallback: a glyph missing from the chosen family
  is taken from another installed font.
- Unify both sources behind a single Face type; a shared run tokenizer
  drives the stroke (per-glyph) and shaped (TTF) renderers identically.
- Stroke fonts now fall back to a system TTF (cosmic-text picks it) for
  characters no LFF font covers, replacing the unicode/iso3098 chain.

Also trim the embedded LFF set from 47 to 26, keeping the AutoCAD-SHX
equivalents and dropping LibreCAD-only fonts now covered by TrueType.

LFF builtins keep priority over same-named system fonts so DXF text
renders identically across machines.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 11:18:17 +03:00
Hakan SevenandClaude Opus 4.8 9d823363f1 feat(scene): render 3D surface entities (loft/sweep/extrude/etc.)
ACAD_SURFACE entities now load and render. Their NURBS faces are
tessellated by building a truck BSplineSurface from the ACIS `spline-surface`
control net and knot vectors, then sampling its parametric grid — reusing the
same NURBS kernel the Model tab already builds on. Analytic faces
(plane/cone/sphere/torus) continue through the existing tessellator.

- New spline_tess.rs: parse the ACIS `nubs` block (degrees, knots with the
  clamped end-multiplicity fix, control grid) into a truck BSplineSurface and
  mesh it.
- tessellate_surface + Surface wired through the mesh-build, fallback-wire,
  and entity-classification paths.
- Bump acadrust to the commit that adds the Surface entity.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 20:11:44 +03:00
Hakan SevenandClaude Opus 4.8 e263cb2458 fix(dimension): break the dim line behind on-line text so DIMTFILL reads (#94)
2-D fills (the DIMTFILL background) render in a pass before all wires, so
the background could never sit above the dimension line — the line showed
through it. When the text sits on the line, gap the dimension line across
the text box (AutoCAD-style break) so the background reads cleanly over
the line. Text placed above/below the line is unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 10:09:28 +03:00
Hakan SevenandClaude Opus 4.8 6aa47c6ead fix(dimension): DIMZIN feet/inch suppression and DIMJUST 3/4 rotation (#94)
- DIMZIN now drives architectural feet/inch zero-suppression (modes 0-3)
  inside format_architectural, threaded through format_with_unit (primary
  and alternate units).
- DIMJUST 3 and 4 (over first / second extension) now rotate the text 90°
  to read along the extension line, not just shift its position.
- DIMTAD 2 (outside) and 3 (JIS) resolve to the away-from-object side,
  which equals "above" for 2-D linear dims; clarified in a comment.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 09:42:11 +03:00
Hakan SevenandClaude Opus 4.8 edacc895f7 fix(dimension): honour DIMTAD below and DIMTFILL background fill (#94)
Two style settings were accepted by the editor but ignored by the
renderer:
- DIMTAD=4 (Below) placed text above the dimension line like DIMTAD=1;
  the text is now offset to the opposite side.
- DIMTFILL=1 (drawing background) drew no fill; it now masks the geometry
  behind the text with the background colour (DIMTFILL=2 already used
  DIMTFILLCLR).

Verified the other enum settings render: DIMLUNIT/DIMFRAC unit formats,
DIMAUNIT/DIMADEC/DIMAZIN angular formats, DIMJUST positions,
DIMATFIT/DIMTMOVE fit, DIMTOLJ tolerance alignment, and the alternate-unit
/ zero-suppression families.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 09:37:01 +03:00
Hakan SevenandClaude Opus 4.8 b211ca07a9 feat(dimension): flip arrowheads outside when they don't fit (#94)
When the dimension line is too short for the arrowheads, they are now
placed outside the extension lines (tip on the line, body outward) with a
short stub to sit on, instead of overlapping inside. DIMSOXD suppresses
those outer stubs. Ticks are unaffected (they always fit).

DIMATFIT's exact mode ordering and DIMUPT remain for a later pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 02:13:00 +03:00
Hakan SevenandClaude Opus 4.8 40a91372fd feat(dimension): honour DIMTIX/DIMATFIT text fit; clarify remaining flags (#94)
Centred dimension text that doesn't fit between the extension lines now
slides outside (text-outside placement), unless DIMTIX forces it to stay
inside. DIMTOFL is already satisfied for linear/aligned dims (their dim
line is always drawn between the extension points).

The remaining style flags are documented in place:
- DIMATFIT arrow-outside modes and DIMUPT need an arrow autofit pass.
- DIMTXTDIRECTION (RTL) needs single-line text direction support.
- DIMARCSYM / DIMJOGANG apply only to arc-length / jogged-radius dims,
  which aren't shipped as entity variants.
- DIMUNIT is obsolete (superseded by DIMLUNIT).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 02:03:05 +03:00
Hakan SevenandClaude Opus 4.8 006dbe4631 fix(dimension): make DIMTAD/DIMTVP/DIMJUST drive text placement (#94)
Dimension text position ignored the style: the renderer returned the
saved text_middle_point whenever it was non-zero, which it always is for
app-created dims, so DIMTAD ("text over the dimension line" / ISO) and
friends had no effect — text could only be moved by grips.

Use the new acadrust text_user_positioned flag: when the text wasn't
user-placed, compute the position from the style (DIMTAD/DIMTVP/DIMJUST)
anchored on the dimension line (definition_point); when it was (grip drag
or properties edit, both now set the flag), keep the saved point.
Imported drawings keep their fidelity via the flag round-tripping through
DXF/DWG.

Also fixes linear/aligned text anchoring at the measured midpoint instead
of the actual dimension line.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 01:57:58 +03:00
Hakan SevenandClaude Opus 4.8 d09614be39 fix(ellipse,mirror): WCS ellipse rendering + delegate hatch MIRROR upstream
Two follow-ups from the boundary-arc convention audit:

- ELLIPSE is a WCS entity in DXF (codes 10/11 are world coordinates,
  unlike ARC/CIRCLE). The renderer ran center and major_axis through the
  arbitrary-axis OCS, which misplaced any ellipse whose normal isn't Z-up
  — e.g. the (0,0,-1) result of a mirrored-block explode. Read them as
  WCS; pairs with acadrust 569bd4d, which stores WCS on its explode
  paths. Z-up ellipses are unaffected (the conversion was an identity).

- The MIRROR command's hand-rolled hatch closure swapped boundary-arc
  angles itself — only valid for ccw arcs and ignorant of the stored
  mirrored-angle / wrap-encoded-sweep conventions. It now builds a
  reflection Transform (new reflection_about_xy_line helper) and
  delegates to acadrust's transform_hatch, the single source of truth
  for those conventions. Covered by a Line→Arc→Line continuity + sweep
  regression test.

Note: full effect of the ellipse change lands once the acadrust pin is
bumped to 569bd4d; until then only the (previously broken) non-Z-up
corner differs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 09:03:18 +03:00
Hakan SevenandClaude Opus 4.8 80059922c5 fix(mirror): negate polyline bulges in the MIRROR command
The MIRROR command's LwPolyline / Polyline2D closures reflected the
vertices but left the bulges untouched, so every curved segment of a
mirrored polyline bowed to the wrong side. A reflection reverses the
side a bulge arc bows to — negate it alongside the vertex reflection
(same convention acadrust applies on its reflecting-transform paths).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 01:17:47 +03:00
Hakan SevenandClaude Opus 4.8 25715d1b03 fix(grips): compute grip positions in f64 so they sit on the wire
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]>
2026-06-11 00:04:37 +03:00
Hakan SevenandClaude Opus 4.8 70702d063f perf(hash): swap std HashMap/HashSet for rustc-hash FxHash
Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.

Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-05 00:35:53 +03:00
Hakan SevenandClaude Opus 4.8 edc8cbc969 feat(scale): drive scales from the drawing's scale list
Read the model-space annotation scale from the standard CANNOSCALE /
CANNOSCALEVALUE header variables instead of a user-variable hack, and
populate both the status-bar scale picker and the viewport Scale
property purely from the drawing's ACAD_SCALELIST — no built-in
fallback set. Xref-derived scales (`_XREF` suffix, `name|` prefix)
are filtered out so only the host drawing's own scales appear.

Bumps the patched acadrust to the commit that adds CANNOSCALE/
CANNOSCALEVALUE read/write.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-04 16:18:01 +03:00
Hakan SevenandClaude Opus 4.8 1b20d7d781 feat(table): coloured synthesis render honouring all TableStyle fields
Tables without a baked block (e.g. created in-app) were drawn as a single
monochrome wire, ignoring cell colours/fills/border styling. Add a
coloured synthesis path that emits per-colour WireModels for:
- cell background fill (fill_color + fill_enabled),
- per-cell text in its content/text colour,
- grid borders with per-edge type/weight/colour/visibility (cell-style
  override → row-style → default),
- horizontal/vertical margins and flow direction (down/up).

Imported tables keep using AutoCAD's baked block. Resolution layers the
per-cell CellStyle over the TableStyle row style over defaults.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-04 02:28:12 +03:00
Hakan SevenandClaude Opus 4.8 8b3d083789 feat(leader): dynamic MLEADER text placement, grips, and consistent arrows
Reworks MLEADER text/leader geometry and grip editing, and aligns LEADER
to MLEADER's conventions (kept as separate entities).

Text & landing (render path):
- Text is side-anchored on the leader-facing edge and reads outward; the
  side, alignment and the horizontal landing are recomputed every frame
  from the text grip's position relative to the leader, so moving the
  arrow or the text re-mirrors the whole layout and the landing stays
  attached to the text's near edge.
- Fixes #49: text was centred on the leader endpoint regardless of side.

Grips:
- Text-location grip plus an MTEXT-style triangle wrap-width grip at the
  box's far edge; dragging it sets the MText wrap width.
- Multifunction grip menu restored and made functional: leader-line
  vertices offer Add Leader / Remove Leader (Add grabs the new arrow so it
  follows the cursor — click places, Esc removes it); the text grip offers
  Move Independent (text only) and Move with Leader (translates the whole
  multileader via a move-all sentinel grip).

Arrows:
- LEADER and MLEADER arrowheads now render at the same size (text height),
  matching previews.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-03 16:17:29 +03:00
Hakan SevenandClaude Opus 4.8 ac9022676f feat(mtext): editable preview with caret, blink, and new-line support
Type directly into the rendered MText preview: click to position the
caret, insert/delete, Enter for a new line, Space for a literal space,
and arrow keys to move. The caret blinks when idle.

Keep the trailing empty paragraph in the editor so the caret appears on a
fresh line immediately after Enter, before any typing. parse_mtext_paragraphs
split into a _ex variant; layout_mtext skips the blank-edge trim when
emitting glyph boxes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-05-31 13:14:35 +03:00
Hakan SevenandClaude Opus 4.8 c1b33677e2 feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
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]>
2026-05-31 03:31:20 +03:00
Hakan SevenandClaude Opus 4.8 32136dd9dd feat(mtext): character-level text selection on the preview
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]>
2026-05-31 02:33:47 +03:00
Hakan SevenandClaude Opus 4.7 9db206f310 feat(select): Select Similar + Quick Select panel (#28)
Two ways to extend the current selection by entity type + layer:

* `Select Similar` (right-click → Select Similar, or `SELECTSIMILAR` /
  `SELSIM`): for every (type, layer) pair in the current selection, add
  every entity in the active layout that matches the pair. The seed
  selection stays selected; matches already selected are not
  double-counted.
* `Quick Select` (right-click → Quick Select…, or `QSELECT` / `QS`):
  opens a centred floating panel with Object-type and Layer pick_lists
  (populated only with values that actually exist in the active layout)
  and an "Append to current selection" checkbox. `(Any)` in either slot
  skips that filter. Apply replaces the selection (or extends it with
  Append on) and reports the match count on the command line. Cancel,
  Esc, and outside-click all dismiss without applying. When opened with
  a selection present, the panel pre-fills both filters from the first
  selected entity so QSelect doubles as an editable Select Similar.

Scoped to entities owned by the active layout's block-record so a paper
layout's QSELECT never reaches into model-space entities (and vice
versa). Entity-type names live in a single `entity_type_name` helper in
`entities::traits` to keep the UI strings and the filter keys aligned.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 20:30:56 +03:00
Hakan SevenandClaude Opus 4.7 4b4fe5ca6d feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.

Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.

Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-29 15:14:43 +03:00
Hakan Seven 559cd76369 feat(grip): value-prompt actions Lengthen/Radius/Arc Length
Lengthen / Radius / Arc Length need a number after the pick. Stash
`grip_pending` on the picked action, push prompt to command line,
route next `CommandSubmit` parse into `apply_grip_menu_value`.
Escape cancels.

- `Grippable::grip_menu_value_prompt` returns label when action
  wants value; `apply_grip_menu_value` runs the edit. Dispatched
  via `EntityTypeOps`.
- Line: Lengthen extends either endpoint along line direction.
- Arc: Radius sets radius; Arc Length redrives end angle from
  `value / radius`; Lengthen extends start / end angle the same
  way.
2026-05-29 14:58:35 +03:00
Hakan Seven a4c65649e1 feat(grip): entity-specific popup menus for Line/Arc/Dimension
- Line: endpoint grips expose Stretch + Lengthen; midpoint exposes
  Stretch only.
- Arc: endpoint exposes Stretch + Lengthen; midpoint exposes
  Stretch + Radius + Arc Length; centre exposes Stretch.
- Dimension text grip exposes the full text-position vocabulary
  (Move with Dim Line / with Leader / Independent, Reset Text,
  Rotate Text, Above Dim Line, Center). Dim-line-position grip
  exposes Stretch + Reverse Arrows. Extension origins keep the
  default Stretch.
- Each affected entity drops `impl_entity_basics!` and writes the
  three trait impls (Grippable, PropertyEditable, Transformable)
  manually so `grip_menu` / `apply_grip_menu` can override.
- `apply_grip_menu` handles Reset Text (clear text override) and
  Center (snap text to midpoint of extension origins) inline;
  remaining actions stub to no-op pending the follow-up
  prompt / drag plumbing.
2026-05-29 09:46:05 +03:00