TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
WireModel/LocalWire/TruckEntity snap buffers and the producers/consumers
now carry snap candidates as DVec3 (offset_snap_pts, dimension/multileader
producers, block-cache transform, pick/xclip, snap module). Still
offset-relative, so behaviour is unchanged — this just lets the snap
transport survive the upcoming switch to absolute coordinates without an
extra f32 quantization. The snap module still resolves in f32 (as_vec3 at
entry), matching today's precision.
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]>
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]>
Raster-image quad corners derive from a large f64 insertion point plus
small u/v spans; the single-f32 cast quantized them at UTM scale. The
ImageModel now keeps a corners_low residual, ImageVertex carries pos_low
(location 2), and image.wgsl reconstructs the eye-relative position from
the high/low pair.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The hatch anchor (world_origin / pattern origin) was cast to a single
f32, quantizing UTM-scale fills to ~0.5 m and reintroducing jitter the
wire path no longer had. Both the batched and per-hatch pipelines now
carry the anchor as a high/low f32 pair (HatchInstance.world_origin_low,
HatchUniformData.origin_low) and reconstruct the eye-relative position in
the shader as (origin_high - eye_high) + (local + origin_low - eye_low).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Vertices now carry the low residual alongside the high half all the way
to the GPU: tessellation splits the offset-relative f64 coordinate into
(high, low) f32 (TruckTessResult::Lines/Point, the Text path, Lines /
SegmentedLines, and the block-cache instance transform), WireModel and
the block batch carry a paired points_low buffer, WireInstance gains
pos_a_low/pos_b_low (locations 10/11), and wire.wgsl reconstructs
rel = (pos_high - eye_high) + (pos_low - eye_low).
This is the piece the camera-only RTE was missing: at UTM-scale
coordinates the single-f32 vertex was quantized to ~0.5 m, so it snapped
each time the eye crossed an f32 boundary on pan. With the low half the
position is exact and the cross-drawing-paste jitter is gone.
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]>
Switch the remaining world-space shaders (mesh, image, face3d, hatch,
hatch_batched) to the same relative-to-eye transform the wire pipeline
uses: subtract the eye (high+low f32) per vertex and multiply by the
rotation-only view-projection. Each shader's Uniforms struct is widened
to the full shared layout so the appended RTE fields land at the correct
offsets. Math is unchanged (proj·R·(pos−eye)); large coordinates no
longer jitter on camera movement.
Co-Authored-By: Claude Opus 4.8 <[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]>
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]>
Single-line TEXT and ATTRIB/ATTDEF cast the insertion/alignment point to
f32 before computing the stroke origin. At large coordinates (UTM ~4.5M)
the f32 ulp is ~0.5, snapping text baselines onto a coarse grid so
adjacent rows collapse onto each other and overlap. Keep the anchor in
f64 throughout.
Horizontal alignment now uses the pen advance box [0, advance] instead of
the inked min_x/max_x, so leading/trailing spaces keep their width and
space-aligned columns no longer shift. text_local_bounds returns
TextLocalBounds { ink_min, ink_max, advance }.
TABLE laid out its whole grid in an f32 world frame; switch to a local
f32 frame plus an f64 base added at the widening step, fixing cell-text
baseline snap and the i32 overflow in the border-dedup keys.
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]>
Drawings authored under a rotated UCS save a view twist so they open
square. camera_from_vport ignored VPort.view_twist (roll hardcoded 0),
so such files rendered in raw world orientation and looked tilted.
Feed -view_twist in as the camera roll: the twist rotates world-X onto
screen-right, so the direction that becomes horizontal is its negative.
The roll rides the camera quaternion, surviving render and orbit.
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]>
A toolchain/libc shift broke the previously-locked transitive set on wasm32
(memchr 1.0.2's libc::memchr binding stopped resolving), failing the web/Pages
deploy. Re-resolve within the existing version requirements — no Cargo.toml
change needed. Native and wasm32 both build.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Picks up the fix that keeps a block's owned-handle list in sync with the
entities actually written, so AutoCAD no longer drops model-space geometry from
OCS-saved files.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
delete_layout removed the Layout object and its entities but left the paper
space block record, the ACAD_LAYOUT dictionary entry and any standalone
PlotSettings behind. AutoCAD pairs every paper-space block record with a layout,
so the deleted tab could reappear on reopen. Also drop the block record, the
dictionary entry and the page setup so the layout is gone for good.
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]>
The overall (sheet) viewport was created with its center as (x, 0, z) —
carrying the paper-height midpoint in z with y = 0. AutoCAD/TrueView read a
paper-space viewport center as (x, y), so the sheet view ended up centered at
y = 0, shifting the whole layout half a page down; activating the layout in
AutoCAD could fault on the off-plane viewport. Store the center as (x, y, 0),
matching the convention MVIEW already uses for floating viewports. Verified: a
newly created paper-space viewport no longer faults AutoCAD on layout switch.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Mark commands that are wired and dispatching as done: 3DARRAY, the solid
booleans (UNION/SUBTRACT/INTERSECT), the CONE/WEDGE/TORUS primitives,
LAYISO/LAYUNISO, DIST/ID/LIST inquiry, named-view VIEW, and QUIT. Refresh the
summary totals (done 141 -> 158, missing 91 -> 74).
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]>
ARRAY3D/3DARRAY, ATTEDIT/ATE, DDEDIT/ED and WIPEOUT/WO dispatched on literal
matches but had no CommandRegistration entry, so none surfaced in command-line
autocomplete. Add the inventory submit in each command's module, matching the
convention used by the other interactive commands. Audit confirms no remaining
active_cmd dispatch lacks a registration.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
TORIENT dispatched on a literal match but had no CommandRegistration entry,
so it never surfaced in command-line autocomplete. Add the inventory submit
like the other interactive commands.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold the Single/Multiple value parsing in TexteditCommand's Mode step into
the shared parse_texteditmode() helper, and report unrecognized input to
the user — the Mode step now stays active and re-prompts with
"Requires Single or Multiple." instead of silently ignoring the value.
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]>