Commit Graph
308 Commits
Author SHA1 Message Date
sLuCHa a55e1ac4f8 perf(render): skip hatch pass while navigating (interaction LOD)
The batched hatch pass evaluates its fill procedurally per fragment — a
point-in-polygon test over the whole boundary plus the pattern test, with a
`discard` that disables early-Z. When the view sits inside a hatch its quad
covers the screen, so panning / zooming re-runs that full-screen shader every
frame (~40 ms/frame on a hatch-heavy plan) even though the rest of the scene
is cheap — the wire pass and CPU prepare are negligible by comparison.

Suppress the hatch pass while the view is actively panning / zooming /
orbiting, detected from `camera_generation` plus a short settle timer
(`Scene::navigating_lod`), and fold that state into the render signature so
the moment the view settles it renders hatches once and the scene-render
cache holds that full-quality frame. A brief `is_settling` subscription keeps
requesting frames just past the settle point so the hatched frame renders
even when no input event would otherwise fire. Mirrors AutoCAD's adaptive
degradation. The paper sheet keeps its fills.
2026-07-05 09:48:49 +02:00
Hakan SevenandClaude Opus 4.8 626031de00 perf(render): reuse static wire set for model-space pick/snap
Every pan/rotate on a large drawing jerked once at the start of the
gesture. Instrumentation traced it to `paper_sheet_wires_arc` re-running a
full ~300 ms tessellation of the whole model block on the first hit-test
after each camera move.

`entity_wires_arc` (the pick/snap source in Model layout) delegated to the
camera_generation-keyed, view-culled `paper_sheet_wires_arc`. A camera
move bumps `camera_generation`, so the next hit-test cold-missed that cache
and re-tessellated every visible entity — O(visible), which is why the
jump grew with on-screen object count and shrank when zoomed in. The GPU
render was unaffected (it holds the separate camera-independent
`model_static_wires`), so only that one CPU frame stalled and the
accumulated mouse motion applied at once, reading as a jump.

Point the Model pick path at the same resident static set the render holds
(keyed on geometry_epoch only). Pick wants every entity, not a view-culled
subset, so this is also more correct — off-screen entities stay pickable —
and it never re-tessellates on a camera move.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 03:26:07 +03:00
Hakan SevenandClaude Opus 4.8 62680883cd fix(pick): don't XY-cull the pick wire set in tilted 3D views
view_world_aabb built a world-XY cull rectangle from the camera target and
ortho size without checking the view angle, so in a tilted / 3D view it
produced a top-down box that does not bound the on-screen footprint. That
box culls the pick wire set (while the render holds the full set via the
static hold), so a mesh running partially off-screen fell outside the box
and became unselectable — by edge or by face — even though it still
rendered. Top views were unaffected (the box is correct there), and 3D
solids pick through their mesh AABBs (not this wire set) so they never
showed it.

Return None for tilted views (fwd.z.abs() < 0.999), matching view_cull_aabb,
so no world-XY cull is applied and the pick reaches every on-screen wire.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 22:13:37 +03:00
Hakan SevenandClaude Opus 4.8 1065bc12b2 feat(render): draw ACIS solids as B-rep feature edges, meshes as tri edges
Solid wireframes rendered every triangle edge of the tessellation, so a
curved solid read as a dense mesh instead of its real outline. Now each ACIS
solid's B-rep face-boundary edges are collected from the SAT `edge` records
(straight edges as segments, ellipse/circle edges sampled along their bounded
arc), body-transformed and split into the double-single pair, and stored on
the MeshLodSet. The block-instance and top-level offset paths transform these
edges alongside the mesh vertices.

The batch builder routes a solid with feature edges into a dedicated LineList
vertex buffer and skips its triangle wireframe; a mesh with no B-rep edges
keeps its triangulation edges. Edge colour follows the display mode: the flat
`fs_edge` shader draws the entity colour in the lines-only modes (wireframe,
hidden-line) and a `fs_edge_black` twin forces black for the edge overlay in
filled render modes.

