Commit Graph
653 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 9b8ee94c0c refactor: rename mislabeled scene files to match contents
Two names collided with the pick subsystem and misdescribed their files:

- scene/hittest.rs -> scene/camera_ops.rs. Its contents are camera/view
  operations (zoom, fit-all, named-view restore, camera<->document sync,
  per-tile grid/snap), not hit-testing — the real screen-space hit-test
  lives in scene/pick/hit_test.rs.
- scene/pick/selection.rs -> scene/pick/selection_state.rs. It holds only
  the SelectionState struct; the name clashed with scene/selection.rs
  (the Scene selection operations).

Callers are unaffected (the crate::scene::SelectionState re-export path is
unchanged). No behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:45:16 +03:00
Hakan SevenandClaude Opus 4.8 1cd7aaed4e refactor: move parallel tessellation fns into scene/convert/
scene/tess.rs held the free parallel-tessellation functions
(tessellate_entity, lod_stub_wire, entity_aabb, …) that the giant-file
split had left at the scene root. They belong with the rest of the
tessellation code, so move the file to scene/convert/tess.rs alongside
tessellate.rs / tess_util.rs and re-point the re-export.

Leaves the scene/ root as a consistent set of `impl Scene` method
groups; the free-function outlier is gone.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:39:43 +03:00
Hakan SevenandClaude Opus 4.8 415dbe08d4 refactor: move resource/IO root modules under io/
Relocate three root-level modules into io/, where they belong by concern:

- update_check.rs -> io/update_check.rs (its own doc-comment notes it
  mirrors how crate::io wraps blocking calls in async).
- patterns.rs, linetypes.rs -> io/ (standard resource catalogs parsed
  from asset files and populated into the document — IO-shaped work).

Fix the moved files' include_str! paths (one level deeper), add the
mod declarations to io/mod.rs, drop them from lib.rs/main.rs, and
re-point all crate:: references to crate::io::. No behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 23:06:40 +03:00
Hakan SevenandClaude Opus 4.8 bddc050b3e refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.

- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
  dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
  with thin delegating arms; the 66 fattest arm bodies move to topic
  methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
  mspace,project,selection,modify,group_layer,preview}. The impl Scene
  body is split across files via inherent-impl-per-file; struct, ctor,
  fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
  The 233-arm dispatch match becomes source-ordered per-family
  dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.

Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 22:51:29 +03:00
Hakan SevenandClaude Opus 4.8 2baabdf4a0 feat(viewport): reorder model controls, add close-vport button
Reorder the model-tile control chip to grid, grid snap, render mode,
vertical split, horizontal split. Add a close button (shown only with
more than one model tile) that merges the active pane into its
longest-contact neighbour.

Divider drags now clamp each side to the minimum tile size, so dragging
a boundary to the screen edge no longer collapses a pane — closing is
done with the button instead.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 21:15:00 +03:00
Hakan SevenandClaude Opus 4.8 169a8cde3f chore: clear dead-code and unused-import warnings
Remove the unused `crate::par::prelude` glob import in wire_gpu.rs and
the never-called `viewport_at_paper_point` method; fold its doc into the
screen-space twin that is actually used so no intra-doc link dangles.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 18:04:52 +03:00
Hakan SevenandClaude Opus 4.8 14ebdd36d1 perf(render): hold model wire set static instead of re-tessellating on camera moves
The model-space wire geometry is camera-independent, yet every pan/zoom
re-tessellated the whole model and re-uploaded it to the GPU. On a
block/text-heavy drawing that is millions of segments, costing a ~100ms
re-tessellation plus a ~150ms GPU re-upload per camera change (~258ms
freeze) while the GPU itself draws the full set comfortably in real time.

Tessellate the full, un-culled model once per geometry epoch, hold it
resident, and return it for every camera and tile. A pan/zoom now only
changes the view uniform; no frustum cull, no zoom LOD, no re-tess or
re-upload on camera moves. Pan/zoom frame time drops to ~7ms (~140fps)
with no stutter.

