Commit Graph
367 Commits
Author SHA1 Message Date
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 31ed911714 fix(3d): place and move ACIS solids and tilted-normal blocks correctly
Bump acadrust to pick up real ACIS body transforms (so moving/copying/
rotating a 3D solid relocates its geometry) and OCS-aware INSERT placement.

- body_transform: read the SAT transform record from its first 13 float
  tokens instead of by raw index, which skipped the matrix because the
  record leads with a book-keeping pointer — so the body placement was
  silently ignored and solids never honored their stored transform.
- Insert grips: the grip lives in world space but insert_point is stored in
  the block's OCS, so grip display and grip-drag now round-trip through the
  OCS. A block whose extrusion normal isn't +Z no longer shows its grip in
  the wrong place or drags along the wrong axes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 01:52:53 +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 54106ca9b0 feat(web): lazy per-script font loading for international CAD text
The web build has no system-font access, so CAD text in non-Latin scripts
(Cyrillic, Greek, CJK, …) rendered as nothing — build_fallback was a hard
None on wasm (#141). Instead, ship per-script Noto subsets under web/fonts
(one alphabet per file, SIL OFL 1.1) and fetch them lazily over HTTP the
first time a drawing uses that script, then outline glyphs with ttf-parser.

- web_font: Script enum + script_of (char → script font), a per-script
  store with lazy fetch, and a pending queue drained by the app loop.
- CJK is split by language (chinese/japanese/korean). Han ideographs share
  code points but differ by language, so the shared block is routed by the
  document's $DWGCODEPAGE (932→JP, 949→KR, GB/936→CN); kana is always
  Japanese, Hangul always Korean. Re-tessellates when the language changes.
- build_fallback (wasm) outlines from the fetched subset; clear_fallback_cache
  lets glyphs that missed while a font was in flight reappear on load.
- PollWebFonts subscription (web only) + WebFontLoaded message drive fetches.
- Trunk copy-dir serves web/fonts; nothing is bundled into the native binary
  (desktop keeps using system fonts).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 16:13:26 +03:00
Hakan SevenandClaude Opus 4.8 1ed3315e50 fix(ocs): use Wz in arbitrary-axis algorithm else branch
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]>
2026-06-20 13:28:36 +03:00
Hakan SevenandClaude Opus 4.8 3139a73753 docs(3d): explain pierced-face outer-loop selection
ACIS stores no outer-vs-hole loop flag, so plane_face derives the outer
boundary as the largest-area loop and winds holes opposite. Document why
this is robust: try_attach_plane takes its plane normal from the first
(outer) wire, making the choice self-consistent without relying on the
analytic face normal. No behaviour change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 11:38:03 +03:00
Hakan SevenandClaude Opus 4.8 249922e8d9 fix(3d): pick pierced face outer loop by area, not list order
A planar ACIS face lists its loops outer-first only by convention; a
pierced wall lists its window holes before the wall edge. plane_face
trusted loops[0] as the outer boundary and oriented every other loop
relative to it, so on a face with two or more holes one real hole wound
the same way as the true outer and truck's winding-number trim filled it
in. Select the outer boundary as the largest-area loop (it encloses the
rest) and wind all others opposite. (#123)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 10:44:52 +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 93e2d20412 fix(3d): render pierced solid faces with their holes
Build pierced planar faces (a wall with a window opening) as truck faces
with inner hole boundaries: collect every face loop (outer + holes), wind
each hole opposite the outer so try_attach_plane cuts it, and let truck
triangulate. Drop the back-face cull on the solid-mesh pipelines so faces
show regardless of the import's winding (the shader already lights
two-sided). Relies on the acadrust SatLoop next_loop fix that makes hole
loops reachable. (#123)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 15:21:21 +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 c6b3975fd4 feat(properties): show entity handle in the General section
Adds a read-only Handle row at the top of the properties palette so an
entity can be identified by handle (e.g. when cross-referencing a file
dump or reporting a specific object).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 01:52:02 +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 5a36494ed7 chore: clear compiler warnings
Remove accidental dead code (unused imports, the superseded paste tool()
helper, a never-read TORIENT handles field, a duplicate let binding, an
unneeded mut) and mark intentional-but-unwired scaffolding with
allow(dead_code): the web-only save_to_bytes, the plugin loadable() probe,
the planned PICK_HIGHLIGHT colours, the DynFieldSpec label, and the layout
ribbon helpers. Native build is now warning-clean apart from the upstream
nom/quick-xml future-incompat notices.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 21:18:09 +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 6996343e84 feat(wasm): open files in the browser
Wire up file open for the web build:

- `io::pick_and_load_web`: browser file picker → read bytes → parse via
  `load_bytes` → build caches, returning the same payload as the native
  loader so it feeds the shared `Message::FileOpened` handler.
- `Message::OpenFile` cfg-dispatches: native picks a path then loads on a
  worker thread; web picks + parses in one async step. The web branch also
  sets `opening` (the handler drops results when no open is in progress).
- Disable the cosmic-text shaping / fallback paths on wasm: the web has no
  system fonts, so cosmic-text panics with "no default font found". LFF
  stroke fonts (embedded) still render; LFF-missing glyphs are skipped.

Native build + 75 tests unchanged. Hatches still don't draw on the web.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 18:08:57 +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 b2e20c95fe feat(wasm): compile the library for wasm32 (issue #45 groundwork)
First step toward a web build: the library now compiles for
wasm32-unknown-unknown with --no-default-features.

- Add a `solid3d` feature (default on) gating truck-meshalgo /
  truck-shapeops / lzma-sys. These pull vtkio → xz2 → lzma-sys (C),
  which cannot cross-compile to wasm. Without the feature the 3-D
  solid path (tessellation, booleans, ACIS import) becomes no-ops;
  2-D CAD is unaffected. Native builds keep the feature by default.
- Move native-only crates (ureq, open) to non-wasm target deps and
  add cfg shims: update-check is a no-op on wasm, and a new `sys`
  module provides open_url + handle_path (rfd FileHandle → PathBuf;
  synthesizes a name-based path on the web).

Native build + all 75 tests unchanged. Browser runtime wiring
(entry point, trunk, real file I/O) is a follow-up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 13:01:19 +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 e2be8197b4 feat(stretch): live ghost preview while dragging the new point
STRETCH now shows a live ghost where vertices inside the crossing window
follow the cursor and the rest stay anchored, instead of only a rubber-band
line. Add WireModel::stretched and pass the selection's wires into the
command.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-15 23:43:07 +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 b173f32d5a fix(scene): tessellate cone and cylinder 3D solids
Cone and cylinder bodies rendered empty because their geometry depends on
circular edges the previous boundary walk couldn't use:

- Sample ellipse/circle boundary edges into multiple points so circular caps
  build a real polygon instead of a single degenerate vertex.
- Recover a cone/cylinder lateral face's height span from the solid's coaxial
  circle rims (plus a true cone's analytic apex) when the face boundary
  collapses to one height.
- Wind cone-side quads and plane-face fans so their face normals point
  outward, keeping flat-shaded and Gouraud lighting consistent.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 15:43:24 +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 5fa93b9cb0 fix(solid3d): stop drawing a placeholder line beside modeler solids
Solid3D / Region / Body have no truck wire conversion, so the wire path
fell through to the generic fallback, which returns a 1-unit placeholder
segment at the origin — a stray little line next to every 3D solid. Give
these types their own fallback that emits only the entity's pre-computed
edge wires (empty for binary SAB) plus an insertion snap, never the
placeholder.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 00:04:55 +03:00
Hakan SevenandClaude Opus 4.8 876474f5aa fix(solid3d): place ACIS solids by their body transform
ACIS keeps a solid's geometry in body-local space and records the world
placement in a `transform` record (3×3 affine + translation + scale).
The tessellator ignored it, so solids rendered at their local origin
instead of where the drawing puts them. Apply the body transform to the
mesh vertices (and rotate the normals) after tessellation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 23:57:40 +03:00
Hakan SevenandClaude Opus 4.8 f366aae33c fix(blocks): hide invisible sub-entities in block definitions
A dynamic block with a visibility-state parameter stores the geometry
for every state in one anonymous block and marks all but the active
state's entities invisible. The block-cache builder tessellated every
sub-entity regardless, so a visibility block rendered all variants
stacked on top of each other instead of the one selected profile.
Skip invisible sub-entities in both the render pass and the precision-
offset centroid pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 19:06:55 +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
Hakan SevenandClaude Opus 4.8 d09614be39 fix(ellipse,mirror): WCS ellipse rendering + delegate hatch MIRROR upstream
Two follow-ups from the boundary-arc convention audit:

- ELLIPSE is a WCS entity in DXF (codes 10/11 are world coordinates,
  unlike ARC/CIRCLE). The renderer ran center and major_axis through the
  arbitrary-axis OCS, which misplaced any ellipse whose normal isn't Z-up
  — e.g. the (0,0,-1) result of a mirrored-block explode. Read them as
  WCS; pairs with acadrust 569bd4d, which stores WCS on its explode
  paths. Z-up ellipses are unaffected (the conversion was an identity).

- The MIRROR command's hand-rolled hatch closure swapped boundary-arc
  angles itself — only valid for ccw arcs and ignorant of the stored
  mirrored-angle / wrap-encoded-sweep conventions. It now builds a
  reflection Transform (new reflection_about_xy_line helper) and
  delegates to acadrust's transform_hatch, the single source of truth
  for those conventions. Covered by a Line→Arc→Line continuity + sweep
  regression test.

Note: full effect of the ellipse change lands once the acadrust pin is
bumped to 569bd4d; until then only the (previously broken) non-Z-up
corner differs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 09:03:18 +03:00
Hakan SevenandClaude Opus 4.8 14b5067e25 fix(hatch): mirrored-block arcs — bump acadrust, document angle convention
Hatches inside mirrored INSERTs (x_scale = -1, e.g. the *U blocks in
xref_Plan-Sade) drew some boundary arcs swept the wrong way, covering
huge complementary regions — in the pattern clip before EXPLODE, and in
both pattern and boundary wires after (the exploded entity bakes the
broken angles).

Root cause was in acadrust's transform_hatch, not the renderer: DXF
stores CW (ccw=false) boundary-arc angles MIRRORED (verified against
AutoCAD output by endpoint continuity — mirrored interpretation gives
Δ=0.0 against adjacent edges), and the transform stored geometric angles
after flipping the direction flag. The legacy (TAU-θ) sampling flip in
arc_signed_span is exactly right for file data and stays; its comment now
records the verification so the convention isn't "fixed" away again.

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