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]>
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]>
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]>
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]>
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]>
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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>