Commit Graph
265 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.7 b473f6a8cb feat(render): retire world_offset — geometry is now absolute (P6 flip)
world_offset_from_centers always returns [0,0,0]: geometry reaches the
GPU as absolute world coordinates and the double-single relative-to-eye
pipeline keeps it precise at UTM scale, so the coarse origin is no longer
needed. Camera target, snap candidates (f64) and the coordinate readout
all operate in absolute coordinates now. The `- world_offset` subtractions
remain as no-ops and are removed in the field-deletion step. local_extent_max
(camera/cull span) is still derived from content.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 22:34:23 +03:00
Hakan SevenandClaude Opus 4.7 cca1086696 refactor(snap): widen key_vertices to f64 (world_offset removal P4)
WireModel/LocalWire/BatchEntry key_vertices are now Vec<[f64; 3]>;
tessellation keeps them in f64 (no f32 cast at offset subtraction),
producers (dimension, leader, multileader) build them from the acadrust
f64 source, the block-cache transform keeps f64, and the face3d Face3D
quad path splits them into the double-single (high, low) pair. Consumers
(snap endpoint/midpoint, xclip) read via f32 at the leaf. Still
offset-relative (inert); endpoint/midpoint snap is no longer f32-capped.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-23 22:23:56 +03:00
Hakan SevenandClaude Opus 4.7 a7a1fac09a refactor(snap): widen snap_pts to f64 (world_offset removal P2)
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]>
2026-06-23 18:52:42 +03:00
Hakan SevenandClaude Opus 4.7 ddf6344dbe feat(render): double-single vertices for the mesh pipeline
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]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.7 c0f3d0de5e feat(render): double-single fills for the face3d pipeline
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]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.7 97d6993768 refactor(wire): add WireModel.points_low parallel buffer (empty default)
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]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.8 091b8e1805 refactor(camera): store orbit target in f64
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]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.8 d78ed60582 feat(ucs): orient the ViewCube to the active UCS
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]>
2026-06-22 10:10:47 +03:00
Hakan SevenandClaude Opus 4.8 44d2df9361 fix(view): unify model VPORT / paper VIEWPORT view handling
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]>
2026-06-22 09:35:06 +03:00
Hakan SevenandClaude Opus 4.8 7a1988e851 fix(view): apply saved view twist as camera roll on load
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]>
2026-06-22 08:52:20 +03:00
Hakan SevenandClaude Opus 4.8 10e4c18ba7 chore: silence native and wasm build warnings
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]>
2026-06-22 02:03:34 +03:00
Hakan SevenandClaude Opus 4.8 429f3035b1 fix(layout): fully remove a deleted layout's record and references
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]>
2026-06-22 00:37:58 +03:00
Hakan SevenandClaude Opus 4.8 26479b6faa fix(grid): independent, clipped grid per viewport
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]>
2026-06-22 00:18:01 +03:00
Hakan SevenandClaude Opus 4.8 ba834fb166 fix(layout): faithful paper-space round-trip and page setup (#156)
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]>
2026-06-21 23:47:36 +03:00
Hakan SevenandClaude Opus 4.8 9d5ee75bcc fix(layout): place sheet viewport center in the paper plane (#156)
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]>
2026-06-21 13:20:16 +03:00
Hakan SevenandClaude Opus 4.8 038052f5f9 feat(block): fade context, free editing and a Save/Discard toolbar in REFEDIT
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]>
2026-06-21 11:46:43 +03:00
Hakan SevenandClaude Opus 4.8 65b8e85751 fix(select): highlight a block's solid body on hover/selection
The instanced block solid meshes kept the inner solid's handle as their
name, but the highlight tint matches the mesh name against the hovered /
selected handle — which for a block is the parent INSERT. So hovering or
selecting a block whose body is a solid never tinted it. Tag the instanced
meshes with the parent INSERT handle so the orange hover / blue selection
tint applies.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 07:29:18 +03:00
Hakan SevenandClaude Opus 4.8 cd10aa0363 fix(select): click the front-most solid across top-level and block bodies
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]>
2026-06-21 07:24:31 +03:00
Hakan SevenandClaude Opus 4.8 d1395e8f3d fix(solid): refresh a DXF SOLID's fill after move/copy
A DXF SOLID renders as a cached solid-fill hatch, but transform_entities and
copy_entities only rebuilt that cache for true HATCH entities — a SOLID fell
through and kept drawing at its old position (or a copy showed nothing).
Rebuild the solid-fill hatch for EntityType::Solid in both paths, matching
the existing grip/add/open handling.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 07:15:03 +03:00
Hakan SevenandClaude Opus 4.8 ae9940929c fix(select): box/lasso-select blocks whose body is a solid
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]>
2026-06-21 07:03:56 +03:00
Hakan SevenandClaude Opus 4.8 ae3b305f95 fix(dim): move the baked dimension block when the dimension is transformed
A dimension's final graphics are baked into a per-instance *D block, and the
render draws that block's sub-entities directly rather than the definition
points. Move/rotate/scale/mirror updated the definition points but left the
block sub-entities in place, so the dimension stayed drawn where it was.
Apply the same transform to the dimension's *D block sub-entities.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 06:55:34 +03:00
Hakan SevenandClaude Opus 4.8 d5f1348f1b fix(3d): box/lasso-select 3D solids and refresh them after a transform
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]>
2026-06-21 06:45:49 +03:00
Hakan SevenandClaude Opus 4.8 5f3eaa621c fix(properties): recolour solid meshes on a colour/layer change
ACIS solids bake their colour into the mesh, so a property-panel or ribbon
colour (or layer) change left the solid showing its old colour — only the
wires, which resolve colour at render time, updated. Run recolor_meshes
(now covering block-definition meshes too) when property targets are
invalidated, and route the ribbon colour change through the same path.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 02:12:58 +03:00
Hakan SevenandClaude Opus 4.8 fd15dd0aac feat(select): invert selection from the right-click menu
Add an Invert Selection entry to the selection context menu (below
Select Similar): it replaces the current selection with every other
selectable object in the active layout. Candidates come from the visible
wire set, so objects on off/frozen layers stay excluded.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 21:31:05 +03:00
Hakan SevenandClaude Opus 4.8 e562738d9e fix(clipboard): carry block definitions across drawings on paste
Copying an INSERT and pasting it into another drawing rendered nothing:
the clipboard snapshotted referenced layers/linetypes/styles but not the
block definition itself, so the pasted reference pointed at a block the
target drawing didn't have.

