Commit Graph
112 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 67fe363775 fix(text): grip-drag no longer hides text (#316)
Dragging text by a grip hid it until release. The dragged entity is
hidden from the base wire set and shown as a preview overlay, but its
SDF glyph quads (text_verts) were never uploaded for the preview, so
the text vanished mid-drag.

Add a per-frame preview_text GPU buffer (sibling to preview_wires). A
rigid whole-entity move of pure text (TEXT/MTEXT, Square insertion grip)
slides the drag-start glyphs by the drag delta instead of re-shaping the
run every move; reshape grips (MTEXT width), dimensions and wire
entities re-tessellate and feed the same buffer.

Also fold preview_text into render_signature: a pure-text slide leaves
preview_wires empty, so without it the scene-render cache froze the
dragged text at its first frame (grip moved but text stayed, then
jumped to place on release).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-10 10:39:44 +03:00
Hakan SevenandClaude Opus 4.8 d37aacd895 refactor(pipeline): name the real hatch renderer hatch_gpu, legacy → wipeout_gpu
The batched path is the sole hatch renderer and has no family cap, yet it
was named hatch_batched_gpu while the old capped per-primitive renderer
kept the hatch_gpu / MAX_FAMILIES name — which reads as if visible hatch
fills are truncated at 16 families (they are not). Swap the names so the
active renderer is hatch_gpu.rs / hatch.wgsl and the legacy one, now used
only for wipeout masks, is wipeout_gpu.rs / wipeout.wgsl. MAX_FAMILIES now
lives only in the wipeout module. Pure rename; no behaviour change.

Refs #323.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 10:45:40 +03:00
Hakan SevenandClaude Opus 4.8 54a29409e7 fix(web): grow-only viewport textures to stop Windows-Firefox divider-drag freeze (#191)
Dragging a model-tile pane_grid divider changed each pane's pixel size
every frame, and `ensure_depth_texture` recreated the depth + 4×MSAA +
resolve textures on any size delta. On Windows Firefox (WebGL2 → ANGLE →
D3D11) this per-frame texture realloc hangs the D3D driver — the same
hang class as the native DX12 lock in #120. Linux Firefox (native GL) and
Chromium/Edge tolerate the churn, which is why the freeze is specific to
Windows Firefox.

Round the depth/MSAA/resolve allocation up to a 128px grid and only
recreate when the rounded size changes, so a live drag reallocates at
most once per 128px instead of every frame. Geometry passes still render
at the real size into the [0,0,render] corner of the larger texture, and
the blit UV is scaled by render/alloc so it samples exactly the filled
region — visually a no-op on every platform.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 18:20:27 +03:00
Hakan SevenandClaude Opus 4.8 4df41cdf34 fix(hatch): render fills via the per-fragment in_polygon test again
The batched hatch path pre-triangulated every boundary on the CPU
(PR#300) with no cap on the resulting vertex buffer. A hatch-dense
drawing (13k solid-filled hatches) built a ~6.75 GB `hatch_batched.vertex`
buffer and panicked in `Device::create_buffer` (device max 256 MB), after
a long stall spent triangulating on load.

Drop the CPU triangulation and always draw each hatch as its AABB quad
(6 verts) with `poly_test = 1`, letting the shader resolve inside/outside
per fragment against the uploaded boundary — the path the shader always
kept as a fallback. The vertex buffer is now 6 verts/hatch regardless of
boundary complexity, so no drawing can overflow it. The interaction-LOD
flicker PR#300 addressed no longer occurs (OCS_HATCH_LOD defaults off).
Removes the now-unused hatch_tess triangulation module.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 13:30:49 +03:00
Hakan SevenandClaude Opus 4.8 fe00bc30ba Merge PR #300: tessellate hatch fills so navigation no longer flickers (#258)
External contribution by sLuCHa. Triangulates each hatch boundary once
(lyon even-odd, cached with the batch build) and rasterizes the triangles
instead of the per-fragment point-in-polygon test, removing the screen-fill
overdraw + boundary raycast that forced the flicker-causing interaction LOD
(now default off behind OCS_HATCH_LOD). Precision preserved (triangles in the
same world_origin-relative frame + RTE path); untessellatable boundaries fall
back to the old AABB-quad + in_polygon path (poly_test=1).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 21:45:02 +03:00
Hakan SevenandClaude Opus 4.8 110683f419 refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).

- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
  cached by the tess memo, cloned on hit, and transformed by the block
  expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
  the strokes; render gathers text_verts from the viewport wire set
  (`gather_text_verts`, cached on wire content id) — the old collector is
  removed. Covers model, dimension, table, block-internal and paper-sheet
  text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
  SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
  entity_aabb which mis-places the box for MTEXT; composite paths no longer
  clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
  highlight, keyed on selection_generation) mirroring the selected-wire
  xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 17:01:36 +03:00
sLuCHa b4152ae1fe perf(hatch): tessellate fills so navigation no longer flickers (#258)
The batched hatch pass drew a screen-covering AABB quad per hatch and,
for every fragment, ran a point-in-polygon ray-cast over the whole
boundary plus the pattern test, with a discard that disabled early-Z.
Zoomed into a hatch the quad covers the screen, so the shader re-ran
per pixel every frame — the cost the PR #273 interaction-LOD worked
around by suppressing hatches during navigation, which made fills
flicker on and off while zooming.

Triangulate each boundary once (lyon, even-odd — same coverage as the
old in_polygon, islands/holes included) and rasterize those triangles
instead: no screen-filling overdraw, no per-fragment boundary test. The
fragment shader keeps the boundary test only on a per-instance fallback
path (poly_test == 1) for boundaries that fail to tessellate.
Tessellation is cached with the batch build (geometry_epoch) in local
space, so the double-single relative-to-eye precision is preserved.

With the fast path the interaction-LOD is no longer needed; it is gated
behind OCS_HATCH_LOD (default off) as a safety net for now.
2026-07-07 15:23:54 +02:00
Hakan SevenandClaude Opus 4.8 896b6c385c feat(text): render TEXT as SDF quads behind OCS_TEXT_SDF flag
Wires the SDF glyph atlas into the render loop end-to-end for top-level
TEXT entities, gated by the OCS_TEXT_SDF env var so the default path is
unchanged. Verified rendering on a real drawing: glyph quads land exactly
over the existing stroke text (position, size, rotation), confirming the
atlas → shader → pipeline → screen path and the RTE / UV math.

- text::text_run_placement — extracted from to_truck (behaviour-preserved)
  so the stroke and SDF-quad paths compute an identical run placement.
- sdf_atlas::text_atlas — process-wide GlyphAtlas (Mutex/OnceLock), shared
  by the collector (bakes glyphs) and the GPU upload (reads texels).
- Scene::sdf_text_vertices — collects per-glyph TextVertex for the frame's
  TEXT entities via layout_glyph_quads + push_glyph_vertices. Empty unless
  OCS_TEXT_SDF is set.
- ViewportData.text_verts + Pipeline text_pipeline/atlas/vbuf, upload_text
  (rebuilds the R8 atlas texture when it grows), and a text render pass
  drawn over the wires.

Still additive over the stroke text; only top-level TEXT is wired (MTEXT,
attributes, and block-internal text are next), and the collector re-runs
per frame (vertex caching by geometry epoch is a follow-up).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 23:02:34 +03:00
Hakan SevenandClaude Opus 4.8 374cf7e89f feat(text): SDF glyph-atlas foundation for shader text rendering
Groundwork to render CAD text as per-glyph SDF quads (box-quad snap,
perf, no LOD) instead of stroke-polyline wires. CPU side fully tested;
GPU side compiles but is not yet wired into the render loop.

- scene::text::sdf_atlas — GlyphAtlas: shelf packer + R8 SDF tiles baked
  per (font, char). Unified field so one shader draws both kinds: TrueType
  = signed distance to the filled outline, LFF stroke = pen band. Uses only
  crates already in the tree (ttf-parser/lyon via the existing Glyph).
- scene::text::glyph_quads — layout_glyph_quads: lays a run out as per-glyph
  quads using the exact tessellate_text_run transform, so quads land where
  the strokes would. Per-char advance (TTF shaping is a follow-up).
- shaders/text.wgsl — SDF fragment shader (smoothstep over fwidth), RTE
  vertex path mirroring image.wgsl, per-vertex colour + draw-order bias.
- scene::pipeline::text_gpu — TextVertex + double-single vertex builder,
  R8 atlas texture upload, bind group, and the image-cloned pipeline.

13 unit tests across the three CPU stages, all green. Modules carry
#![allow(dead_code)] until the render-loop integration lands.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 22:03:36 +03:00
sLuCHa a55e1ac4f8 perf(render): skip hatch pass while navigating (interaction LOD)
The batched hatch pass evaluates its fill procedurally per fragment — a
point-in-polygon test over the whole boundary plus the pattern test, with a
`discard` that disables early-Z. When the view sits inside a hatch its quad
covers the screen, so panning / zooming re-runs that full-screen shader every
frame (~40 ms/frame on a hatch-heavy plan) even though the rest of the scene
is cheap — the wire pass and CPU prepare are negligible by comparison.

Suppress the hatch pass while the view is actively panning / zooming /
orbiting, detected from `camera_generation` plus a short settle timer
(`Scene::navigating_lod`), and fold that state into the render signature so
the moment the view settles it renders hatches once and the scene-render
cache holds that full-quality frame. A brief `is_settling` subscription keeps
requesting frames just past the settle point so the hatched frame renders
even when no input event would otherwise fire. Mirrors AutoCAD's adaptive
degradation. The paper sheet keeps its fills.
2026-07-05 09:48:49 +02:00
sLuCHa f99f1cd7a5 perf(render): cache rendered scene, skip re-raster on unchanged frames
Every cursor movement over the viewport re-rendered the entire GPU scene
(all wires, hatches, meshes) even when nothing visible changed: iced redraws
the whole window on each input message, and the shader's draw/prepare/render
runs every time. On large drawings one such redraw exceeds the frame budget,
so moving the mouse or panning stutters — and it gets worse the bigger the
drawing.

The pipeline already resolves each frame into a persistent, clip-sized
resolve texture and blits that to the surface. This hashes the inputs that
determine the rendered image (camera uniforms, geometry/selection
generations, wire content id, render-mode flags, clip size, live preview
coordinates) into a per-viewport signature. When a frame's signature matches
the previous one the image is pixel-identical, so `prepare` skips the buffer
uploads + per-frame scissor/LOD recompute and `render` skips passes 1-7 plus
the MSAA resolve, re-blitting the resolve texture instead. A pure cursor move
becomes a single fullscreen blit rather than an O(N) re-rasterization.

The ViewCube is excluded from the signature (it renders in its own always-on
pass) so cube hover still tracks while the scene is cached. Any real change —
edit (geometry_epoch), select/hover (selection_generation), camera move or
render-mode toggle — differs the signature and forces a full render, so
nothing goes stale. Navigation still renders every frame (the view genuinely
changes); this targets the wasted re-render on cursor-only movement.
2026-07-05 08:11:35 +02:00
Hakan SevenandClaude Opus 4.8 17077ba8bf fix(render): don't apply the draw-order z-bias to 3D mesh outline edges
Mesh (PolyfaceMesh / PolygonMesh) outline edges are drawn through the wire
pipeline, whose shader pulls each wire toward the camera by
draw_depth * DRAW_ORDER_BIAS (0.001) for 2-D draw-order layering. Every
entity gets a draw_depth in [-1, 1], so a mesh's edges were pulled forward
too — and when the mesh's depth extent projects to less than the bias (a
small or distant mesh), its hidden back-face edges peeked through its own
shaded fill. Solid feature edges use the mesh shader, which has no such
bias, which is why only meshes showed it and why adjusting the fill's depth
bias had no effect.

3-D mesh outline edges are real geometry occluded by true depth, so force
their draw_depth to 0 (they are already grouped as `mesh_edge` in
upload_wires). They now occlude exactly like solid edges.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 21:04:37 +03:00
Hakan SevenandClaude Opus 4.8 e6a961be6c feat(render): draw 3D mesh outline edges black in filled modes
Extends the solid feature-edge behaviour to mesh entities (PolyfaceMesh /
PolygonMesh): their triangulation outline edges now render black when a
filled-with-edges mode is active, and keep the entity colour in the
lines-only modes (wireframe / hidden-line).

A mesh entity emits its face fill and its outline edges as separate
WireModels sharing the entity handle: the fill carries fill_tris plus a
non-empty fill_tris_low (real 3D depth), the edge carries points. The old
is_3d_mesh_edge flag keyed on `!fill_tris.is_empty()`, which tagged the
points-less fill wire rather than the edge wire, so the edges were never
recognised. upload_wires now collects the handles of real 3D fill wires and
flags any points-wire sharing one as the mesh edge (text greek, whose fill
has an empty fill_tris_low, is excluded — matching the face3d test). The
wire draw loop then routes flagged edges through a new black wire pipeline
(fs_black) in filled-with-edges modes.

fs_black lives in wire_indexed.wgsl (the native shader) and wire.wgsl (the
wasm shader).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 18:28:27 +03:00
Hakan SevenandClaude Opus 4.8 1065bc12b2 feat(render): draw ACIS solids as B-rep feature edges, meshes as tri edges
Solid wireframes rendered every triangle edge of the tessellation, so a
curved solid read as a dense mesh instead of its real outline. Now each ACIS
solid's B-rep face-boundary edges are collected from the SAT `edge` records
(straight edges as segments, ellipse/circle edges sampled along their bounded
arc), body-transformed and split into the double-single pair, and stored on
the MeshLodSet. The block-instance and top-level offset paths transform these
edges alongside the mesh vertices.

The batch builder routes a solid with feature edges into a dedicated LineList
vertex buffer and skips its triangle wireframe; a mesh with no B-rep edges
keeps its triangulation edges. Edge colour follows the display mode: the flat
`fs_edge` shader draws the entity colour in the lines-only modes (wireframe,
hidden-line) and a `fs_edge_black` twin forces black for the edge overlay in
filled render modes.

Verified: the Clinic storefront arch bars now yield ~14-16 clean edge
segments (vs the full triangulation) bounded to the solid's true extent.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 17:57:00 +03:00
Hakan SevenandClaude Opus 4.8 a08cf477c1 fix(render): draw transparent solids without depth writes
Once solids carried a real sub-1 alpha (after the ENC transparency fix),
the single depth-writing mesh pass made a transparent solid occlude the
geometry behind it: it wrote depth at its near surface, so anything farther
failed the LessEqual test and was never drawn — the transparent fill looked
like it erased what was behind it.

The batched mesh builder now routes each solid's triangles into an opaque or
a transparent index stream by its baked colour alpha (< 0.999). The opaque
fills draw first and write depth; the transparent fills draw last through a
new depth-write-disabled pipeline, so they blend over the geometry behind
them instead of culling it. The hidden-line depth prepass still rasterises
both streams so solid surfaces keep occluding hidden edges.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 16:27:59 +03:00
Hakan SevenandClaude Opus 4.8 d0dcc6cb4c fix(render): size mesh-batch buffers to the device limit
The batch split at a fixed 6M-vertex cap that assumed 40 B/vertex, but
MeshVertex grew to 52 B when the double-single `position_low` field was
added — so a full chunk was 6M x 52 = 312 MB, past wgpu's 256 MB
max_buffer_size, and Device::create_buffer panicked on large models.

Derive the caps from device.limits().max_buffer_size and the real vertex
size, bound both the vertex buffer and the (fatter) wire-index buffer,
and split any single mesh too large for one chunk into triangle-soup
sub-chunks so no batch buffer can exceed the device limit.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 10:20:24 +03:00
Hakan SevenandClaude Opus 4.8 7185f6e4fa fix(render): draw 3D surface fills at their real depth, not draw order
PolyfaceMesh / PolygonMesh / SubDMesh fills and 3DFACE quads were
classified as 2D fills and given the draw-order z-bias, so they were
pulled toward the camera and drew over solids. The tessellator emits a
mesh's edges and its fill as separate WireModels (points-only vs
fill-only), so the old `!points.is_empty()` discriminator misrouted the
fill. Classify a fill as a 3D surface by its double-single low residual
(`fill_tris_low`) instead, and drop the draw-order bias from 3DFACE
quads. 2D annotation fills (text greek, MultiLeader / dimension
backgrounds) leave `fill_tris_low` empty and keep ordering by rank.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 01:23:27 +03:00
Hakan SevenandClaude Opus 4.8 a994a148fe perf(render): batch solid meshes, fix per-pick freezes on heavy 3D models
Opening / navigating a heavy 3D DWG (e.g. a BIM model with ~10k ACIS solids)
was effectively frozen. Several independent costs, all addressed here:

