Commit Graph
481 Commits
Author SHA1 Message Date
Sebastian dcfaed0182 Initial commit 2026-06-25 00:01:36 +03:00
Hakan SevenandClaude Opus 4.8 125b89d8da feat(save-dialog): extend drive navigation to macOS and Linux
Issue #170's fix added a "This PC" volumes root above the drive
letters on Windows. Generalize it so the Save As dialog can switch
volumes on every platform:

- parent_folder() now climbs the filesystem root into the volumes
  list on all platforms (`/` -> Computer), not just Windows.
- available_drives() is platform-specific: Windows probes A:-Z:,
  macOS lists `/` + /Volumes/*, Linux lists `/` + the removable
  mounts under /run/media/$USER, /media/$USER and /mnt. Other Unix
  and wasm fall back to `/`.
- drives_root_label() shows "This PC" on Windows, "Computer" elsewhere.
- read_dir_entries() lists the volumes at the drives root on every
  platform.

Also gitignore the dist/ web build output so the wasm binary is never
committed by accident.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 23:45:46 +03:00
Kevin GriffinandClaude Opus 4.8 ef229f5d85 fix(save-dialog): allow navigating to other drives (#170)
The custom Save As dialog navigated up the tree solely via
`Path::parent()`, which returns `None` at a drive root (`C:\`).
Navigation therefore dead-ended on the starting volume, so other
drives and USB sticks were unreachable.

Add a synthetic Windows "This PC" drives root above the drive
letters. `parent_folder()` now climbs `…\sub → C:\ → This PC`, and
`read_dir_entries()` lists the available volumes (probed A:–Z: by
stat, no new dependency) when at that root. The dialog's up-button
and path bar route through the same helper and show "This PC".

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 23:45:30 +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 cc43969ce0 refactor: remove remaining world_offset vestiges in app/
Clean up the last zero-valued world_offset locals outside the scene
tessellation core: the UCS wire-affine / grid-basis origin shift
(document.rs), the coordinate readout (view.rs), grip absolute-apply
(update.rs), the dead properties-edit re-offset guard, the MTEXT preview
glyph-box shift (mtext_editor.rs), and the cross-drawing paste correction
(commands.rs / cmd_result.rs) — including the now-dead
`clipboard_world_offset` field. Coordinates are absolute throughout.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 19:01:25 +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 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 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 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 dd1f2e49bb fix(render): UTM-precise UCS icon + fallback outlines (hatch etc.)
Two UTM-scale rendering defects.

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 10:09:19 +03:00
Hakan SevenandClaude Opus 4.8 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 <[email protected]>
2026-06-24 09:53:28 +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 bca8958025 fix(snap): f64-precise computed snaps + UTM-tolerant cull + f64 previews
Computed snaps (midpoint/nearest/perpendicular/intersection/extension/
apparent/grid) reconstructed the snap point from f32 `wire.points`
(absolute high half), so at UTM-scale they landed ~0.5 m off the geometry
while endpoint/center (sourced from f64 key_vertices/snap_pts) stayed
correct. Now they rebuild each vertex from points + points_low (the
double-single low residual) and run the geometry in f64:
- snap() cursor + geometry helpers (nearest_on_segment, perp_foot,
  seg_intersect_xy, extension_snap) are DVec3; wp_f64() reconstructs each
  vertex; midpoint uses f64 key_vertices directly (was downcast to f32).
- snap callers pass the precise DVec3 cursor.

Snap capture at deep zoom: the wire-AABB cull compared a sub-millimetre
snap radius against f32 bounds quantized by ~1 ulp (~0.7 m at 5.7e6),
wrongly excluding wires under the cursor. Pad the cull by the bound's own
f32 quantization so it never rejects an in-range wire.

Rubber-band previews (line/circle/arc/ellipse/rect/polygon) built f32
absolute points with an empty points_low, so the wire pass (relative-to-
eye) drew them ~0.5 m off the cursor at UTM. Add WireModel::solid_f64,
which splits f64 points into the double-single high/low pair; previews now
track the cursor exactly.

Known: snapping at extreme zoom on large drawings spikes CPU (per-move
snap cost) — deferred.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 04:26:05 +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 52882749eb feat(overlay): relative-to-eye CPU projection for grips and grid (P5)
Route the on-screen chrome that projects absolute world coordinates
through the f64 relative-to-eye helpers so it stays glued/jitter-free at
UTM scale after the world_offset removal: grips (grips_to_screen) and
grip hit-testing (find_hit_grip) now project from the f64 GripDef.world
via Camera::project_f64; the model grid and its UCS axes carry the camera
eye (DVec3) + rotation-only matrix and a f64 origin, projecting each line
relative to the eye. Snap-marker / OST-point / compute_grid_step still use
the f32 path (single points / spacing — negligible) and can follow.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 22:57:31 +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 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
Hakan SevenandClaude Opus 4.8 e39c4b4424 fix(clipboard): refresh blocks/meshes on PASTEORIG and PASTEBLOCK
PASTECLIP recreated missing block definitions, tessellated pasted solids,
and refreshed the layer panel after a paste; PASTEORIG and PASTEBLOCK did
not. A block pasted through either of those rendered empty because its
definition was never recreated in the target drawing.

Both paths now call merge_clipboard_blocks (so referenced/nested block
definitions exist) and populate_meshes_from_document (so pasted ACIS
solids tessellate); PASTEORIG also refreshes the layer panel.

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

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 18:18:35 +03:00
Hakan SevenandClaude Opus 4.8 0c2a97e17e feat(ucs): build axis-aligned shapes in the active UCS
Commands that construct geometry with implicit axis directions ignored
the UCS, so a rectangle drawn under a rotated UCS came out world-aligned.
Give commands the UCS and build square to it:

- CadCommand::set_ucs(Mat4) (UCS→wire affine, default no-op); the host
  pushes the current UCS via App::push_ucs_to_cmd before each point /
  preview / text dispatch (DocumentTab::ucs_wire_affine, identity outside
  model space). New axis-aligned commands just override set_ucs.
- RECT (two-corner) and RECT_CEN (center) place their derived corners on
  the UCS axes (ucs_box_corners / ucs_box_around_center).
- ARRAYRECT row/column offsets run along the UCS axes.

RECT_ROT, POLYGON, and ELLIPSE already take their orientation from picked
points, so they need no change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 17:30:42 +03:00
Hakan SevenandClaude Opus 4.8 d63602161f fix(ucs): resolve typed dynamic-input values in the active UCS
The dynamic-input boxes displayed deltas/angle in the UCS, but
dyn_resolve_point still resolved the typed (and live) values on the world
axes, so committing a typed coordinate ignored the UCS. It now rotates
the cursor delta into the UCS, interprets the typed cartesian/polar
values in that frame, and maps the relative result back to world via
UcsXform::vec_to_wcs — display and commit now agree. Absolute first-point
fields (no base) stay world; identity UCS is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 11:33:55 +03:00
Hakan SevenandClaude Opus 4.8 481adba66b fix(ucs): grid axis lines and grid snap follow the active UCS
Two leftovers from the UCS grid work:
- The coloured X/Y/Z axis lines (draw_axes) were drawn at the world
  origin along world axes; they now run through the UCS origin along the
  UCS axes, matching the grid lines.
- Grid snap rounded the cursor on the world XYZ lattice; it now rounds in
  the UCS grid frame (to-UCS, round, back-to-world) so it lands on the
  grid the user sees. The UCS origin (wire space) + rotation reach the
  snapper via DocumentTab::ucs_grid_basis; identity outside model space.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 11:27:34 +03:00
Hakan SevenandClaude Opus 4.8 2509396ba0 fix(ucs): object snap tracking rays follow the active UCS
OTRACK alignment rays (0°/90° and polar steps, plus the polar pencil from
the last point) were built along world X/Y. They now rotate with the
active UCS via a Mat4 passed into otrack_snap (scene.viewcube_ucs_mat() —
the UCS in model space, identity in paper), so tracking lines and the
intersection lock align to the user's coordinate system like ortho,
polar, and the grid. sync_ucs_to_scene now reuses UcsXform::rotation_mat.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 11:17:55 +03:00
Hakan SevenandClaude Opus 4.8 98142ee4af fix(ucs): snap ViewCube face views square to the active UCS
Clicking a ViewCube face rotated the gaze into the UCS frame but derived
the up vector from world X/Y/Z, so the snapped view landed world-aligned
(most visibly Top, where the UCS shares world Z and only the in-plane
roll differs). snap_to_direction now takes the UCS basis and picks its up
from the UCS axes, so a face snap lands square to the user's coordinate
system. Identity UCS reproduces the world-aligned snap.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 11:12:43 +03:00
Hakan SevenandClaude Opus 4.8 b676874a6f feat(ucs): dynamic-input deltas and angle in the active UCS
The on-canvas dynamic-input boxes measured relative X/Y deltas and the
polar angle in world axes. They now read in the active UCS plane (via
UcsXform::vec_to_ucs on the offset-invariant delta), matching the UCS
ortho/polar constraints and coordinate readout. Identity UCS reproduces
the world-frame values; absolute first-point fields are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 10:13:47 +03:00
Hakan SevenandClaude Opus 4.8 d78ed60582 feat(ucs): orient the ViewCube to the active UCS
The ViewCube showed world Front/Top/Right. It now composes the camera
rotation with the active UCS (scene.viewcube_ucs, synced from the tab's
active_ucs), so its faces follow the user's coordinate system in model
space. Render uniform, hover/click hit-test, and the click→snap direction
all go through scene.viewcube_ucs_mat() so they stay in lock-step;
identity outside model space leaves paper-viewport navigation unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 10:10:47 +03:00
Hakan SevenandClaude Opus 4.8 88bf2e57dc feat(ucs): align the model grid to the active UCS
The grid ruled along world X/Y/Z through the world origin. It now rules
along the active UCS axes through the UCS origin (passed in render/wire
space), so it lines up with the user's coordinate system. Paper space
stays plain WCS; identity UCS reproduces the old world-aligned grid.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 10:03:35 +03:00
Hakan SevenandClaude Opus 4.8 746a2c97be feat(ucs): ortho/polar constrain in the active UCS plane
Ortho and polar drawing constraints snapped to world X/Y; under an active
UCS they now snap to the UCS axes, routed through the shared UcsXform
(identity = unchanged world-XY behaviour). Applied at all three input
sites (grip drag, command point, command preview). Typed coordinates
already flow through the converter via ucs_to_wcs / ucs_rotate_vec.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 10:00:53 +03:00
Hakan SevenandClaude Opus 4.8 4aa1e90dcf feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.

- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
  vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
  axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
  free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
  current UCS (header model-space UCS) into active_ucs on open, so the file's
  coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
  instead of always showing world X/Y/Z.

First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 09:57:39 +03:00
Hakan SevenandClaude Opus 4.8 44d2df9361 fix(view): unify model VPORT / paper VIEWPORT view handling
Model-space tiled VPORTs and paper-space floating VIEWPORTs each carried
their own copy of the saved-view→camera math, so a fix to one drifted
from the other (the recurring grid/twist regressions). Route both, plus
the View-table and sheet-viewport paths, through one decoder and fix the
bugs that the duplication hid.

Decoder unification:
- New camera_from_view(direction, target, center, height, twist,
  world_offset): the single saved-view→Camera decoder. camera_from_vport
  (tiled), camera_for_viewport (floating, + auto-fit), and the View-entry
  / sheet-viewport apply paths all delegate to it. Tiled vs floating now
  differ only in field source and the floating auto-fit fallback.

Twist round-trips on layout switch and file save:
- camera_from_view applies roll = -twist; the write path was dropping it,
  so model→paper→model (and reload) reset the view to raw orientation.
- New Camera::roll() recovers the camera roll; sync_camera_to_document,
  write_camera_view_entry, and vport_from_camera now store -roll as the
  view twist on the *Active VPort, the OpenCADStudio_Camera_Model View
  entry, the per-tile VPorts, and the paper sheet viewport.

Rotation-aware frustum cull:
- New view_cull_aabb(): the entity-cull rectangle is built from the
  camera's right/up basis (so a twisted/rotated view's rotated rectangle
  is enclosed) instead of world X/Y axes; returns None for tilted views
  to disable the flat-XY cull rather than wrongly hide geometry. Applied
  to the model-tile and paper-viewport cull sites.

Paper viewport scale:
- The floating-viewport auto-fit overlap test summed view_target +
  view_center raw; under a twist view_center is a DCS offset, so the sum
  landed far from the real WCS centre and wrongly tripped the auto-fit,
  rendering content at the wrong zoom. It now tests the decoded
  (twist-folded, wire-space) target. The CPU projection path derives its
  scale from that same camera, locking hit-test/snap to the GPU render.

Grid enumeration:
- ViewportInstance gains grid_on; the two grid-view collectors collapse
  into one grid_views() derived from the same active_viewports() list the
  renderer uses, so the grid overlay can no longer drift from the views
  on screen.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 09:35:06 +03:00
Hakan SevenandClaude Opus 4.8 10e4c18ba7 chore: silence native and wasm build warnings
Gate target-specific code so neither build warns about the other's items:
native-only helpers (view, window icon, file dialogs, file association, plugin
host/guard/registry, self-update) are cfg'd to non-wasm; the wasm-only font
fallback (script_of / cjk_lang / request) is cfg'd to wasm. Drop an unused mut,
allow the few genuinely-conditional dead items (mesh_click_hit, GPU layout
helpers, parity stubs), and gate the embedded PlotSettings-unrelated dead
constants. Both targets now build clean (only the unavoidable nom/quick-xml
future-incompat dependency notice remains).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 02:03:34 +03:00
Hakan SevenandClaude Opus 4.8 26479b6faa fix(grid): independent, clipped grid per viewport
Grid (and grid-snap) state was shared through the global show_grid flag, so
toggling it in model space also turned it on in paper space, and a floating
viewport's grid painted across the whole paper — and beyond the 3-D view when
zoomed in.

- Store grid/snap per view: a model tile in model space, the active floating
  viewport or the layout's sheet viewport in paper space, round-tripped via the
  viewport status flags.
- adopt_view_display on layout switch and viewport enter/exit so the live
  toggle follows the view you are in.
- Render one grid per view (paper_viewport_grid_views): the sheet over the
  paper, each floating viewport clipped to its screen rectangle.
- Clamp the grid clip to the widget so an overflowing viewport rectangle never
  paints grid outside the 3-D view.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 00:18:01 +03:00
Hakan SevenandClaude Opus 4.8 ba834fb166 fix(layout): faithful paper-space round-trip and page setup (#156)
Builds on the acadrust viewport/PlotSettings round-trip fixes:

- Write the sheet viewport's view position into view_center (DCS) with
  view_target at the origin, matching AutoCAD; the old view_target form shifted
  the layout.
- PAGESETUP now writes paper size, rotation, origin and units onto the Layout's
  embedded PlotSettings (not just a side object), and bumps geometry, so an edit
  shows on the sheet immediately and survives a save.
- Draw the printable-area guide (paper inset by plot margins, rotation-aware) as
  a dashed rectangle, matching AutoCAD's layout view.
- Plot/PDF export falls back to the Layout's embedded PlotSettings via
  effective_plot_settings(), so a loaded file's rotation/origin/scale apply.

Bumps acadrust to pick up the viewport-status and PlotSettings round-trip fixes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 23:47:36 +03:00
Hakan SevenandClaude Opus 4.8 a72df6a805 feat(command): surface one-shot commands in command-line autocomplete
File/view/layer/style/undo-redo and similar one-shot commands dispatch a
single action instead of installing an interactive CadCommand, so they had no
module to register from and never appeared in autocomplete. Add a consolidated
CommandRegistration covering all 132 user-typeable one-shots (NEW, OPEN, SAVE,
SAVEAS, PLOT, PURGE, UNDO, REDO, ZOOM, LAYER, XREF, …). Internal dispatch
tokens the user never types (REFEDIT_BEGIN, REFCLOSE_SAVE, REFCLOSE_DISCARD)
stay excluded.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 12:24:34 +03:00
Hakan SevenandClaude Opus 4.8 8c48233dcf merge: TEXTEDIT command + TEXTEDITMODE sysvar (#130)
Merge PR #130 (KarimJerbi) and resolve review findings:

- Conflict (settings.rs): keep current persisted-settings layout, append
  texteditmode field.
- Conflict (command/mod.rs): keep both new CmdResult variants — main's
  live-entity ops and the PR's text-edit suspend/resume.
- Bug: invalid TEXTEDITMODE value no longer aborts via the Measurement
  arm; it now re-prompts with the error and stays active.
- Dedup: single parse_texteditmode() helper replaces the value parsing
  duplicated across commands.rs, settings.rs and textedit.rs.
- Strip trailing whitespace.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 12:05:54 +03:00
Hakan SevenandClaude Opus 4.8 038052f5f9 feat(block): fade context, free editing and a Save/Discard toolbar in REFEDIT
In-place block edit now reads more like a dedicated editor:

- Fade everything except the edited entities toward the background (wires,
  block instances and 3D solid meshes) so the block stands out while the
  surrounding drawing stays visible for context.
- Don't leave a command active on enter — the user edits the block's geometry
  freely (move, grips, draw, erase) and finishes explicitly.
- Show the right-edge vertical toolbar with clickable Save (REFCLOSE_SAVE) and
  Discard (REFCLOSE_DISCARD) buttons while a session is active, so the edit
  can be ended without typing.

The fade clears on save/discard, restoring full brightness.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 11:46:43 +03:00
Hakan SevenandClaude Opus 4.8 9f447308a6 feat(block): double-click to edit a block in place (#136)
Double-clicking a block reference (its wire or its solid body) now enters
in-place block edit (REFEDIT), so its geometry can be modified and the change
reflects in every instance on REFCLOSE.

Also drop REFEDIT's "non-uniform scale not supported" restriction: the
session now carries the INSERT's full forward/inverse affine (OCS, rotation
and non-uniform / mirrored scale) and applies it via the entity transform,
instead of a uniform scale + rotate + translate sequence. Mirrored blocks
(e.g. x_scale = -1) are editable now.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 07:43:59 +03:00
Hakan SevenandClaude Opus 4.8 cd10aa0363 fix(select): click the front-most solid across top-level and block bodies
Clicking a block whose body is a solid tried top-level solid meshes first
and block-internal ones only as a fallback, so a stray solid behind the
block always won and the block couldn't be picked. Merge both into one
depth-sorted hit-test (solid_click_hit) keyed by the solid's own handle or
the parent INSERT, so the front-most body wins regardless of kind.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 07:24:31 +03:00
Hakan SevenandClaude Opus 4.8 ae9940929c fix(select): box/lasso-select blocks whose body is a solid
Box/lasso only tested top-level solid meshes, so a block whose visible body
is a solid (its mesh lives in the per-INSERT instanced cache, and it has no
wires to catch) was never picked up by a window/crossing/lasso. Add
block_mesh_box_hit / block_mesh_poly_hit that test each INSERT's instanced
block solid meshes and resolve a hit to the parent INSERT, and wire them
into the selection paths.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 07:03:56 +03:00
Hakan SevenandClaude Opus 4.8 d5f1348f1b fix(3d): box/lasso-select 3D solids and refresh them after a transform
Box, window, crossing and lasso selection only tested wires and hatches, so
3D solids (which render as meshes) could never be selected that way. Add
mesh_box_hit / mesh_poly_hit that project the solid mesh and test it against
the box / lasso (window = all vertices inside, crossing = any vertex inside
or the region sitting inside the solid), and wire them into the selection
paths.

Also re-tessellate solids after a move/rotate/scale/mirror/copy: the entity
moved but its cached mesh didn't, so the solid stayed drawn at its old
position. Rebuild the mesh caches when a transformed/copied handle is an
ACIS volume.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 06:45:49 +03:00