Verified: the Clinic storefront arch bars now yield ~14-16 clean edge
segments (vs the full triangulation) bounded to the solid's true extent.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 17:57:00 +03:00
Hakan SevenandClaude Opus 4.8 98aa051a74 feat(nav): turntable orbit, selection/cursor pivot, Shift+MMB rotate (#229)
Rework 3D view navigation:

- Turntable / lock-horizon orbit: horizontal drag yaws about world +Z,
  vertical drag pitches about the horizontal camera-right axis, so the
  view never banks. Pitch is pole-clamped but the straight-down default
  view can still be tilted out of.
- Orbit centre: the selection's centre when something is selected,
  otherwise the point under the cursor — no longer only the file centre.
  The camera revolves rigidly about the pivot, so it stays put on screen
  and the view doesn't jump.
- Mouse scheme: Zoom = wheel, Pan = MMB, Rotate = Shift+MMB. Right-drag
  no longer orbits (right-click stays context menu / Enter). Floating
  viewports orbit with Shift+MMB too.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 21:58:04 +03:00
Hakan SevenandClaude Opus 4.8 56e1c03ac2 feat(layers): enforce layer lock (select/modify/fade + cursor badge)
Locked-layer objects were fully editable. Now they stay visible and
snappable but are otherwise protected:

- Not selectable: the five interactive pick paths (box, lasso, single
  click, cycling, box-release) plus SELECTALL and QSELECT skip locked
  entities.
- Not modifiable (defense-in-depth even if a handle slips through):
  transform_entities, erase_entities, apply_grip and the Properties
  commit all skip locked entities.
- Faded: entities on a locked layer render dimmed toward the background
  (render_style + wire tessellation); toggling lock re-tessellates.
- Snap still works: locked entities are intentionally kept in the wire
  set (visibility_ok untouched), so object snap keeps finding them.
- Feedback: a small padlock is drawn by the crosshair when hovering a
  locked object, and clicking one prints "Object is on locked layer …".

New Scene::is_layer_locked / locked_layer_name helpers centralise the
check (mirroring layer_hidden).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 13:10:39 +03:00
Hakan SevenandClaude Opus 4.8 a994a148fe perf(render): batch solid meshes, fix per-pick freezes on heavy 3D models
Opening / navigating a heavy 3D DWG (e.g. a BIM model with ~10k ACIS solids)
was effectively frozen. Several independent costs, all addressed here:

- Face3D edge/fill buffers were re-uploaded on every camera move (keyed on
  camera_generation). They are world-space and selection-independent, so gate
  them on (geometry_epoch, fill_mode) instead — a pan no longer re-walks every
  wire.
- Solid meshes were drawn one buffer + one draw call per solid (~10k draws/
  frame), which strangled the GPU front end. Concatenate every solid's LOD0
  into a few large buffers (split only to stay under the 256 MB buffer cap) and
  draw the whole set in a handful of calls. Built once per geometry epoch, so
  selection / hover never re-pack it.
- Selection / hover highlight is now a small tinted overlay of just the picked
  solids, drawn last with an Always-depth pipeline so it shows on top even when
  the solid is occluded — instead of re-tinting and re-uploading all meshes.
- Hover / pick hit-testing scanned every triangle of every solid each move
  (seconds on a heavy model). Add a 3D-AABB screen-rect broad phase
  (MeshLodSet now carries z_aabb) and reuse the renderer's cached expanded mesh
  set, so picking is O(solids) cheap projections plus the few solids actually
  under the cursor.
- Block-internal hatch hit-testing exploded every INSERT on every hover.
  Cache the result per geometry epoch and skip blocks that contain no hatch.
- Curved-surface (cone/sphere/torus) tessellation tolerance is now radius-
  relative (CURVE_REL_TOL) so a cylinder's facet count is size-independent and
  matches the circle/arc wire tessellation, instead of exploding on large radii.

Also adds docs/tessellation.md mapping every EntityType to its tessellation
path (truck B-rep vs truck curve topology vs direct).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:46:29 +03:00
Hakan SevenandClaude Opus 4.8 1d7f5d2618 fix(background): step-by-step prompt + Default preset (#217)
Bare BACKGROUND now opens an interactive colour prompt (via
ValuePromptCommand, same idiom as PDMODE/LTSCALE) instead of just
printing usage, so the command works both step-by-step and as a
one-shot `BACKGROUND <colour>`. Options reordered to
Default/Black/DarkGray/Gray/LightGray/White; DEFAULT restores the app
default, now unified to rgb(33,40,48) across startup, load and reset.
Also drop the unreachable OPTIONS arm in dispatch_display.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 17:55:44 +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 <[email protected]>
2026-06-30 17:24:04 +03:00
Hakan SevenandClaude Opus 4.8 495b13ec4f refactor(viewport): migrate model-space tiling to iced pane_grid
Replace the custom model_tiles tiling with iced 0.14 pane_grid as the layout
source of truth, while paper-space floating viewports are left untouched (they
overlap/free-place, which pane_grid cannot model).

Each Model pane is its own shader widget rendering its camera into the pane
bounds; the renderer Primitive carries a base_slot so several shader widgets
share the type-keyed MultiPipeline storage on disjoint slots. A second pane_grid
of transparent mouse_areas (plus on_resize) layers above the crosshair overlay
to receive input — the overlay's Hidden cursor interaction otherwise levitates
the cursor and starves any layer beneath it. The active pane follows the cursor
(hover-activate), and grid / crosshair / UCS icon render per pane while the
ViewCube and render-mode picker stay on the active pane only.

Split / close / VPORTS presets and the VPort table round-trip now drive
pane_grid (guillotine reconstruction on load). Adds drag-to-swap via a controls
bar handle with a ghost-card preview, visible pane dividers from the pane_grid
split regions, a default model background of RGB(33,40,48), and removes all the
superseded custom-tile code.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 11:50:11 +03:00
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 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 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 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 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 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
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 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 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 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 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 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 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 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 6667042372 fix(xclip): precise clip + carry clip across cross-drawing paste
Clipped blocks were broken at UTM and lost their clip when pasted into
another drawing.

- Clip geometry: world_clip_polygon_f64 keeps the boundary in absolute f64;
  clip_wires now clips in a frame relative to the boundary's first vertex
  (small coords → exact f32 Sutherland–Hodgman) and re-splits the result
  into the double-single high/low pair the relative-to-eye renderer needs,
  so the clipped block draws correctly and no longer poisons ZOOM Extents.
- ZOOM Extents (fit_all): the outlier reject compared absolute UTM
  coordinates against a centre-RELATIVE span (local_extent_max), rejecting
  the whole drawing → no-op. Made the reject distance-from-median-centre.
- Cross-drawing paste: clipboard now snapshots each copied entity's whole
  xdictionary object graph (ClipboardDeps.ext_objects) and recreates it on
  paste with fresh handles + remapped references, re-pointing the pasted
  entity's xdictionary at the new root. Wired into PASTECLIP and PASTEORIG.
  Fixed handle allocation (allocate_handle, not the non-advancing
  next_handle) so the dictionary chain doesn't collapse onto one handle.
- Updated hit_test / plugin_host tests for the eye-relative + DVec3 signatures.

Known gaps: PASTEBLOCK does not yet carry the clip (clip would need to live
inside the new wrapper block definition); hatch-clip path still f32.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 09:33:55 +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 96aaf2a293 feat(snap,pick): eye-relative CPU projection — drop full view_proj at UTM
Route the remaining absolute-coordinate CPU projection through the
relative-to-eye path so it no longer multiplies by a view_proj matrix
carrying a ~1e7 translation (which cancels catastrophically in f32 after
world_offset removal):
- camera pick_on_plane / pick_on_target_plane delegate to
  unproject_on_plane_f64 (rotation-only inverse + f64 eye)
- snapper (world_to_screen), hit_test (click_hit / mesh / hatch), the
  scene mesh/solid hit methods, grid + UCS axes, and polar_constrain_near
  all take view_rot + eye and project relative-to-eye
- compute_grid_step measures the per-unit scale via the rotation-only
  matrix (no eye term needed)

view_proj remains only for conservative CPU frustum-cull / scissor and the
screen-space viewcube / UCS-icon directions. NOTE: SnapResult.world and
the command-point chain are still Vec3 (f32) — a ~0.5 m quantization at
extreme UTM zoom remains; the command layer goes f64 next.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 23:33:21 +03:00
Hakan SevenandClaude Opus 4.7 ccc933e7b0 refactor(scene): remove the world_offset field (P7)
Delete the world_offset field from Scene and DerivedCaches and the
load-time assignment; every reader now uses a zero origin. Geometry is
absolute and the double-single relative-to-eye pipeline keeps it precise,
so the coarse origin is gone. world_offset_from_centers still derives
local_extent_max (camera/cull span) but returns no offset. The remaining
`world_offset: [f64;3]` function parameters are now always zero — dead
plumbing left for a later mechanical sweep; behaviour is unchanged.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 22:42:33 +03:00
Hakan SevenandClaude Opus 4.7 b473f6a8cb feat(render): retire world_offset — geometry is now absolute (P6 flip)
world_offset_from_centers always returns [0,0,0]: geometry reaches the
GPU as absolute world coordinates and the double-single relative-to-eye
pipeline keeps it precise at UTM scale, so the coarse origin is no longer
needed. Camera target, snap candidates (f64) and the coordinate readout
all operate in absolute coordinates now. The `- world_offset` subtractions
remain as no-ops and are removed in the field-deletion step. local_extent_max
(camera/cull span) is still derived from content.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 22:34:23 +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 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 ddf6344dbe feat(render): double-single vertices for the mesh pipeline
3D solid / surface meshes were the last single-f32 path. truck's mesh
tessellation now returns a paired verts_low residual (TruckTessResult::Mesh,
polygon_to_result), MeshModel/MeshLodSet carry it, the model-space offset
and block-instance transforms reconstruct the f64 position and re-split it
after subtracting world_offset, MeshVertex gains position_low (location 3),
and mesh.wgsl reconstructs the eye-relative position. Native SAT / ACIS /
OBJ mesh paths default to a zero residual (no regression — they were never
sub-f32 precise).

With this every world-space pipeline (wire, hatch, image, face3d, mesh)
is full double-single relative-to-eye; large-coordinate jitter is gone.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 18:28:52 +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 091b8e1805 refactor(camera): store orbit target in f64
Foundation for relative-to-eye rendering: the pivot the camera orbits
must stay precise at large offset-relative coordinates so the derived eye
(which the RTE path subtracts per vertex) is exact. `Camera::target` is
now DVec3 and `eye_f64()` exposes the full-precision eye; `view_proj`
still casts to f32 at the matrix, so behaviour is unchanged for now.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 18:28:52 +03:00