- Face3D edge/fill buffers were re-uploaded on every camera move (keyed on
  camera_generation). They are world-space and selection-independent, so gate
  them on (geometry_epoch, fill_mode) instead — a pan no longer re-walks every
  wire.
- Solid meshes were drawn one buffer + one draw call per solid (~10k draws/
  frame), which strangled the GPU front end. Concatenate every solid's LOD0
  into a few large buffers (split only to stay under the 256 MB buffer cap) and
  draw the whole set in a handful of calls. Built once per geometry epoch, so
  selection / hover never re-pack it.
- Selection / hover highlight is now a small tinted overlay of just the picked
  solids, drawn last with an Always-depth pipeline so it shows on top even when
  the solid is occluded — instead of re-tinting and re-uploading all meshes.
- Hover / pick hit-testing scanned every triangle of every solid each move
  (seconds on a heavy model). Add a 3D-AABB screen-rect broad phase
  (MeshLodSet now carries z_aabb) and reuse the renderer's cached expanded mesh
  set, so picking is O(solids) cheap projections plus the few solids actually
  under the cursor.
- Block-internal hatch hit-testing exploded every INSERT on every hover.
  Cache the result per geometry epoch and skip blocks that contain no hatch.
- Curved-surface (cone/sphere/torus) tessellation tolerance is now radius-
  relative (CURVE_REL_TOL) so a cylinder's facet count is size-independent and
  matches the circle/arc wire tessellation, instead of exploding on large radii.

