The single-batch arena required a set with no mesh/solid edges, so any drawing
with a 3D solid or mesh (this one: 1693 mesh-edge wires of 12200) disqualified the
WHOLE set — every edit fell back to build_wire_buffers re-uploading ~265 MB
(4.4M instances), stalling ~1s per edit on a shared-memory GPU.
Split the resident set into a regular-2D arena and a mesh-edge arena (the wires
that need the draw loops mesh-edge skip/black treatment), one WireArena each,
passed as &[&WireModel] subsets (no clone). Both patch incrementally; the mesh
arena carries is_3d_mesh_edge so the existing draw loop handles it. A changed
handle absent from an arenas subset is a no-op for it, so each edit touches only
the batch it belongs to. Result: the wire block drops from ~300ms + a 265 MB GPU
upload to ~2ms.
Two correctness points folded in from adversarial review:
- mesh-edge wires are stamped draw_depth 0 (matching WireGpu::from_run) so their
hidden back edges dont peek through the shaded fill; the mesh arena also always
bails on append (all its wires share depth 0, so relocation would reorder
coincident edges).
- buffers are capped at wgpus 256 MB limit — a batch larger than one buffer
returns None so the caller chunks it via the batched path (fixes a create_buffer
panic on the 355 MB regular batch).
Still behind OCS_WIRE_GPU_PATCH (default off). OCS_PERF prints the per-edit wire
timing + outcome.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a geometry edit the resident set gets a fresh content id, so the batched wire
path (build_wire_buffers) re-emits and re-uploads EVERY wire instance — the whole
buffer even when one line moved. Behind the OCS_WIRE_GPU_PATCH flag (default off,
native only), add a persistent per-entity instance arena: one long-lived
VERTEX|COPY_DST instance buffer + its WireConst storage, with a per-entity slab
table. An in-place edit (move / rotate / scale / colour — same segment count)
becomes a queue.write_buffer of just that entity`s slab.
Scope is deliberately IN-PLACE ONLY. draw_depth_map normalises each entity`s
draw-order z-bias by the block`s entity count, so an Add or Remove re-scales EVERY
entity`s z-bias — which a per-slab patch can`t honour — and a segment-count change
relocates the entity. patch() returns false for any of those (and for mesh-edge /
scissored sets), and the caller falls back to the identical full batched rebuild,
so correctness never rides on the fast path. An adversarial review of an earlier
append/tombstone design surfaced exactly these draw-order hazards; restricting to
in-place removes them while keeping the common interactive-edit win.
The Scene hands the changed handles to the arena via model_wire_gpu_patch,
recorded by try_resident_patch for the Model block and matched by content id in
prepare(); scissored paper viewports and mixed 2D/3D sets keep the batched path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each pipeline slot owned its own gpu_wires: every paper viewport and every
Model tile rebuilt an identical resident wire set into its own GPU vertex
buffers and its own handle index. With N content viewports drawing one shared
tessellation that is N copies of the (potentially huge) wire buffers in VRAM
plus N batch+index builds on every content change — the issue-131 memory
exposure multiplied by the viewport count.
A wgpu::Buffer is internally ref-counted, so sharing a WireGpu by Arc shares
its GPU allocation. Make Pipeline.gpu_wires an Arc<Vec<WireGpu>> (and
wire_handle_index an Arc<map>), turn upload_wires into build_wire_buffers(&self)
that returns those Arcs, and add MultiPipeline.wire_buffer_cache keyed by
wire_content_id. prepare() builds an entry once on a cache miss and hands Arc
clones to every slot with that id — so N viewports (and, since one MultiPipeline
backs all panes, N panes) upload the wire vertices exactly once between them.
Camera uniforms and scissor stay per-slot, so the draw still issues one call per
viewport with the right view; only the geometry is deduplicated. The shared Vec
is never mutated in place — only reassigned on a content change — so the sharing
is sound.
The cache drops entries no slot still references (Arc::strong_count == 1) once it
passes a small bound, so a live buffer (held by any pane) is never evicted.
Model multi-tile gets the same dedup for free, and two panes showing the same
model now share buffers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WebGL2 has no vertex/fragment storage buffers, so the batched storage-buffer
hatch renderer (hatch_gpu) is disabled on wasm and real hatch fills never
rendered on the web build — the pick highlighted the boundary and the command
then appeared to cancel (issue #204).
Add a wasm-only per-hatch renderer (hatch_web_gpu + hatch_web.wgsl) that packs
the variable-length boundary / family / dash arrays into one RGBA32F data
texture read with textureLoad, instead of the fixed-size uniforms of the
wipeout path. This removes the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS
(1024) / MAX_DASHES (128) caps, so every hatch — solid, gradient, and
arbitrarily complex line patterns — renders on the web. The fill algorithm
(in_polygon, per-family line/dash/dot, gradient, LOD, double-single vertex
transform) is identical to the desktop path; only the array ACCESS changes.
Pattern phase is anchored at world_origin with the boundary stored raw, and
scale / gradient-range clamped, to match the desktop batched renderer exactly.
Native (hatch_gpu + hatch.wgsl) and the wipeout mask renderer are untouched;
the new module and pipeline are gated on target_arch = "wasm32".
Also silence the resulting (and pre-existing) wasm-only dead-code warnings for
native-only paths so both the native and web builds compile clean.
Closes#204.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pipeline slots are addressed by list index, but the renderer drops
off-canvas viewports from the list, so a slot can be reused by a
different viewport across frames — e.g. when the first paper viewport
scrolls off the canvas the second slides into its slot and inherits its
stale, differently frustum-culled GPU buffers. The text upload is gated
on a geometry-epoch change (unaffected by the index shift), so the
survivor rendered the gone viewport's text and its own vanished.
Give each viewport a stable identity, remember which one last used a
slot, and reset all cache keys when the occupant changes so wires, text,
hatches, meshes and Face3D re-upload for the new viewport. No cost in the
steady state — the reset fires only when a viewport enters/leaves view.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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.
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.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.
Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refactor the per-viewport blit so a content viewport whose screen rect
extends off the canvas composites the correct slice of its MSAA target
to the visible portion of the surface, instead of being slammed against
the canvas corner by an `(f32 as u32)` saturation on the negative
offset.
- blit.wgsl + Pipeline gain a small uniform buffer (`uv_offset` /
`uv_scale`). `Primitive::prepare` computes the per-viewport crop from
`screen_rect`'s on-canvas fraction and writes it via the new
`upload_blit_uv` helper while it still has the `Queue`.
- `Pipeline::render` now takes `vp_size` (full vp — MSAA / camera size)
and `surface_dest` (visible intersection — blit destination) and
reads the crop from the uniform; passes that don't have a non-empty
surface intersection are skipped.
- `Primitive::render` clips the full vp rect against the surface clip
in `i32` and skips empty intersections. The earlier "skip GPU for
inactive off-canvas viewports" workaround in `active_viewports` is
no longer needed.
- ViewCube draws only when its hosting vp is fully on-canvas (its own
pipeline writes directly to the surface and would distort under a
clamped `set_viewport`).
With the GPU path correct for every vp position, drop
`viewport_content_wires` from `paper_canvas_wires` — the inactive
viewport CPU projection was only there to mask the blit bug and was
duplicating the unified shader's output. The hit-test / snap path
(`entity_wires_arc`, `wires_for_block`) still calls
`viewport_content_wires` directly, so picking through inactive
viewports keeps working.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Geometry passes draw with standard `SrcAlpha / 1-SrcAlpha` into a
transparent MSAA target, so AA-edge fragments sit as `(rgb * a, a)` in
the resolve texture. The previous straight-alpha blit then multiplied
those by `a` a second time, darkening thin lines and curves over the
transparent background. `PREMULTIPLIED_ALPHA_BLENDING` matches what is
actually in the resolve texture and renders edges at full intensity
while still leaving fully-transparent pixels untouched on the surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapse the paper-space rendering path into the same shader widget the
model layout uses. The PaperCanvas widget draws the paper sheet, paper
entities and viewport borders underneath; the unified shader paints all
content viewports on top, scissored to each viewport's rect.
- Drop PaperViewportPane / PaperViewportPipeline newtype /
PaperViewportPrimitive, ViewportPaneMode::Paper, ViewportPane::paper,
build_primitive and build_viewport_primitive. Scene exposes only
build_viewports.
- view.rs: the paper branch stacks PaperCanvas + the unified shader
instead of the old paper_canvas_view function. The active paper
viewport's blue border, render-mode picker and ViewCube hit-area are
layered in the same viewport_stack as the model layout.
- Transparent shader clear (alpha=0) + alpha-blended blit pipeline so
the underlying widget (container bg in model, PaperCanvas sheet in
paper) shows through everywhere the shader has not painted geometry —
including inside content viewport rectangles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Birleşik viewport mimarisinin yapısal temeli. Primitive artık tek kamera/
geometri yerine viewport başına bir ViewportData listesi taşıyor; yeni
MultiPipeline her viewport için bir iç Pipeline instance tutuyor.
prepare/render viewport listesi üzerinde dönüp her iç-pipeline'ı kendi
(normalized) ekran rect'ine çiziyor.
Mevcut Pipeline kodu (upload/LOD/render/blit) HİÇ değişmedi — sadece
viewport başına bir kez çalışıyor. build_primitive ve
build_viewport_primitive tek-elemanlı liste (FULL_VIEWPORT_RECT)
ürettiği için render davranışı tek-viewport ile birebir aynı; çoklu
viewport sonraki adımda (view.rs tek widget + active_viewports) aktive
edilecek.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PolyfaceMesh / PolygonMesh için outline edges WireModel.points üzerinden
gpu_wires'a giriyor; bu nedenle FlatShaded / GouraudShaded modlarında
shaded yüzeyin üstünde görünmeye devam ediyordu. WireGpu artık kaynak
WireModel'da fill_tris doluysa 'is_3d_mesh_edge' bayrağını set ediyor;
wire pass'i show_3d_edges=false iken bu instance'ları atlıyor.
*WithEdges, Wireframe ve HiddenLine modları show_3d_edges=true ile
geliyor — mesh kenarları orada görünür kalır.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3D yüzeyleri (Solid3D mesh, 3DFACE quad, PolyfaceMesh/PolygonMesh face
tris) HiddenLine modunda renk yazmadan z-buffer'a yazıyor; ardından
kenarlar wireframe pipeline ile çizilince LessEqual depth testi
arkadaki segmentleri eliyor. 2D fill'ler (text greek, MultiLeader
arka planı) ayrı bir vertex buffer'a ayrıldı ve her modda renkli
çizilmeye devam ediyor.
- Face3DGpu: vertex_buffer artık 3D / 2D olarak ikiye ayrıldı
- mesh_depth_pipeline + face3d_depth_pipeline (write_mask=empty)
- RenderModeFlags.hidden_line + Primitive.hidden_line plumbing
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wireframe 2D/3D modlarında Solid3D meshlerini edge-only çizmek için
LineList topolojili bir mesh wireframe pipeline ve her üçgenin
(a,b),(b,c),(c,a) kenarlarına genişletilmiş ayrı bir index buffer
eklendi. PolyfaceMesh / PolygonMesh face fill'leri (fill_tris + dolu
points) wireframe'de gizleniyor; text greek / MultiLeader gibi 2D
fill'ler (points boş) etkilenmiyor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the binary Wireframe/Solid toggle with a 7-mode dropdown
that maps onto acadrust's `ViewportRenderMode` enum: Wireframe 2D /
Wireframe 3D / Hidden Line / Flat Shaded / Gouraud Shaded / Flat
Shaded + Edges / Gouraud Shaded + Edges. Per-tab state lives on
`DocumentTab::render_mode`; the old `SetWireframe` message forwards
to `SetRenderMode` so the ribbon + WIREFRAME / SOLID command line
keep working.
`scene::render::render_mode_flags()` translates the mode into three
pipeline-facing booleans (`face3d_fill`, `mesh_fill`,
`show_3d_edges`); `mesh_fill` / `show_3d_edges` are stubs that the
upcoming phases will consume. `Primitive` carries the derived
`view_wireframe` plus the two new fields through to `prepare()`.
`Pipeline::upload_face3d` takes a `wireframe_only` flag that drops
3DFACE quad fills (built from `key_vertices`) while keeping the
`all_wires.fill_tris` channel — that channel carries text-LOD greek
rectangles and PolyfaceMesh face triangles which must stay visible
in wireframe mode. 3DFACE edges stay on regardless of mode.
Solid3D mesh wireframe rendering, Hidden Line z-prepass, and Flat /
Gouraud shader uniforms come in phases 1–4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 3 of the wire pipeline cold-open optimization plan — the biggest
single lever.
Each segment used to upload as 6 vertices × 96 B = 576 B with the segment
payload (pos_a/pos_b/color/half_width/distance/pattern_length/pat0/pat1)
duplicated across all six corners of the quad. Wire pipeline now uploads
one INSTANCE per segment (88 B) and the vertex shader derives `which_end`
and `side` from `@builtin(vertex_index)` against a 6-entry corner table.
Net effect on cold open: ~6.5× less wire vertex memory and PCIe upload,
plus a smaller Vec to allocate and copy on the CPU side.
Wire instance attributes also collapse distance_a / distance_b into a
single mix() inside the vertex shader, so per-segment distance still
interpolates correctly for the GPU-side linetype dash test.
Touches the wire, wire_xray, and face3d-edges draw passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The batched hatch pipeline has soaked through visibility + LOD; it's
correct on every file tested (Floor plan.dxf, UTM site plans, the
non-UTM regression case). Time to remove the parallel per-hatch
plumbing that's been sitting empty since step 3:
- `gpu_hatches: Vec<HatchGpu>` — never populated since
`upload_hatches` switched to the batched builder; dropped.
- `hatch_pixel_scissors`, `hatch_skip_flags` — same story; dropped.
- `compute_hatch_scissors` — no longer has a consumer; dropped, with
the render-time call removed from `Primitive::prepare`.
- Hatch render pass — the dead `else if` branch over `gpu_hatches`
is gone; only the single batched `pass.draw(0..vertex_count, 0..1)`
remains.
- `compute_hatch_lod` — no fallback path; if `gpu_hatch_batched` is
`None` (empty hatch list) it just returns early.
`HatchGpu` itself stays — the wipeout pipeline still uses it. So do
`hatch_pipeline` / `hatch_bgl1`, kept around for that path.
`#[allow(dead_code)]` on the four storage-buffer fields in
`HatchBatchedGpu` — they exist only to keep the bind group's
referenced resources alive; nothing reads them directly.
Known gap left as a follow-up: per-hatch viewport scissor for paper-
space MSPACE viewports isn't ported to the batched path yet. Hatches
inside a paper-space viewport can render past the viewport border
until that's added. Hasn't surfaced as a visible regression in the
test files so far.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>