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]>
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]>
Relocate three root-level modules into io/, where they belong by concern:
- update_check.rs -> io/update_check.rs (its own doc-comment notes it
mirrors how crate::io wraps blocking calls in async).
- patterns.rs, linetypes.rs -> io/ (standard resource catalogs parsed
from asset files and populated into the document — IO-shaped work).
Fix the moved files' include_str! paths (one level deeper), add the
mod declarations to io/mod.rs, drop them from lib.rs/main.rs, and
re-point all crate:: references to crate::io::. No behavior change.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
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]>
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]>
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]>
`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]>
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]>
Every wire segment is one instance, and each carried the wire's color,
line-weight, dash pattern and draw-depth (~44 B) inline — re-fetched per
segment even though they're constant along the wire. On native, move
those into a per-wire `WireConst` storage buffer (group 1) indexed by a
`wire_id` on the instance. The instance drops from 104 B to 60 B (~42 %)
across every entity's wires, and the shared constants are fetched once
per wire (cached) instead of once per segment — less vertex-attribute
bandwidth and less GPU upload on rebuilds.
WebGL2 has no vertex-stage storage buffers, so the wasm build keeps the
original self-contained fat instance and wire.wgsl; native uses the new
slim instance, wire_indexed.wgsl, and a group-1 bind group built per
batch in WireGpu::from_run / from_batch. Visual output is unchanged
(colors are now full f32 rather than 8-bit, if anything more precise).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
A HATCH was emitting its boundary as a dense outline polyline in the
wire set on top of its fill. On hatch-heavy drawings that outline was
the dominant wire-instance cost — 14.9M of 19.1M segments on the #131
sample — and on integrated GPUs (shared system RAM) the per-segment
instances pushed memory past the OOM threshold on open.
Skip the outline in `tessellate`: hatches render as a fill, are
highlighted by their fill tint when selected (#71), and are still
picked by their fill area via the existing `click_hit_hatch` path, so
nothing else depends on it. Wire segments drop ~78% on the sample.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
`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]>
Preview / interim / grip-drag overlay wires were drawn in the main wire
pass with the depth-tested wire_pipeline (depth_compare=LessEqual), so a
live command preview could be hidden behind occluding geometry — a 3D
solid, or 2D geometry drawn in front of it.
Draw them with the existing wire_xray_pipeline instead (depth_compare=
Always, no depth write), the same always-on-top path selection highlights
already use. Previews are now never occluded, for every command.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Follow-up cleanups on the merged TTF-fill work:
- Drop the unconditional resolve_text_style log line — it fired for every
text style and spammed ~7k stderr lines opening a single real drawing.
- Drop the per-glyph tessellation-error eprintln; a malformed font would
otherwise log once per character. The glyph still falls back silently.
- Tessellate glyph fills with the nonzero winding rule (lyon defaults to
even-odd, which mis-fills TrueType outlines whose contours overlap).
- Make the sysfont prefix fallback deterministic (sorted) and require ≥3
chars so a 1–2 letter request can't grab an arbitrary family.
- Remove the unrelated windows-gnu --exclude-all-symbols linker flag that
slipped into .cargo/config.toml.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A drawing opened with a twisted view made a TOP-face click keep the
twist, so north ended up at the bottom. Add Camera::snap_to_face, which
snaps to a canonical orientation — north (UCS +Y) up for top/bottom,
world (UCS +Z) up for the side elevations — ignoring the current
up-sense. Face clicks use it; edges/corners still preserve their up so
they spin smoothly around the clicked feature.
The Home button routes through the same path, giving a reliable reset
for a twisted view.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Lay each label's glyph quads out in the face's own local plane and
project the corners individually, instead of drawing screen-aligned
billboard text at the projected face centre. The letters now skew with
perspective and sit on the surface as the cube rotates.
Per-face (u, v) basis vectors are chosen so u x v equals the outward
normal, which keeps the text from ever reading mirrored.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Text elements previously rendered as hollow wireframe outlines and sometimes
fell back to the wrong system font due to case-sensitive exact matching. Also,
the viewport background grid drew on top of solid entities.
Case-insensitive and partial-name fallback matching (e.g., mapping "arialn"
to "Arial Narrow") is now added to `sysfont::canonical_family_name`, ensuring
text resolves to the correct installed system font.
TrueType font contours are now triangulated into solid fills (`fill_tris`)
using `lyon_tessellation` in `ttf_glyph.rs`. To prevent the GPU from misclassifying
this 2D text as a 3D mesh (which broke flat shading), text tessellation splits
the output into separate outline and fill `WireModel`s.
The background grid is extracted from `SelectionCanvas` into `GridCanvas` and
moved beneath the 3D viewport to ensure proper layering. Finally, `DepthBiasState`
is enabled on all face/fill pipelines to apply a polygon offset, perfectly
resolving Z-fighting between coplanar wireframe lines and solid faces.
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]>
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]>
Two issues in a file whose dimensions carry baked *D### blocks.
Missing diameter text: the baked MText values are "\U+220520" /
"\U+220510" — the \U+2205 (⌀) escape immediately followed by the literal
digits "20"/"10". The MText parser read up to six hex digits for a \U+
escape, so it greedily consumed "220520" = 0x220520, which is past the
maximum code point, char::from_u32 returned None, and the whole text
dropped. A \U+ escape is exactly four hex digits; read four so the symbol
decodes and the trailing digits stay literal. (Angular "90\U+00B0" only
worked because its escape sits at the end of the string.)
Node artifacts: PDMODE=0 points were drawn as a fixed 0.1-world-unit "+",
which in a small drawing turned the 31 dimension def-points on the
Defpoints layer into visible crosses. Size the dot from world-per-pixel
(~1 px, zoom-adaptive via the tessellation cache key) to match the near-
invisible dot other CAD apps draw.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A dot in an LTYPE pattern is a zero-length element. The pattern resolver
encoded it as a tiny `0.01 * scale` world-length dash, which had two bad
effects: it was sub-pixel at normal zoom so the dash LOD saw the pattern's
smallest feature drop below one pixel and collapsed the whole line to
solid, and at larger LTSCALE it left only invisible sub-pixel dots between
big gaps. Either way dotted and dash-dot linetypes were never visible;
only plain dashed (no zero elements) worked.
Keep dots as literal 0.0 so they no longer drag `min_elem` down, and
render each as a fixed ~1.5 px mark in the wire shader (pixel-snapped via
world_per_pixel, mirroring the hatch shader's dot handling). The shader
tells a real dot from trailing array padding by element count — the index
of the last non-zero element plus one — so no producer needs a sentinel.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Lineweights were rendered at their true physical size (mm × 96/25.4 px),
which is ~2 px for a 0.5 mm line — too timid to tell weights apart, and
every weight ≤ 0.25 mm floored to 1 px (no visible difference). Other DWG
editors display model-space lineweights larger than true size for screen
legibility; apply the same 2× boost so weights are pronounced and
distinguishable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
`clip_hatch_boundary` lifted each loop into the clip polygon's world
space (`+world_origin`) to run the Sutherland–Hodgman clip, but returned
the result in absolute world coordinates. The renderer adds the per-hatch
`world_origin` anchor back in the vertex shader, so clipped hatches with a
large origin (e.g. UTM drawings) were drawn double-shifted and lost f32
precision. Subtract the origin again on the way out so the output stays in
the same precision-preserving anchor space as the input.
Also drop a stale third argument from two xclip tests left over from the
world-offset removal, which had broken `cargo test` compilation.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
With the relative-to-eye double-single render path, the per-defn
`local_offset` precision origin is redundant: block geometry can be stored
absolute and stays precise via the high/low residual. Remove it
end-to-end:
- Drop the Pass-1 centroid computation, the BlockDefn.local_offset field,
and the `defn_lo` parameter threaded through emit_wire /
emit_greeked_text / emit_text_baseline / transform_tangent and the AABB
helper (transform_offset_aabb_xy → transform_aabb_xy), plus the nested
parent/child offset re-basing.
- Store text_obb_local as absolute f64 (was offset-relative f32) so the
greek rectangles stay precise; emit splits to double-single. Give the
greek-baseline points a low residual too.
Geometry is now absolute + double-single throughout the block cache,
matching the top-level path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
The GPU draws exclusively via the rotation-only `view_rot` + eye double-
single; the full view-projection survived only to project AABBs in the
CPU frustum-cull / scissor / LOD passes. Migrate those to the same
relative-to-eye math (project `point - eye` through `view_rot`) — pixel-
equivalent but precise at UTM scale — then drop the dead uniform:
- pipeline cull/scissor/LOD helpers take (view_rot, eye) instead of the
full view_proj; render.rs reconstructs eye from the uniform's
eye_high/eye_low and passes the cropped view_rot.
- Remove the `view_proj` field from the shared Uniforms struct and all six
shader structs that only declared it for layout (none read it), and
delete Camera::view_proj(). The struct shrinks by one mat4 on both
sides, keeping the Rust / WGSL layouts in lock-step.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The world_offset removal stripped two precision mechanisms in the block
cache that were masked while geometry was offset-relative (small coords)
but broke once coordinates became absolute UTM:
- build_defn no longer subtracted the defn's `local_offset` from the
tessellated wire (it was passed as tessellate's world_offset). The defn
was stored absolute while expand still re-added local_offset → block
lines shifted. Restore the subtraction (with a double-single re-split)
locally in tessellate_sub_local.
- Block fill triangles (greek-text rectangles, block solid fills) were
batched with f32-only `fill_tris` and an empty `fill_tris_low`, so at
UTM scale the absolute f32 quantized to ~0.5 m and the greek rects
sheared. Plumb `fill_tris_low` through BatchEntry and emit the
double-single residual from emit_wire and emit_greeked_text.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
- 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]>
In the files where ox/oy/oz came solely from the now-zero offset
destructure (tessellate, block_cache, dimension, leader, text_support),
remove the `let [ox,oy,oz] = [0,0,0]` binding and the `- ox` arithmetic so
the coordinate math reads as the plain absolute value it now is.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
The GPU scissors floating-viewport content to the viewport rectangle, but
the CPU hit-test/snap wire set runs past it (the frustum cull keeps a
margin and lines extend beyond the rect), so box selection, lasso, click
and object snap could reach geometry clipped out of the viewport.
Reject candidates whose projected screen point falls outside `bounds` (the
pane rectangle), mirroring the scissor. `bounds` is the full canvas in
model space, so every change is a no-op there.
- snap: reject out-of-bounds points in try_pt, the grid block and tangent.
- click_hit / click_hits_all: no hit when the cursor is off-pane.
- box_hit: clamp the selection box to the pane rectangle.
- poly_hit: drop wire points projected off-pane.
- hatch click: no hit when the cursor is off-pane.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
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]>
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]>
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]>
Selection projected wire vertices from points (the double-single HIGH half)
alone, ignoring points_low, so at UTM scale every selection test ran against
coordinates ~0.5 m off — box/crossing/window edges and click hit-tests missed
or grabbed the wrong entities, worsening with zoom.
hit_test now reconstructs each vertex's absolute f64 from points + points_low
(wp64) before projecting, across click_hit, click_hits_all, box_hit and
poly_hit (lasso). Hatch hit-tests use the f64 world_origin instead of an
f32-cast, and mesh hit-tests (click/box/lasso face picking) reconstruct from
verts + verts_low. world_to_screen now takes the f64 point directly.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Zooming in close made the drawing vanish. The orthographic near/far were
near = distance*0.001, far = distance*1000, which parks the target plane at
ndc-z ~= 0.001 — flush against the near plane. The shared draw-order depth
bias shifts clip-z by up to DRAW_ORDER_BIAS (0.001), so a front-biased
entity landed exactly at z = 0 and dropped out as soon as f32 rounding (which
worsens at high zoom) tipped it past the plane.
ortho_depth_range() now returns a symmetric near/far (distance ± r) that
centres the target plane at ndc-z ~= 0.5, giving the bias half the depth
buffer of headroom on each side. Perspective is unchanged. Applied to both
view_proj and view_proj_rte.
Co-Authored-By: Claude Opus 4.8 <[email protected]>