Also adds docs/tessellation.md mapping every EntityType to its tessellation
path (truck B-rep vs truck curve topology vs direct).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:46:29 +03:00
Hakan SevenandClaude Opus 4.8 495b13ec4f refactor(viewport): migrate model-space tiling to iced pane_grid
Replace the custom model_tiles tiling with iced 0.14 pane_grid as the layout
source of truth, while paper-space floating viewports are left untouched (they
overlap/free-place, which pane_grid cannot model).

Each Model pane is its own shader widget rendering its camera into the pane
bounds; the renderer Primitive carries a base_slot so several shader widgets
share the type-keyed MultiPipeline storage on disjoint slots. A second pane_grid
of transparent mouse_areas (plus on_resize) layers above the crosshair overlay
to receive input — the overlay's Hidden cursor interaction otherwise levitates
the cursor and starves any layer beneath it. The active pane follows the cursor
(hover-activate), and grid / crosshair / UCS icon render per pane while the
ViewCube and render-mode picker stay on the active pane only.

Split / close / VPORTS presets and the VPort table round-trip now drive
pane_grid (guillotine reconstruction on load). Adds drag-to-swap via a controls
bar handle with a ghost-card preview, visible pane dividers from the pane_grid
split regions, a default model background of RGB(33,40,48), and removes all the
superseded custom-tile code.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 11:50:11 +03:00
Hakan SevenandClaude Opus 4.8 8bdf5228d2 refactor(viewcube): shrink cube 30%, compact controls, centre UCS picker
Reduce the ViewCube 30% (VIEWCUBE_PX 120→84) and tighten the nav region
(NAV_INSET_F 2.2→2.0) so the home / roll / nudge controls hug the cube in the
corner instead of floating in a large dead area, with sizes scaled to keep them
from overlapping. Bump the home and roll buttons back up slightly, shift the
UCS picker down below the region, and give it a fixed width so it centres
exactly under the cube centre.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 01:56:17 +03:00
Hakan SevenandClaude Opus 4.8 edcf24e324 feat(ucs): interactive UCS icon with origin-snap grips, world-fixed compass
The UCS icon now anchors to the projected UCS origin when it is on-screen
(falling back to the bottom-left corner), and becomes a selectable object:
hover highlights it, a click shows draggable grips, and dragging the origin
grip relocates the UCS while the X/Y tip grips rotate it. Drags snap to
geometry/grid through the existing snapper, and the new UCS is persisted on
release. The icon stays selectable while parked in the corner.

