Replace the custom model_tiles tiling with iced 0.14 pane_grid as the layout
source of truth, while paper-space floating viewports are left untouched (they
overlap/free-place, which pane_grid cannot model).
Each Model pane is its own shader widget rendering its camera into the pane
bounds; the renderer Primitive carries a base_slot so several shader widgets
share the type-keyed MultiPipeline storage on disjoint slots. A second pane_grid
of transparent mouse_areas (plus on_resize) layers above the crosshair overlay
to receive input — the overlay's Hidden cursor interaction otherwise levitates
the cursor and starves any layer beneath it. The active pane follows the cursor
(hover-activate), and grid / crosshair / UCS icon render per pane while the
ViewCube and render-mode picker stay on the active pane only.
Split / close / VPORTS presets and the VPort table round-trip now drive
pane_grid (guillotine reconstruction on load). Adds drag-to-swap via a controls
bar handle with a ghost-card preview, visible pane dividers from the pane_grid
split regions, a default model background of RGB(33,40,48), and removes all the
superseded custom-tile code.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The UCS icon now anchors to the projected UCS origin when it is on-screen
(falling back to the bottom-left corner), and becomes a selectable object:
hover highlights it, a click shows draggable grips, and dragging the origin
grip relocates the UCS while the X/Y tip grips rotate it. Drags snap to
geometry/grid through the existing snapper, and the new UCS is persisted on
release. The icon stays selectable while parked in the corner.
Also make the ViewCube compass (N/E/S/W) world-fixed: the cube body still
reorients with the active UCS, but the cardinals project through a camera-only
rotation and snap in world frame, so the directions no longer spin with the UCS.
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]>
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]>
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]>
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]>
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]>
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]>
- 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]>
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]>
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]>
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]>
Add camera-relative-f64 world↔screen on Camera (project_f64,
unproject_on_plane_f64): the point is made eye-relative in f64 before the
rotation-only projection, so CPU picking / overlay stay exact at large
absolute coordinates — the CPU twin of the GPU's relative-to-eye path.
Additive and inert for now; the upcoming phases route overlay, pick/snap
and coordinate readout through these so world_offset can be dropped.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
zoom_about_point picked the world point under the cursor twice via
view_proj.inverse() (the full f32 matrix) and shifted target by their
difference. At UTM coordinates that inverse carries ~0.5 m of f32 error
that didn't cancel between the two picks, so target drifted every zoom
step and the whole scene jumped as a block.
Compute the pivot as the cursor's offset *relative to target* in the
camera frame (small numbers, rotated to world) instead — it never touches
the large absolute target, so the before/after difference is exact and
the scene stays put while zooming.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
No GPU pipeline reads camera_pos any more — lighting and all transforms
moved to the relative-to-eye path (view_rot + eye_high/eye_low). Remove
the field from the shared Uniforms and every shader struct, plus the now
unused Camera::position_vec4. view_proj stays: the CPU frustum-cull /
scissor / LOD passes still project offset-relative AABBs with it (it is
no longer part of any GPU draw).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The wire shader now subtracts the eye (high+low f32) per endpoint and
transforms by the rotation-only view-projection instead of multiplying
absolute positions by a view-projection that bakes in a large eye
translation. The math is identical (proj·R·(pos−eye)) but the large eye
no longer enters the f32 matrix, so lines stop jittering on
pan/zoom/rotate at large (UTM) coordinates and far from world_offset.
Geometry low bits are intentionally dropped: (pos − eye_high) is exact in
f32 for same-magnitude operands (Sterbenz) and eye_low corrects the eye,
so the only residual is a static sub-f32 position error, not motion
jitter — no per-vertex low buffer needed. view_rot is cropped in lock-step
with view_proj for tiled panes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Append a rotation-only view-projection plus the eye split into high/low
f32 (double-single) to the shared Uniforms buffer, and the matching
Camera helpers (view_proj_rte, eye_high_low). Fields are added at the end
so existing field offsets — and every shader still reading only the
legacy view_proj — are unaffected. No shader consumes them yet; the wire
pipeline migrates first in the next step.
Co-Authored-By: Claude Opus 4.8 <[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]>
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 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]>
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]>
The OCS arbitrary-axis algorithm crossed the normal with Wx=(1,0,0) in the
else branch, which collapses to zero for an X-aligned normal (e.g.
N=(-1,0,0)): the OCS axes became zero and every point mapped to the
origin. The resulting coincident points fed a NaN truck arc whose
parameter_division never converged, hanging the load on "Parsing
entities". Cross with Wz=(0,0,1) per the DXF spec. (#142)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Solids (meshes) now tint on rollover and selection: hover washes orange,
selection washes blue, with selection winning when both apply. Meshes
re-upload their tint when the highlight set changes, and the rollover pick
now also hits shaded solid bodies and hatches (mirroring the click-select
order: wire → hatch → block hatch → solid).
Wires get the same scheme: the selection xray overlay recolours selected
entities blue and the hovered entity orange (previously both blue). Drops
the now-unused highlight_handles union helper and its cache.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move the 30 flat modules under src/scene/ into six folders by role:
convert (entity→geometry), text (fonts/shaping), model (render models),
pick (hit-test/selection/grips/xclip), view (camera/transform/render),
cache (block + property caches). pipeline/ is unchanged.
Full path migration — all `scene::X` references become
`scene::<group>::X`, no compatibility re-exports. render.rs items that
were `pub(super)` (parent was scene) become `pub(in crate::scene)` to
keep their original scene-wide visibility.
No behavior change; build and tests are green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>