Commit Graph
23 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 fa322991c6 fix(text): clip SDF text to xclip boundary; stop debug-assert crash on block fills
Two fixes for opening drawings with XCLIP'd blocks (crashed / mis-rendered
since SDF text became the default).

- block_cache emit_wire: the fill_tris/fill_tris_low length debug_assert
  panicked in debug builds on a legitimate case — a Leader/dimension arrowhead
  fill nested in a block carries fill_tris with an empty fill_tris_low (the
  WireModel contract's "all-zero low" form, which the code already handles via
  get().unwrap_or). Relax the assert to permit the empty case; a *partial* low
  half is still caught.
- xclip clip_wires: also clip the SDF glyph quads. Block-internal text was
  bleeding past the clip boundary (mixed batch) or vanishing (the tail retain
  dropped empty-points wires). Now each glyph quad is kept/dropped by its
  centroid vs the boundary (whole-glyph granularity), text_verts-carrying wires
  survive the retain, and the pick-box AABB is recomputed from the surviving
  glyphs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 21:32:17 +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 <noreply@anthropic.com>
2026-07-07 21:07:00 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
Hakan SevenandClaude Opus 4.8 970ee5a09f fix(blocks): layer-0 children inherit the block instance layer (#221)
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.

Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.

Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
  explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities

New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:24:04 +03:00
Hakan SevenandClaude Opus 4.8 a93271c1bd fix(block-cache): address PR211 review follow-ups
- expand_defn: dedup the <1px baseline LOD branch the same way the <5px
  greek branch does, so colour-split MTEXT no longer stacks N overlapping
  baselines (one per \C segment) over the same OBB.
- sysfont: memoise canonical_family_name process-wide. resolve_font calls
  it once per word on the MTEXT measure hot path and Face::resolve re-runs
  it right after; the fontdb query + linear family scans were uncached.
- dimension/multileader/table: reword the 6 fill_tris_low FIXMEs. They
  cited a stale line and the wrong consumer — these fills render on the
  top-level path (panic-safe .get().unwrap_or), not the block cache, so
  they cannot trip emit_wire's debug_assert. State the real status:
  latent f32-precision debt, not a crash. Also fix the mis-indented
  text-fill WireModel in dimension.rs.
- tests: add a host-independent colour-split test (two \C segments in a
  block keep ≥2 distinct wire colours — guards the per-wire colour fix)
  and a TTF-gated UTM fill test (paired, non-zero fill_tris_low). Note the
  arial-style test's LFF-fallback limitation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:57:51 +03:00
Hakan SevenandClaude Opus 4.8 a84da122f4 fix(block-cache): harden fill_tris_low access against unpaired wires
emit_wire reconstructed fill verts via a raw `fill_tris_low[idx]` index,
guarded only by a debug_assert. Slice bounds checks are not gated by
debug-assertions, so a Leader with a filled arrowhead nested in a block
(populated fill_tris, empty fill_tris_low via the low-level tessellate
early-return) panicked in release as well as debug.

Use the panic-safe `.get(idx).copied().unwrap_or([0.0; 3])` — the same
"empty = all-zero low half" contract the other fill consumers
(face3d_gpu, xclip) already honour. The debug_assert stays as a tripwire.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:42:16 +03:00
Karim Jerbi ae2946a8a9 fix(block-cache): emit one greek substitute per source MTEXT entity
Colour-split MTEXT produces one outline wire per inline \C/\c override. At <5px zoom each outline independently emitted the full OBB greek rectangle — N overlapping boxes with last-colour-wins. Track last_lod_obb and skip duplicate substitutes.

Also corrects the fill-only LOD bypass: suppress fill-only wires at <5px instead of sending them unconditionally through emit_wire. The outline sibling's baseline or greek box suffices at sub-pixel zoom.
2026-06-28 19:17:24 +01:00
Karim Jerbi 7da8fbe316 fix(block-cache): preserve per-wire colour and fill precision in cached blocks
Three related bugs in tessellate_sub_local and expand_defn:

1. Wire folding lost per-segment colour. Multi-wire tessellator output
   was concatenated (NaN-separated) into a single LocalWire using the
   first wire's colour. Inline \C/\c MTEXT colour overrides inside block
   definitions rendered in the entity colour instead of the override colour.
   Fix: tessellate_sub_local returns Vec<LocalWire>; each wire from the
   tessellator becomes its own LocalWire with its own colour.

2. fill_tris_low was never forwarded. The double-single precision path
   was silently degraded on cached wires. Fix: fill_tris_low forwarded
   from WireModel through to LocalWire and BatchEntry. debug_assert_eq!
   added at emit site to catch future invariant violations.

3. Fill-only wires hit the text LOD ladder. MTEXT outline/fill pairs
   produce separate LocalWires; the fill-only wire was routed through
   emit_text_baseline/emit_greeked_text which ignore fill_tris (invisible
   double-draw). Fix: is_fill_only field on LocalWire bypasses the LOD
   ladder. Also fixes StyleKey::fill_only being hardcoded false at both
   emit_wire call sites (latent batch misclassification).
2026-06-28 18:45:57 +01:00
Hakan SevenandClaude Opus 4.8 bbf148f601 refactor: remove block_cache local_offset (fully absolute + double-single)
With the relative-to-eye double-single render path, the per-defn
`local_offset` precision origin is redundant: block geometry can be stored
absolute and stays precise via the high/low residual. Remove it
end-to-end:

- Drop the Pass-1 centroid computation, the BlockDefn.local_offset field,
  and the `defn_lo` parameter threaded through emit_wire /
  emit_greeked_text / emit_text_baseline / transform_tangent and the AABB
  helper (transform_offset_aabb_xy → transform_aabb_xy), plus the nested
  parent/child offset re-basing.
- Store text_obb_local as absolute f64 (was offset-relative f32) so the
  greek rectangles stay precise; emit splits to double-single. Give the
  greek-baseline points a low residual too.

Geometry is now absolute + double-single throughout the block cache,
matching the top-level path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:37:52 +03:00
Hakan SevenandClaude Opus 4.8 5a8ec35f4f fix(blocks): restore per-defn precision after world_offset removal
The world_offset removal stripped two precision mechanisms in the block
cache that were masked while geometry was offset-relative (small coords)
but broke once coordinates became absolute UTM:

- build_defn no longer subtracted the defn's `local_offset` from the
  tessellated wire (it was passed as tessellate's world_offset). The defn
  was stored absolute while expand still re-added local_offset → block
  lines shifted. Restore the subtraction (with a double-single re-split)
  locally in tessellate_sub_local.
- Block fill triangles (greek-text rectangles, block solid fills) were
  batched with f32-only `fill_tris` and an empty `fill_tris_low`, so at
  UTM scale the absolute f32 quantized to ~0.5 m and the greek rects
  sheared. Plumb `fill_tris_low` through BatchEntry and emit the
  double-single residual from emit_wire and emit_greeked_text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:04:41 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
Hakan SevenandClaude Opus 4.8 ef55b49db5 refactor(paste): one finalize path; carry XCLIP through all three pastes
Consolidate the duplicated paste logic and extend XCLIP-carry to every path.

- finalize_paste(): single shared step for PASTECLIP and PASTEORIG —
  merge deps + block defs + xdictionary graphs, add clones (optionally
  transformed), tessellate solids. A new cross-drawing concern is now wired
  once instead of re-implemented per command (the gap that lost the clip on
  PASTEORIG in the first place).
- recreate_clipboard_ext_roots() + recreate_ext_subtree(): PASTEBLOCK now
  recreates each entity's xdictionary graph and stamps the new root onto the
  wrapped entity before defining the block, so the block's nested insert
  keeps its spatial filter.
- block_cache: a nested insert's clip is baked into NestedRef.clip_poly at
  build time (the spatial filter isn't reachable at expand time) and applied
  on expansion — the clipped wires are finalized in an isolated batch set,
  clipped, and carried out via Batches.extra_wires. Single-instance only;
  arrays of clipped nested inserts fall through unclipped.

All three paste paths (PASTECLIP / PASTEORIG / PASTEBLOCK) now preserve an
XCLIP clip across a cross-drawing paste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:53:28 +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 <noreply@anthropic.com>
2026-06-23 22:23:56 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.7 6a74f12a92 feat(render): double-single vertex positions for the wire pipeline
Vertices now carry the low residual alongside the high half all the way
to the GPU: tessellation splits the offset-relative f64 coordinate into
(high, low) f32 (TruckTessResult::Lines/Point, the Text path, Lines /
SegmentedLines, and the block-cache instance transform), WireModel and
the block batch carry a paired points_low buffer, WireInstance gains
pos_a_low/pos_b_low (locations 10/11), and wire.wgsl reconstructs
rel = (pos_high - eye_high) + (pos_low - eye_low).

This is the piece the camera-only RTE was missing: at UTM-scale
coordinates the single-f32 vertex was quantized to ~0.5 m, so it snapped
each time the eye crossed an f32 boundary on pan. With the low half the
position is exact and the cross-drawing-paste jitter is gone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.8 caafb9b99d fix(layers): honor layer off/freeze for block sub-entities
Block-internal entities ignored their own layer state: 2D subs were
only filtered by the invisible flag in the block-defn cache, and 3D
solids render through a separate mesh path that applied no layer filter
at all. Both now skip subs on off/frozen layers, matching top-level
entities. Caches rebuild on every toggle, so visibility updates live.

Fixes #133

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:57:19 +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 <noreply@anthropic.com>
2026-06-17 11:13:12 +03:00
Hakan SevenandClaude Opus 4.8 9ca19a9db8 feat(wasm): sequential rayon shim for the web build
wasm32 has no threads, so rayon's global pool would panic at runtime.
Add `crate::par::prelude`: native re-exports `rayon::prelude`, wasm
provides sequential `std`-iterator equivalents (par_iter / into_par_iter
/ flat_map_iter over slices, Vec, HashMap, HashSet). All call sites now
`use crate::par::prelude::*;` instead of rayon directly.

- wire_gpu's rayon `reduce` (2-arg, no std equivalent) is cfg-split to
  a sequential flat_map concat on wasm.
- rayon moves to a non-wasm target dependency.

Native still runs in parallel; all 75 tests pass. wasm bin compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:11:20 +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 <noreply@anthropic.com>
2026-06-16 11:33:46 +03:00