Also make the ViewCube compass (N/E/S/W) world-fixed: the cube body still
reorients with the active UCS, but the cardinals project through a camera-only
rotation and snap in world frame, so the directions no longer spin with the UCS.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 01:12:45 +03:00
Hakan SevenandClaude Opus 4.8 169a8cde3f chore: clear dead-code and unused-import warnings
Remove the unused `crate::par::prelude` glob import in wire_gpu.rs and
the never-called `viewport_at_paper_point` method; fold its doc into the
screen-space twin that is actually used so no intra-doc link dangles.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 18:04:52 +03:00
Hakan SevenandClaude Opus 4.8 0317dad14e perf(render): hoist per-wire constants to a storage buffer (native)
Every wire segment is one instance, and each carried the wire's color,
line-weight, dash pattern and draw-depth (~44 B) inline — re-fetched per
segment even though they're constant along the wire. On native, move
those into a per-wire `WireConst` storage buffer (group 1) indexed by a
`wire_id` on the instance. The instance drops from 104 B to 60 B (~42 %)
across every entity's wires, and the shared constants are fetched once
per wire (cached) instead of once per segment — less vertex-attribute
bandwidth and less GPU upload on rebuilds.

WebGL2 has no vertex-stage storage buffers, so the wasm build keeps the
original self-contained fat instance and wire.wgsl; native uses the new
slim instance, wire_indexed.wgsl, and a group-1 bind group built per
batch in WireGpu::from_run / from_batch. Visual output is unchanged
(colors are now full f32 rather than 8-bit, if anything more precise).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 10:39:23 +03:00
Hakan SevenandClaude Opus 4.8 34e750407b fix(render): draw command-preview wires always on top
Preview / interim / grip-drag overlay wires were drawn in the main wire
pass with the depth-tested wire_pipeline (depth_compare=LessEqual), so a
live command preview could be hidden behind occluding geometry — a 3D
solid, or 2D geometry drawn in front of it.