Removes the now-dead per-tile cull cache, pan-invariant hash, AABB
containment helper, and the reuse-during-motion / settle-regen machinery
that an earlier iteration introduced. Also exempts Face3D from the
sub-pixel stub LOD: a flat face is trivially cheap to tessellate, so it
is drawn full at any zoom and no longer pops a cube stub at the threshold.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 16:06:11 +03:00
Hakan SevenandClaude Opus 4.8 4e8521010b fix(wipeout): keep fill boundary precise at UTM coordinates
`wipeout_boundary_2d` built the fill boundary from the wipeout's
insertion point cast straight to f32, stored in a HatchModel with
world_origin = [0, 0]. At UTM-scale coordinates (~1e6) f32 resolves to
~0.06, so the boundary collapsed and the relative-to-eye fill shader
cancelled `boundary - eye` to noise — the mask rendered as squares.

Return the boundary as small f32 offsets relative to a world_origin
(the insertion point, kept in f64) and set HatchModel.world_origin to
it at both the model-space and paper/viewport fill sites. Same fix as
the wide-polyline and on-create hatch fills.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 11:20: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 0317dad14e perf(render): hoist per-wire constants to a storage buffer (native)
Every wire segment is one instance, and each carried the wire's color,
line-weight, dash pattern and draw-depth (~44 B) inline — re-fetched per
segment even though they're constant along the wire. On native, move
those into a per-wire `WireConst` storage buffer (group 1) indexed by a
`wire_id` on the instance. The instance drops from 104 B to 60 B (~42 %)
across every entity's wires, and the shared constants are fetched once
per wire (cached) instead of once per segment — less vertex-attribute
bandwidth and less GPU upload on rebuilds.

WebGL2 has no vertex-stage storage buffers, so the wasm build keeps the
original self-contained fat instance and wire.wgsl; native uses the new
slim instance, wire_indexed.wgsl, and a group-1 bind group built per
batch in WireGpu::from_run / from_batch. Visual output is unchanged
(colors are now full f32 rather than 8-bit, if anything more precise).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 10:39:23 +03:00
Hakan SevenandClaude Opus 4.8 0db513e07a fix(hatch): one grip per corner, and live preview while editing
Two follow-ups to dropping the hatch boundary outline from the wire set:

- `closed_outlines` copied segment-list wire points (LwPolyline emits
  each segment as a NaN-separated pair, so every shared corner repeats)
  straight into the hatch boundary, giving two grips at each corner.
  Collapse the points back into a clean ring — skip NaN separators and
  any vertex coincident with the previous one, and drop a trailing
  vertex equal to the first. One grip per corner; point-in-polygon and
  the BOUNDARY / GRADIENT commands get the clean ring too.

- Edit previews (move / copy / array / grip-drag) tessellate the edited
  entity into preview wires, but a hatch now produces none, so a dragged
  hatch didn't update until release. Build a live outline from the
  current HatchModel for the preview only (`apply_grip` keeps it in
  step), so the shape tracks the cursor again. The normal render still
  shows just the fill.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 09:47:29 +03:00
