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