Draw them with the existing wire_xray_pipeline instead (depth_compare=
Always, no depth write), the same always-on-top path selection highlights
already use. Previews are now never occluded, for every command.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 07:29:28 +03:00
Hakan Seven e9cb9af924 Merge PR #180: TTF solid glyph fills, font fallback, Z-order fix 2026-06-26 00:20:59 +03:00
Hakan SevenandClaude Opus 4.8 f00c48bebb feat(viewcube): add compass ring, cardinals, and nav controls
Build the ViewCube out into a full navigation widget:

- Compass ring painted in the cube's ground plane (z = base) so it
  reads as a ground compass and foreshortens with the view.
- N/E/S/W cardinals painted flat onto the ring band; clicking one snaps
  to that side elevation. Added S/W glyphs to the bitmap atlas.
- Home button (top-down view), roll arrows (90° roll about the view
  axis), and four nudge triangles (90° tip/spin) as SVG iced controls
  laid out around the cube. New home.svg.
- WCS / named-UCS selector under the cube, switching the active UCS.

The cube hit area now shares one stack with the control buttons, so a
click on a button is caught by the button while a click on the cube
falls through. Face clicks snap to the highlighted region rather than a
cursor position the viewport's move handler can overwrite.

Camera gains home_view / roll_by / nudge_90; floating viewports drive
the same ops through mutate_active_viewport_camera, which re-encodes the
result to the stored view direction + twist.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 23:46:43 +03:00
Hakan SevenandClaude Opus 4.8 6c4b981424 feat(viewcube): paint face names onto the cube surface
Lay each label's glyph quads out in the face's own local plane and
project the corners individually, instead of drawing screen-aligned
billboard text at the projected face centre. The letters now skew with
perspective and sit on the surface as the cube rotates.