Hakan SevenandClaude Opus 4.8 1831a63236 perf(hatch): drop the boundary outline wire from the render set (#131)
A HATCH was emitting its boundary as a dense outline polyline in the
wire set on top of its fill. On hatch-heavy drawings that outline was
the dominant wire-instance cost — 14.9M of 19.1M segments on the #131
sample — and on integrated GPUs (shared system RAM) the per-segment
instances pushed memory past the OOM threshold on open.

Skip the outline in `tessellate`: hatches render as a fill, are
highlighted by their fill tint when selected (#71), and are still
picked by their fill area via the existing `click_hit_hatch` path, so
nothing else depends on it. Wire segments drop ~78% on the sample.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 09:31:59 +03:00
Hakan SevenandClaude Opus 4.8 21d0db0c25 fix(hatch): show fill on creation, not only after a boundary edit
`add_hatch` let `add_entity` build the render model from the new DXF
entity (via `hatch_model_from_dxf`, AABB-centred `world_origin`) and
insert it — then overwrote it with the command-built model, whose
`world_origin` is [0, 0]. After the world_offset removal the fill is
drawn relative-to-eye from `world_origin`, so a [0, 0] origin with a
full-WCS boundary cancels to garbage in f32 and the fill renders
off-screen — invisible until a later edit rebuilt it from the DXF.

Drop the overwrite so creation keeps the correct seed; it now matches
what an edit or reload produces. Fixes the regression for hatches drawn
away from the origin.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 09:31:59 +03:00
Hakan SevenandClaude Opus 4.8 34e750407b fix(render): draw command-preview wires always on top
Preview / interim / grip-drag overlay wires were drawn in the main wire
pass with the depth-tested wire_pipeline (depth_compare=LessEqual), so a
live command preview could be hidden behind occluding geometry — a 3D
solid, or 2D geometry drawn in front of it.

Draw them with the existing wire_xray_pipeline instead (depth_compare=
Always, no depth write), the same always-on-top path selection highlights
already use. Previews are now never occluded, for every command.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 07:29: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
Hakan Seven e9cb9af924 Merge PR #180: TTF solid glyph fills, font fallback, Z-order fix 2026-06-26 00:20:59 +03:00
Hakan SevenandClaude Opus 4.8 012ec3febc fix(viewcube): land face clicks upright, never upside-down
A drawing opened with a twisted view made a TOP-face click keep the
twist, so north ended up at the bottom. Add Camera::snap_to_face, which
snaps to a canonical orientation — north (UCS +Y) up for top/bottom,
world (UCS +Z) up for the side elevations — ignoring the current
up-sense. Face clicks use it; edges/corners still preserve their up so
they spin smoothly around the clicked feature.

The Home button routes through the same path, giving a reliable reset
for a twisted view.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 00:11:40 +03:00
Hakan SevenandClaude Opus 4.8 f00c48bebb feat(viewcube): add compass ring, cardinals, and nav controls
Build the ViewCube out into a full navigation widget:

- Compass ring painted in the cube's ground plane (z = base) so it
  reads as a ground compass and foreshortens with the view.
- N/E/S/W cardinals painted flat onto the ring band; clicking one snaps
  to that side elevation. Added S/W glyphs to the bitmap atlas.
- Home button (top-down view), roll arrows (90° roll about the view
  axis), and four nudge triangles (90° tip/spin) as SVG iced controls
  laid out around the cube. New home.svg.
- WCS / named-UCS selector under the cube, switching the active UCS.

The cube hit area now shares one stack with the control buttons, so a
click on a button is caught by the button while a click on the cube
falls through. Face clicks snap to the highlighted region rather than a
cursor position the viewport's move handler can overwrite.

Camera gains home_view / roll_by / nudge_90; floating viewports drive
the same ops through mutate_active_viewport_camera, which re-encodes the
result to the stored view direction + twist.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 23:46:43 +03:00
Hakan SevenandClaude Opus 4.8 6c4b981424 feat(viewcube): paint face names onto the cube surface
Lay each label's glyph quads out in the face's own local plane and
project the corners individually, instead of drawing screen-aligned
billboard text at the projected face centre. The letters now skew with
perspective and sit on the surface as the cube rotates.

Per-face (u, v) basis vectors are chosen so u x v equals the outward
normal, which keeps the text from ever reading mirrored.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 22:40:17 +03:00
Hakan SevenandClaude Opus 4.8 a72b143628 fix(copy): duplicate a dimension's baked block so COPY copies it (#161)
A dimension's drawn geometry lives in a baked anonymous *D block. COPY
cloned the Dimension entity (translating its definition points) but left
its block_name pointing at the source block, whose sub-entities stay at
the original location — so the copy rendered on top of the original and
appeared not to copy at all.

Add `clone_transformed_block`: when copying a dimension that has a baked
block, duplicate that block under a fresh *D name with every sub-entity
transformed by the same offset, and repoint the copy at it. The copy now
lands at the drop point with its baked geometry and text preserved (no
synthesis, so diameter/radius values stay correct). Covered by a portable
regression test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 22:30:45 +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 918f6c540f fix(layout): centre the camera on the sheet when first opening a layout
A new drawing's layouts carry paper limits but no sheet viewport; it is
created lazily on the first switch into the layout. That viewport kept
Viewport::new's default view (target 0,0 / height 210), so camera_from_view
aimed at the paper's bottom-left corner, and with no geometry yet the
fit_all fallback was a no-op — the layout opened off-centre.

Give the freshly created sheet viewport a view that frames the page:
view_target at the paper centre and view_height a little taller than the
sheet. The first switch now opens centred on the page, and the view persists.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 18:55:20 +03:00
Hakan SevenandClaude Opus 4.8 8f8fa40279 fix(scale): restore default scale list when drawing has none (#154)
Driving the annotation / viewport scale picker purely from the drawing's
ACAD_SCALELIST regressed every drawing that carries no Scale objects
(most fresh drawings and minimal DXF/DWG exports): scale_list() returned
empty, so the status-bar scale popup opened with nothing to pick and the
button looked dead.

Fall back to the standard ratio set (1:500 … 1:1 … 10:1) when the drawing
defines no scales of its own; file-defined scales still take precedence
whenever present. Also re-enables the viewport-scale picker in the
properties panel for those drawings.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 18:44:02 +03: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 fcb97b12da fix(linetype): render dotted and dash-dot patterns (#149)
A dot in an LTYPE pattern is a zero-length element. The pattern resolver
encoded it as a tiny `0.01 * scale` world-length dash, which had two bad
effects: it was sub-pixel at normal zoom so the dash LOD saw the pattern's
smallest feature drop below one pixel and collapsed the whole line to
solid, and at larger LTSCALE it left only invisible sub-pixel dots between
big gaps. Either way dotted and dash-dot linetypes were never visible;
only plain dashed (no zero elements) worked.

Keep dots as literal 0.0 so they no longer drag `min_elem` down, and
render each as a fixed ~1.5 px mark in the wire shader (pixel-snapped via
world_per_pixel, mirroring the hatch shader's dot handling). The shader
tells a real dot from trailing array padding by element count — the index
of the last non-zero element plus one — so no producer needs a sentinel.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 17:37:24 +03:00
Hakan SevenandClaude Opus 4.8 6fb5c89a7b fix(hatch): preserve all island loops in complex hatches (#148)
The hatch boundary was flat-truncated to 1024 vertices before upload.
The active renderer is the batched path, which stores boundaries in a
GPU storage buffer with no length limit, so the cap only ever served
the per-hatch uniform path (now wipeout-only, and self-clamping).

On a real drawing a 36-loop, depth-3 nested hatch (1482 verts) was cut
mid-boundary: trailing island loops were dropped and the final partial
loop was left open, flipping the even-odd parity so the fill bled across
the rest of the shape and islands rendered incorrectly.

Drop the flat truncate. Keep every loop; only guard pathological vertex
counts (16384), and when trimming, cut at a NaN sentinel (whole-loop
boundary) so no sub-loop is ever left open.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 16:11:15 +03:00
Hakan SevenandClaude Opus 4.8 e6e8167b36 feat(render): boost model-space lineweight display for legibility (#147)
Lineweights were rendered at their true physical size (mm × 96/25.4 px),
which is ~2 px for a 0.5 mm line — too timid to tell weights apart, and
every weight ≤ 0.25 mm floored to 1 px (no visible difference). Other DWG
editors display model-space lineweights larger than true size for screen
legibility; apply the same 2× boost so weights are pronounced and
distinguishable.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 15:39:12 +03:00
Hakan SevenandClaude Opus 4.8 d641ecfea2 fix(viewport): frame paper viewports on the robust cluster centre
Floating viewports on a survey-coordinate drawing rendered blank: the
model camera was correct, but content viewports whose saved view was
rejected auto-fitted onto empty space.

Two causes, both in model-space extents handling:

- compute_model_space_extents tested vertex finiteness in f64 but stored
  f32. A long RAY/XLINE endpoint is a huge-but-finite f64 that overflows
  to inf in f32, poisoning the extents to (inf, inf). Also, the wire/mesh
  AABB paths checked only x while folding y in, so a vertical ray's
  infinite y leaked through. Now every component is validated after the
  f32 cast.

- camera_for_viewport framed its overlap test and auto-fit on the raw
  extents centre. When a drawing has a second, far cluster (a small-
  coordinate legend beside a UTM survey), that centre sits in the empty
  gap, so a valid saved view fails the overlap test and the auto-fit aims
  at blank space. cluster_extent_from_centers already computed a robust
  centre (median of entity centroids) but discarded it; surface it as
  Scene.local_center and frame on that instead.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 09:31:22 +03:00
Hakan SevenandClaude Opus 4.8 deec2a8fd3 fix(xclip): keep clipped hatch loops in world_origin-relative space
`clip_hatch_boundary` lifted each loop into the clip polygon's world
space (`+world_origin`) to run the Sutherland–Hodgman clip, but returned
the result in absolute world coordinates. The renderer adds the per-hatch
`world_origin` anchor back in the vertex shader, so clipped hatches with a
large origin (e.g. UTM drawings) were drawn double-shifted and lost f32
precision. Subtract the origin again on the way out so the output stays in
the same precision-preserving anchor space as the input.

Also drop a stale third argument from two xclip tests left over from the
world-offset removal, which had broken `cargo test` compilation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 00:15:58 +03:00
Hakan SevenandClaude Opus 4.8 bd1c51c682 fix(viewport): correct ViewCube flip + activation/border for off-screen viewports
ViewCube "already there → flip to opposite" tested the main (paper) camera,
which always looks straight down, so inside a floating viewport clicking
"top" landed on "bottom". Route the check through active_gaze_dir() — the
gaze of the camera the cube actually reflects.

Off-screen viewports:
- The active-viewport outline clamped only its origin while keeping full
  width/height, shifting the whole border inward. Clip it to the visible
  canvas (origin AND size) so it hugs the real viewport edge.
- Double-click activation tested each viewport's full paper rect, so a
  click on empty space beside a viewport that runs off-screen matched its
  partly-off-canvas rect and switched to it. Activation now keys on the
  visible on-screen rect (viewport_at_screen_point), picks the smallest
  (topmost) on overlap, and accepts the border-wire pick only when the
  click also lands inside that viewport's visible rect — so a far
  viewport's edge passing near the cursor can't steal the activation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 23:14:46 +03:00
Hakan SevenandClaude Opus 4.8 7f36cf512f fix(viewcube): snap a paper viewport square to the UCS, not WCS
Clicking the ViewCube inside a floating viewport set only view_direction,
so the rebuilt camera fell back to WCS-up and the view opened tilted
relative to the UCS-oriented cube the user clicked. Build the full
UCS-aligned orientation (snap_to_direction with the ViewCube UCS) and
decode it to the viewport's stored (view_direction, twist_angle).

twist can't come from -Camera::roll(): camera_from_view rebuilds rotation
with its own yaw convention (atan2(x,-y)) and roll = -twist, which differs
from the camera's internal sync_yaw_pitch. Instead reproduce the decoder's
zero-twist basis and store twist = -signed_angle(up0 -> desired_up).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 22:17:44 +03:00
Hakan SevenandClaude Opus 4.8 3a4d842a7b fix(viewcube): hover/click hit-test must match the drawn cube under UCS
The hover hit-tests used the bare camera rotation (no UCS) and the main
camera even inside a floating viewport, so the highlighted face diverged
from the cursor whenever a UCS was active or in a paper viewport — while
the snap path was correct. Route hover, click-consume and snap through
active_view_rotation_mat(), and compose the ViewCube UCS in its viewport
branch so all three match ViewportData's drawn cam_rotation exactly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 21:38:51 +03:00
Hakan SevenandClaude Opus 4.8 2507a8fe55 refactor: unify eye() to a single f64 accessor (drop eye_f64)
The whole pipeline is relative-to-eye now, so the f32/f64 split is
vestigial. Merge eye_f64 into eye() returning DVec3; direction-only
callers take .as_vec3().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 21:38:51 +03: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 <[email protected]>
2026-06-24 20:37:52 +03:00
Hakan SevenandClaude Opus 4.8 62c7e20f42 refactor: drop the discarded offset from world_offset_from_centers
The function computed a median-centroid offset that is now always
discarded (geometry is absolute + double-single). Rename it to
cluster_extent_from_centers, return only the cluster span used for camera
fit / cull, and drop the dead centroid computation. Rename the companion
WORLD_OFFSET_SANE_EXTENT const to CLUSTER_SANE_EXTENT.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 20:22:54 +03:00
Hakan SevenandClaude Opus 4.8 04c93df2c4 refactor: remove the legacy full view_proj (CPU cull now relative-to-eye)
The GPU draws exclusively via the rotation-only `view_rot` + eye double-
single; the full view-projection survived only to project AABBs in the
CPU frustum-cull / scissor / LOD passes. Migrate those to the same
relative-to-eye math (project `point - eye` through `view_rot`) — pixel-
equivalent but precise at UTM scale — then drop the dead uniform:

- pipeline cull/scissor/LOD helpers take (view_rot, eye) instead of the
  full view_proj; render.rs reconstructs eye from the uniform's
  eye_high/eye_low and passes the cropped view_rot.
- Remove the `view_proj` field from the shared Uniforms struct and all six
  shader structs that only declared it for layout (none read it), and
  delete Camera::view_proj(). The struct shrinks by one mat4 on both
  sides, keeping the Rust / WGSL layouts in lock-step.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 20:14:41 +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 <[email protected]>
2026-06-24 20:04:41 +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 5b1a5a71a0 refactor: finish OST f64 integration; drop vestigial _f64 suffixes
- Object-snap-tracking points now project through the relative-to-eye path
  (view_proj_rte + eye) instead of the full view-projection, so they stay
  precise at UTM-scale coordinates — the last consumer of the legacy full
  view_proj outside the CPU frustum-cull / scissor path.
- Rename Camera::project_f64 → project and unproject_on_plane_f64 →
  unproject_on_plane: the f32 twins are gone, so the disambiguating suffix
  is vestigial. eye_f64 keeps its name — the f32 eye() still builds the
  view matrices.
- Remove stale #[allow(dead_code)] from eye_f64 / project / unproject_on_plane
  (all live now).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 18:41:25 +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 02d40bd2ec fix(viewport): clip pick/snap to the viewport rect (no off-screen hits)
The GPU scissors floating-viewport content to the viewport rectangle, but
the CPU hit-test/snap wire set runs past it (the frustum cull keeps a
margin and lines extend beyond the rect), so box selection, lasso, click
and object snap could reach geometry clipped out of the viewport.

Reject candidates whose projected screen point falls outside `bounds` (the
pane rectangle), mirroring the scissor. `bounds` is the full canvas in
model space, so every change is a no-op there.

- snap: reject out-of-bounds points in try_pt, the grid block and tangent.
- click_hit / click_hits_all: no hit when the cursor is off-pane.
- box_hit: clamp the selection box to the pane rectangle.
- poly_hit: drop wire points projected off-pane.
- hatch click: no hit when the cursor is off-pane.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 17:45:33 +03:00
Hakan SevenandClaude Opus 4.8 ed711671c6 fix(viewport): keep MSPACE camera target in f64 to stop UTM jitter
camera_from_view computed the camera target in f32 (view_target cast to
f32 before folding view_center), quantizing it to ~0.5 m at UTM scale.
Panning/zooming inside a floating viewport nudges view_target by sub-metre
f64 steps, so the camera snapped along the f32 grid and the drawn content
jittered — while the model view (f64 target) stayed smooth.

Compute the target in f64 (axis directions stay f32 — orientation only),
matching the model camera's precision.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 17:35:26 +03:00
Hakan SevenandClaude Opus 4.8 295d3b1b5d feat(ucs): per-viewport UCS applied inside floating viewports
Extend UCS support to in-viewport (MSPACE) editing through the existing
single-converter structure: one active_ucs drives every UCS-aware system,
now in the Model tab AND inside a floating viewport.

- Single predicate DocumentTab::editing_model_space() (Model tab or active
  viewport) replaces the model-only gate in ucs_wire_affine,
  ucs_rotation_angle, ucs_grid_basis, viewcube_ucs_mat, the coordinate
  readout and the cursor-plane pick.
- Per-viewport UCS: refresh_active_ucs() sets active_ucs to the pane's UCS
  on every transition (the entered viewport's own UCS from its
  ucs_origin/x_axis/y_axis/ucs_per_viewport fields, the header model UCS in
  the Model tab, none in plain paper); persist_active_ucs() writes changes
  back to the viewport entity or the header so they round-trip. Wired into
  EnterViewport/ExitViewport, layout switch, load and the UCS command.
- Visuals: grid_views now returns each pane's handle so every content
  viewport's grid aligns to its own UCS; the UCS icon draws inside the
  active viewport projected through the viewport camera.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 17:23:29 +03:00
Hakan SevenandClaude Opus 4.8 a66d873e51 feat(viewport): unify in-viewport editing on the model-space path
Replace the leaky paper-projection approach to MSPACE editing with one
coordinate adapter that routes all interaction through the viewport's own
camera — the exact camera+rect the GPU draws the content with.

- Add Scene::viewport_edit_frame(canvas) -> (camera_for_viewport,
  viewport_screen_rect): the unified adapter. Inside a floating viewport
  the viewport is the active pane, so snap/pick/grip/preview/DYN run in
  model space and land on the pixels the GPU draws. Tracks pan/zoom/twist
  exactly (twist handled by camera_from_view); crop is pixel-preserving so
  full-rect projection + cursor offset matches.
- Route snap-move, click-pick, grip-drag, grip hit-test, grip render,
  hover (HoverDwellTick), selection (single/box/poly/cycle) and command
  entity-pick through it via pick_view / cursor_model_point helpers and an
  edit_cam-aware "active pane = viewport rect" override, so the existing
  tile offset plumbing just works. Fixes wrong-object hover/selection and
  stale snap after pan/zoom.
- DYN: project anchors with the viewport camera and shift to canvas space
  so the guide/labels sit correctly (also fixes a latent tiled-model bug).
- Separate grid snap from object snap entirely: new Snapper.grid_snap_on
  flag, Grid removed from the osnap mode set, evaluated independently of
  the osnap master. Object snap never catches grid; grid corners are
  picked only when grid snap is on, at lowest priority. Default off.
- grip.rs: project_rte + grips_to_screen_rte / find_hit_grip_rte taking
  (view_rot, eye) so grips project through the composed view.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 17:06:59 +03:00
Hakan SevenandClaude Opus 4.8 4aa8e6681f wip(viewport): render UTM viewport content; _f64 cleanup; paper hit-test
Checkpoint on the way to unified in-viewport editing.

- Drop the _f64 transition suffixes: pick_on_plane / pick_on_target_plane /
  paper_to_model are single f64 functions (wrappers removed).
- Paper-space hit-test/snap projects viewport content with points_low, and
  projects its aabb / key_vertices / snap_pts into paper coords so click
  picking and snap candidates land on the visible sheet geometry.
- camera_for_viewport + paper_to_model + EnterViewport agree on an absolute
  drawing-centre view (normalize_active_viewport_view) so UTM viewports
  render instead of aiming at the origin.
- model_to_paper inverse; dynamic-input anchors map model→paper before the
  paper camera projects them, so the guide no longer tessellates a 512 MB
  buffer (clicking the first in-viewport LINE point) — crash fixed.

Known-incomplete (next: unify on the model-space path via one coordinate
adapter): in-viewport snaps stale after pan/zoom (paper_projected_cache not
keyed on the viewport view), grips don't show, command preview misplaced.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 14:53:38 +03:00
Hakan SevenandClaude Opus 4.8 f9b27e7b62 fix(viewport): aim paper-space viewports at the absolute drawing centre
Floating viewports rendered blank on UTM drawings. camera_for_viewport
centred both the saved-view overlap test and the auto-fit target on the
ORIGIN — correct only while world_offset re-centred the model there. With
world_offset removed the model sits at absolute (~5.7e6) coordinates, so a
stale (0,0,0) saved view failed the overlap test AND the auto-fit aimed at
empty origin, leaving the viewport empty.

Both now use the absolute drawing centre from model_space_extents, so saved
views that frame the model are kept and the auto-fit falls back onto the
real content.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 14:01:42 +03:00
Hakan SevenandClaude Opus 4.8 60d4712179 fix(paste): UTM-precise paste/block base via f64 centroid
entities_centroid summed the f32 points (double-single HIGH half) alone, so
at UTM scale (~5.7e6) it both quantized each term ~0.5 m and lost low bits
across the running total — drifting the paste base and PASTEBLOCK's block
base metres off, which placed the wrapped block at the wrong spot/scale.

It now reconstructs each vertex's absolute f64 from points + points_low and
accumulates in f64. clipboard_centroid is DVec3; the PASTECLIP commit delta
is fully f64; define_block_from_owned_entities takes an f64 base so the
block-local fold (Translate(-base)) stays precise. The interactive ghost
anchors (PasteCommand, InsertBlockCommand) still take the f32 cast — display
only; the committed geometry is f64.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 10:42:58 +03:00
Hakan SevenandClaude Opus 4.8 fe5ff376e5 fix(pick): UTM-precise click / box / lasso selection
Selection projected wire vertices from points (the double-single HIGH half)
alone, ignoring points_low, so at UTM scale every selection test ran against
coordinates ~0.5 m off — box/crossing/window edges and click hit-tests missed
or grabbed the wrong entities, worsening with zoom.

hit_test now reconstructs each vertex's absolute f64 from points + points_low
(wp64) before projecting, across click_hit, click_hits_all, box_hit and
poly_hit (lasso). Hatch hit-tests use the f64 world_origin instead of an
f32-cast, and mesh hit-tests (click/box/lasso face picking) reconstruct from
verts + verts_low. world_to_screen now takes the f64 point directly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 10:33:42 +03:00
Hakan SevenandClaude Opus 4.8 e5c8eddedb fix(camera): centre ortho depth range so deep zoom doesn't clip the drawing
Zooming in close made the drawing vanish. The orthographic near/far were
near = distance*0.001, far = distance*1000, which parks the target plane at
ndc-z ~= 0.001 — flush against the near plane. The shared draw-order depth
bias shifts clip-z by up to DRAW_ORDER_BIAS (0.001), so a front-biased
entity landed exactly at z = 0 and dropped out as soon as f32 rounding (which
worsens at high zoom) tipped it past the plane.

ortho_depth_range() now returns a symmetric near/far (distance ± r) that
centres the target plane at ndc-z ~= 0.5, giving the bias half the depth
buffer of headroom on each side. Perspective is unchanged. Applied to both
view_proj and view_proj_rte.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 10:22:44 +03:00