Capture every block an copied INSERT references — walking nested INSERTs
transitively, skipping model/paper space and xref blocks — and store the
definition's base point plus its owned entities. On paste, recreate any
of those blocks the target drawing lacks via a new define_block_raw
(entities kept in block-local coordinates), before adding the pasted
references. Same-document pastes are unaffected since the blocks already
exist.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 21:23:59 +03:00
Hakan SevenandClaude Opus 4.8 caafb9b99d fix(layers): honor layer off/freeze for block sub-entities
Block-internal entities ignored their own layer state: 2D subs were
only filtered by the invisible flag in the block-defn cache, and 3D
solids render through a separate mesh path that applied no layer filter
at all. Both now skip subs on off/frozen layers, matching top-level
entities. Caches rebuild on every toggle, so visibility updates live.

Fixes #133

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 19:57:19 +03:00
Hakan SevenandClaude Opus 4.8 e7eeff5945 feat(select): orange hover / blue selection highlight for all objects
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]>
2026-06-19 09:33:53 +03:00
Hakan SevenandClaude Opus 4.8 25c347f9b0 feat(3d): instance block-definition solid meshes per INSERT
3D solid meshes inside block definitions were drawn once at block-local
coordinates without the INSERT transform, so a block authored in mm and
placed into a metre drawing at a 0.001 INSERT scale rendered ~1000x too
large. Split solid meshes into top-level (layout-owned, offset and drawn
flat) and block-definition (kept block-local) sets, and emit a transformed
instance of each block-def mesh per model-space INSERT, accumulating nested
transforms. Layout blocks are identified from the Layout objects'
block_record handles, since BlockRecord::is_layout() also flags ordinary
blocks in some files. (#123)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 01:32:17 +03:00
Hakan SevenandClaude Opus 4.8 9eb805df23 feat(ui): unified viewport control chip + per-tile grid rendering
Move grid and grid-snap toggles off the status bar (and its customization
menu) into the top-left viewport chip, now a single container holding the
horizontal/vertical split buttons, render-mode picker, and grid/snap
toggles, sized to the render-mode picker. Add grid + magnet (snap) icons.

Render the grid per model tile from each tile's own grid_on, in its own
bounds and camera, instead of a single grid in the active tile. This stops
the grid flickering on/off as the cursor crosses panes or leaves the
viewport, and lets each pane show its own grid independently.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 23:29:34 +03:00
Hakan SevenandClaude Opus 4.8 d4a665a34c feat: per-viewport grid/snap, adopted from the active viewport
Make grid-snap a per-drawing view setting like grid visibility: drop it
from the persisted OSNAP set (settings.txt) and round-trip it through each
viewport's VPort SNAPMODE instead.

Grid display and grid-snap are now stored per model tile (like render
mode), written to and read from each tile's *Active VPort entry. The live
toggles mirror the active tile, and grid/snap are adopted from the active
viewport whenever it changes — on file load, tab switch, model-tile
activation, and new drawing — so they follow the active viewport rather
than a global preference.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 22:25:44 +03:00
Hakan SevenandClaude Opus 4.8 b2feeb716b refactor: make grid visibility per-drawing, not a global setting
Grid on/off now round-trips through the drawing's *Active VPort, so it is a
per-file view setting like every other CAD app treats it. Drop show_grid
from UserSettings (and its settings.txt key) and instead adopt the opened
file's saved grid_on on load. A legacy "grid=" line in an old settings file
is harmlessly ignored.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 22:11:27 +03:00
Hakan SevenandClaude Opus 4.8 cd17096f65 feat(io): persist grid/snap display state to the active VPort
Mirror the in-app grid and grid-snap toggles (plus the adaptive grid
spacing) onto the *Active VPort entries so a saved DWG/DXF reflects what
the user actually has on, instead of always writing VPort defaults. The
flags are stamped when the toggles change and again before a save, and
carried across the camera-sync VPort rebuild so they survive an orbit/pan.

Only grid_on, snap_on and grid_spacing have an OCS counterpart; the other
VPort view fields (view twist, clip planes, lens length, etc.) have no
in-app source and stay at their faithful defaults.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 22:05:25 +03:00
Hakan SevenandClaude Opus 4.8 02b0fc4dcf chore: drop brand names and orphaned PICK_HIGHLIGHT colours
Neutralise vendor references in comments to plain behavioural descriptions
(object acquisition / object snap). Remove the PICK_HIGHLIGHT and
PICK_HIGHLIGHT_DIM wire colours: their only consumer was the stripped
storm-sewer add-on, and the retained plugin acquisition pathway draws the
ObjectPick snap glyph and per-command hover previews instead, so the
constants are dead.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 21:28:07 +03:00
Hakan SevenandClaude Opus 4.8 47f74a9b98 feat(clipboard): paste menu with paste-to-original and paste-as-block
Turn the ribbon Clipboard "Paste" button into a dropdown:
- Paste — existing pick-a-point paste.
- Paste to Original Coordinates — drops the clipboard at its source
  coordinates with no prompt.
- Paste as Block — wraps the clipboard contents in a new block definition
  and starts an interactive insert (prompts for the drop point, with the
  geometry rubber-banding under the cursor).

Also fixes two bugs the new commands exposed:
- entities_centroid summed the NaN separator points wire models carry,
  so the paste base point came out NaN; non-finite points are now
  skipped. A NaN base previously froze block tessellation outright.
- PASTEORIG added clipboard entities with their original handles
  (duplicates on same-document paste); it now goes through
  add_entity_clone and merges referenced records like the other pastes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 16:09:47 +03:00
Hakan SevenandClaude Opus 4.8 446430d2b3 fix: regenerate inline sub-entity handles when copying entities
`document.add_entity` only assigns the top-level handle, leaving inline
sub-entities that carry their own — INSERT attributes and 3D-polyline
vertices — with the source's handles. Copying such an entity therefore
produced duplicate handles in the document, which corrupts the saved DWG
so other CAD apps (QCAD, DWG TrueView) refuse to open it.

Add `Scene::add_entity_clone` / `reset_clone_subhandles`, which allocate
fresh handles for the entity and every inline sub-entity, and route all
duplication paths through it: COPY/ARRAY/MIRROR-copy (via copy_entities)
and clipboard paste. (#129)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 14:45:06 +03:00
Hakan SevenandClaude Opus 4.8 96ec672d5b feat(points): point style commands + DDPTYPE dialog, viewport-aware size
Points could only render as a 1px dot — no way to change PDMODE/PDSIZE.

- PDMODE / PDSIZE commands set the header value and rebuild the glyphs.
- DDPTYPE opens a Point Style dialog: a 5x4 grid of rendered glyphs
  (shape x enclosure), a size field, and relative/absolute radios.
- Relative (<=0) PDSIZE now sizes from the current world-per-pixel so the
  marker stays a roughly constant on-screen size across zoom, rebuilding
  on zoom via the existing camera-generation cache key. Absolute (>0)
  scales with the world as before. Switching to absolute with a zero size
  seeds a positive value from the on-screen size so it stays representable.

Part of the #100 extensibility epic (point style surfaced in #106).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 11:13:12 +03:00
Hakan SevenandClaude Opus 4.8 c49ad887b8 fix(wasm): make the web build actually run in the browser
With the wasm bundle building, get it rendering in a browser:

- Enable iced `webgl` so wgpu uses WebGL2 where WebGPU is absent
  (e.g. Firefox by default), and `fira-sans` so UI text has an
  embedded font (the web has no system fonts) — without it every
  label was blank.
- Replace `std::time::Instant` with `iced::time::Instant` (web-safe);
  `Instant::now()` panics ("time not implemented") on wasm and crashed
  at boot via the command line.
- Skip the batched-hatch pipeline on wasm: its bind group needs
  vertex-stage storage buffers, which WebGL2 lacks (validation error
  on "New drawing"). Fields become Option; upload/draw guard on them.
  Hatches don't draw on the web for now; everything else does.
- Fix the trunk feature flag (`data-cargo-no-default-features`) and
  ignore the `dist/` build output.

Native build + 75 tests unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 17:53:56 +03:00
Hakan SevenandClaude Opus 4.8 9ca19a9db8 feat(wasm): sequential rayon shim for the web build
wasm32 has no threads, so rayon's global pool would panic at runtime.
Add `crate::par::prelude`: native re-exports `rayon::prelude`, wasm
provides sequential `std`-iterator equivalents (par_iter / into_par_iter
/ flat_map_iter over slices, Vec, HashMap, HashSet). All call sites now
`use crate::par::prelude::*;` instead of rayon directly.

- wire_gpu's rayon `reduce` (2-arg, no std equivalent) is cfg-split to
  a sequential flat_map concat on wasm.
- rayon moves to a non-wasm target dependency.

Native still runs in parallel; all 75 tests pass. wasm bin compiles.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 17:11:20 +03:00
Hakan SevenandClaude Opus 4.8 3666c7fa69 refactor(modules): rename home module to draw, restore first-tab order
The Home tab was already titled "Draw"; rename the module to match:
folder modules/home → modules/draw, HomeModule → DrawModule, id "home"
→ "draw". The inner draw/ command folder is kept (modules::draw::draw).

Update build.rs PRIORITY ("home" → "draw") so the tab returns to the
front of the ribbon; registry.rs regenerates accordingly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 11:49:14 +03:00
Hakan SevenandClaude Opus 4.8 c6152ef705 refactor(scene): rename model_solid to solid_model
Match the `*_model` naming of the sibling render models (wire_model,
hatch_model, mesh_model, image_model). Also realign the related Scene
API for consistency: model_solids → solid_models, add_model_solid →
add_solid_model, register_model_solid → register_solid_model.

No behavior change; build and tests are green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 11:39:31 +03:00
Hakan SevenandClaude Opus 4.8 7fe7e0230d refactor(scene): group modules into role-based subfolders
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]>
2026-06-16 11:33:46 +03:00
Hakan SevenandClaude Opus 4.8 cd048165a9 feat(fonts): system TrueType support with shaping and fallback
Add a TrueType/OpenType render path alongside the LFF stroke engine so
users can pick fonts installed on their machine for text and MText.

- Discover system families via fontdb (sysfont) and list them in the
  text-style picker.
- Extract glyph outlines with ttf-parser, flattened into wire strokes
  normalized to the same 9-unit cap-height space as LFF glyphs.
- Shape runs with cosmic-text (ligatures, Arabic joining, kerning, bidi)
  plus automatic font fallback: a glyph missing from the chosen family
  is taken from another installed font.
- Unify both sources behind a single Face type; a shared run tokenizer
  drives the stroke (per-glyph) and shaped (TTF) renderers identically.
- Stroke fonts now fall back to a system TTF (cosmic-text picks it) for
  characters no LFF font covers, replacing the unicode/iso3098 chain.

Also trim the embedded LFF set from 47 to 26, keeping the AutoCAD-SHX
equivalents and dropping LibreCAD-only fonts now covered by TrueType.

LFF builtins keep priority over same-named system fonts so DXF text
renders identically across machines.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 11:18:17 +03:00
Hakan SevenandClaude Opus 4.8 540d5f7b78 fix(xclip): clip hatch fills and render nested-block hatches
Hatch fills render through the separate HatchModel pipeline, so the
block wire clip never touched them: hatches outside an XCLIP boundary
were still drawn. Clip each hatch boundary loop to the clip polygon
(Sutherland-Hodgman, even-odd islands preserved) when the insert carries
a spatial filter.

Also fix hatches inside the boundary that were never drawn: the hatch
collector exploded each insert only one level, so hatches nested in
sub-blocks were dropped entirely. Walk the full block tree (depth
guarded) so nested hatches render and clip like the rest.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-15 13:02:55 +03:00
Hakan SevenandClaude Opus 4.8 dd1ec3ad40 feat(xclip): clip block references to their SPATIAL_FILTER boundary
Block references carrying an XCLIP spatial filter were drawn in full,
ignoring the clip boundary. Resolve the filter through the INSERT's
extension dictionary (ACAD_FILTER -> SPATIAL) and clip the expanded
block geometry to the boundary polygon: polylines are split into the
inside runs, fill triangles are clipped, and snap/key vertices outside
the boundary are dropped.

The boundary vertices live in the clip-definition space, so they are
placed with world = T_insert * (inverse_block_transform * vert); this
keeps the clip correct even when the insert was rescaled after the clip
was created. Bumps acadrust to the build that decodes SPATIAL_FILTER.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-15 12:29:10 +03:00
Hakan SevenandClaude Opus 4.8 0badf48454 feat(scene): tessellate loaded ACIS solids via the truck B-rep kernel
Rebuild every ACIS face as a truck topological Face and mesh it with truck,
routing loaded 3DSOLID/BODY/REGION/SURFACE through the same kernel the Model
tab uses. plane→try_attach_plane, cone/cylinder/sphere/torus→revolution faces
(truck's Surface has no cone/sphere/torus), spline→BSplineSurface grid sample.
Per-vertex normals are computed analytically outward per surface type and
triangle winding is oriented to match, since truck's own orientation isn't
consistent across independently built faces. `tessellate_solid3d|body|region|
surface` try this first and fall back to the bespoke sampler.

Light the mesh shader with three spread directional lights plus ambient
(abs(dot) per light, two-sided) so faces — including the inner walls of an
open lofted surface — never read as a flat dark mass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-15 00:30:18 +03:00
Hakan SevenandClaude Opus 4.8 9d823363f1 feat(scene): render 3D surface entities (loft/sweep/extrude/etc.)
ACAD_SURFACE entities now load and render. Their NURBS faces are
tessellated by building a truck BSplineSurface from the ACIS `spline-surface`
control net and knot vectors, then sampling its parametric grid — reusing the
same NURBS kernel the Model tab already builds on. Analytic faces
(plane/cone/sphere/torus) continue through the existing tessellator.

- New spline_tess.rs: parse the ACIS `nubs` block (degrees, knots with the
  clamped end-multiplicity fix, control grid) into a truck BSplineSurface and
  mesh it.
- tessellate_surface + Surface wired through the mesh-build, fallback-wire,
  and entity-classification paths.
- Bump acadrust to the commit that adds the Surface entity.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 20:11:44 +03:00
Hakan SevenandClaude Opus 4.8 a2cb833c83 fix(scene): render raster images at correct position and size (#98)
Match the image shader's uniform layout to the shared 112-byte Uniforms
struct (was 144) and project quad corners via view_proj so texture-quad
draws no longer fail uniform binding validation.

Offset image quad corners by world_offset, matching all other Model-space
geometry, so rasters land in the same local space instead of being shifted
away. Downscale oversized images to the GPU's max texture dimension (8192)
to keep texture creation from failing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 15:06:06 +03:00
Hakan SevenandClaude Opus 4.8 f6b894813e fix(zoom): fit 3D solids in ZOOM EXTENTS (fit_all)
ZOOM EXTENTS routes through fit_all, which built its bounds from the
wire set and returned early when there were none. A drawing of only 3D
solids has no wires, so ZOOM EXTENTS did nothing. Collect the
layout's mesh AABBs alongside the wires, keep going when only meshes
exist, and fold them into the fitted bounds.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 00:42:50 +03:00
Hakan SevenandClaude Opus 4.8 8eeb155bfe fix(zoom): include 3D solid meshes in model-space extents
ZOOM EXTENTS / auto-fit derived the drawing bounds from wire AABBs and
tessellated key-vertices only. 3D solids (Solid3D/Region/Body) render as
meshes with an empty wire path, so they contributed nothing and a
drawing containing only solids zoomed to nothing. Fold each mesh's
offset-relative XY AABB into the extents on both the wire-cache and
tessellate-fallback paths.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 00:09:05 +03:00
Hakan SevenandClaude Opus 4.8 28a30de15d fix(wipeout): close rectangular fill loop so the mask stops bleeding
The GPU in_polygon ray-cast walks sequential boundary pairs and never
wraps last→first, so it needs an explicit closing vertex. The polygon
fill branch already appends one, but the rectangular branch returned a
bare 4-corner quad — the v3→v0 (left) edge went untested and the solid
mask bled past the boundary. Newly created wipeouts take this branch,
so the fill rendered wider than the clicked box while the border/grips
(drawn as wires) stayed correct. Repeat corner 0 to close the loop.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 18:10:47 +03:00