Per-face (u, v) basis vectors are chosen so u x v equals the outward
normal, which keeps the text from ever reading mirrored.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 22:40:17 +03:00
Karim Jerbi 627761b629 fix(text): render TTF glyphs as solid fills, fix font fallback, correct Z-order
Text elements previously rendered as hollow wireframe outlines and sometimes
fell back to the wrong system font due to case-sensitive exact matching. Also,
the viewport background grid drew on top of solid entities.

Case-insensitive and partial-name fallback matching (e.g., mapping "arialn"
to "Arial Narrow") is now added to `sysfont::canonical_family_name`, ensuring
text resolves to the correct installed system font.

TrueType font contours are now triangulated into solid fills (`fill_tris`)
using `lyon_tessellation` in `ttf_glyph.rs`. To prevent the GPU from misclassifying
this 2D text as a 3D mesh (which broke flat shading), text tessellation splits
the output into separate outline and fill `WireModel`s.

The background grid is extracted from `SelectionCanvas` into `GridCanvas` and
moved beneath the 3D viewport to ensure proper layering. Finally, `DepthBiasState`
is enabled on all face/fill pipelines to apply a polygon offset, perfectly
resolving Z-fighting between coplanar wireframe lines and solid faces.
2026-06-25 18:17:01 +01:00
Hakan SevenandClaude Opus 4.8 04c93df2c4 refactor: remove the legacy full view_proj (CPU cull now relative-to-eye)
The GPU draws exclusively via the rotation-only `view_rot` + eye double-
single; the full view-projection survived only to project AABBs in the
CPU frustum-cull / scissor / LOD passes. Migrate those to the same
relative-to-eye math (project `point - eye` through `view_rot`) — pixel-
equivalent but precise at UTM scale — then drop the dead uniform:

- pipeline cull/scissor/LOD helpers take (view_rot, eye) instead of the
  full view_proj; render.rs reconstructs eye from the uniform's
  eye_high/eye_low and passes the cropped view_rot.
- Remove the `view_proj` field from the shared Uniforms struct and all six
  shader structs that only declared it for layout (none read it), and
  delete Camera::view_proj(). The struct shrinks by one mat4 on both
  sides, keeping the Rust / WGSL layouts in lock-step.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 20:14:41 +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 65413a2c2a refactor(render): drop dead camera_pos from the shared uniform
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]>
2026-06-23 18:28:52 +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 bd12cc2f5a feat(render): double-single corners for the image pipeline
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]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.7 0f7920dc2c feat(render): double-single anchors for both hatch pipelines
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]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.7 6a74f12a92 feat(render): double-single vertex positions for the wire pipeline
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]>
2026-06-23 18:28:52 +03:00
Hakan SevenandClaude Opus 4.8 8f16ba1ac2 feat(render): add relative-to-eye uniform fields (inert)
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]>
2026-06-23 18:28:52 +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 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 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 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 14e85171b3 perf(hover): stop scanning every wire on hover for large drawings
Two per-hover O(N) costs on big drawings, both keyed off the cursor:

- The selection/hover xray overlay rebuilt by scanning ALL wires and
  string-parsing each `name` into a handle on every hover change. Index
  handle → wire slots once per wire upload instead, so the overlay gathers
  only the highlighted entity's wires (O(highlighted)).

- click_hit (runs on every mouse move) projected every vertex of every
  wire to screen. In the flat top-down ortho view it now pre-rejects a
  wire by its world-space AABB (four corners projected, so a Z-rotated
  plan view is still correct) and only projects points for wires actually
  near the cursor. Tilted/orbit views fall back to the full test, so no
  pick is ever missed; covered by a unit test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-10 21:01:12 +03:00
Hakan SevenandClaude Opus 4.8 6b4272521c fix(hatch): deselect refresh, boundary offset, fill-click
Three hatch fixes:

- Selected hatches are tinted, but the hatch model cache and GPU upload
  were keyed only on geometry_epoch, so Esc cleared the selection set
  without repainting — the hatch stayed blue (#71). Key both on
  selection_generation too.

- add_hatch stored the boundary entity in local render space, so the
  boundary wire re-projected `world_offset` away from the fill on
  offset (large-coordinate) drawings. Build it in WCS.

- Clicking a hatch only selected it on its boundary wire, not its fill.
  Two causes: visible_hatches_for_click used a narrower block-visibility
  test than the render path (fill drawn but not clickable), and
  point_in_polygon closed every NaN sub-path — degrading the explicit
  2-point edge encoding of a boundary into zero-area loops. Mirror the
  render's visibility test and only auto-close sub-paths of >=3 verts.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-09 22:47:48 +03:00
Hakan SevenandClaude Opus 4.8 e37dfa5890 perf(scene): draw preview/grip overlay from a separate GPU buffer
Command-preview / interim / grip-drag wires were appended into the main
wire buffer (all_wires), so every drag frame re-packed and re-uploaded the
entire batched base set — O(N) per move even though only the small overlay
changed.

Split them out: build_primitive keeps `wires` as the cached base only and
passes the overlay as a separate `preview_wires` Arc. The base keeps the
stable tile content id (no per-frame nonce), so its buffer stays resident
and is not re-uploaded during a drag. The overlay uploads to its own small
gpu_preview_wires buffer each frame and draws on top in the wire pass
(same pipeline, same order/depth). The dragged entity still renders
highlighted via its selected-path tessellation.

With the no-re-tess changes, a grip / command drag now costs a single
small overlay upload per move and nothing else.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-05 10:57:38 +03:00
Hakan SevenandClaude Opus 4.8 ff9f49511b perf(scene): decouple selection from wire tessellation
Picking an entity called bump_geometry, which invalidated the wire cache
and re-tessellated the entire model just to repaint one entity — 30ms to
400ms on a large drawing (visible as a tess-ms spike on the PERF HUD).

Selection highlight is no longer baked into tessellation. wires_for_block_
culled now feeds tessellate_entity an empty selection set, so wires are
always base-coloured and the wire cache is selection-independent. The
highlight is applied in the GPU xray overlay: prepare filters the cached
wires by the live `selected ∪ hover` handle set, recolours them to
WireModel::SELECTED, and batches them — rebuilt only when a new
selection_generation (or the underlying wire content) changes.

Pure select / deselect / hover paths now call bump_selection instead of
bump_geometry, so a pick refreshes just the overlay — no re-tessellation,
no main wire re-upload (geometry_epoch and wire_content_id are unchanged).
Geometry edits, undo and file-open still bump_geometry and rebuild
everything. Grips/properties refresh through their existing app paths.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-05 10:04:22 +03:00
Hakan SevenandClaude Opus 4.8 0d18e0d1bf perf(scene): batch the selection-overlay wire pass too
Follow-up to 3.3. The selection xray pass still built one buffer + one
draw call per selected wire, so select-all over a large drawing issued a
draw per entity. That pass applies neither scissor nor mesh-edge skip, so
all selected wires merge into one order-preserving run via from_run.
Removes the now-dead WireGpu::new / build per-wire constructors.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-05 09:44:12 +03:00
Hakan SevenandClaude Opus 4.8 c6e17deb7e perf(scene): batch the wire pass into per-run draw calls
Phase 3.3. upload_wires built one GPU instance buffer + one draw call per
WireModel — tens of thousands on a large drawing, a major per-frame CPU /
driver cost. Merge maximal runs of consecutive wires that share scissor +
mesh-edge state into one concatenated instance buffer each (WireGpu::
from_run); the existing draw loop then issues a single draw per run. A 2D
model (uniform None scissor, no mesh edges) collapses to one buffer + one
draw.

Runs are consecutive, never globally regrouped, so the sorted wire order
is preserved exactly — depth-biased overlap and alpha blending both
resolve in submission order, so the image is unchanged. from_run
guarantees instance order (index-ordered collect + sequential flatten).
The WireInstance layout, shader, scissor projection and draw loop are
untouched; only buffer packing changed. Selection overlay stays per-wire.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-05 09:35:18 +03:00
Hakan SevenandClaude Opus 4.8 0f40cfa054 perf(scene): skip wire GPU re-upload when only the camera moved
Phase 3.2 (wire pan path). Wire vertex buffers are world-space, so a
camera move changes only the view_proj uniform (uploaded every frame),
never the wire data — yet upload_wires re-ran on every camera_generation
tick, re-packing vertices and re-writing the GPU buffer on each pan.

Stamp every Model-tile re-tessellation with a monotonic content id
(WIRE_CONTENT_GEN), reused when a pan reuses the tessellation. The
pipeline records the resident buffer's id and skips upload_wires when the
incoming id matches. The gate is pulled out of the (epoch, camera_gen)
block so a preview/interim wire change still uploads even with a static
camera; non-tile paths and overlay frames force a fresh id, preserving
current behaviour. Monotonic id sidesteps the ABA hazard a freed/realloc'd
Arc pointer would carry.

Pairs with the pan tessellation reuse: a pan now skips both the CPU
re-tessellation and the GPU wire upload.

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