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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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 <[email protected]>
Add a transparency_enable uniform read by the wire shader: when the new
TPY pill is off, every line is forced opaque; when on, baked entity
transparency shows through. Toggling is a uniform write, no retessellate
(same mechanism as the lineweight-display toggle). Pill is toggleable
from the status-bar customization menu.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The rendered MText preview is now an editable surface: it shows a text caret,
click to position it between glyphs, and type/Backspace/Delete/Enter to edit.
Left/Right arrows move the caret; drag-selection + toolbar formatting still
work and typing replaces a selection.
Keystrokes are routed by mapping visible-character offsets to byte spans in
the value (raw newlines count as paragraph breaks, matching the layout). While
the preview is open the command line releases its on_input so typed glyphs
reach the preview instead, and the caret starts at the end of the text so
typing works without a click first.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Koordinat girişi artık göreli/mutlak ayrımını destekliyor:
- `@x,y[,z]` → son girilen noktaya göreli (UCS eksenlerinde döndürülür)
- `#x,y[,z]` → her zaman mutlak (DYN durumundan bağımsız)
- ön ek yok → DYN açıksa göreli, kapalıysa mutlak
İlk noktada referans olmadığından göreli giriş mutlağa düşer. Çözülen
WCS noktası last_point olarak saklanıyor, böylece ardışık göreli
girişler zincirleniyor.
parse_coord artık (Vec3, CoordKind) döndürüyor; relative offset için
ucs_rotate_vec eklendi (origin translation olmadan UCS rotasyonu).
UCS ORIGIN komutu prefix'i yok sayıp mutlak kalıyor.
Ayrıca ViewCube snap refactor'undan kalan kullanılmayan kod temizlendi:
angle_close, active_viewport_yaw_pitch, CubeRegion::snap_angles.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
ViewCube tıklaması artık sabit (yaw, pitch) tablosu yerine bir yön
vektörüne (eye direction) snap ediyor. Up vektörü şöyle seçiliyor:
1. Mevcut up alın.
2. ±X / ±Y / ±Z arasından (yeni gaze ile (anti-)paralel olmayanlar)
mevcut up'a en yüksek dot-product veren cardinal axis'i seç.
3. Bu axis'i yeni gaze yönüne dik plane'e projekte et ve up olarak
kullan.
Sonuç:
- Küçük tilt'ler ilgili canonical axis'e snap olarak temizleniyor.
- TOP yukarıda iken TOP-LEFT edge tıklayınca TOP yukarıda kalıyor;
cube edge'e dönüyor ama kullanıcının "yukarı" hissi korunuyor.
- Upside-down orbit edilmişse -Z up olarak seçilir, snap baş aşağı
yapılandırmayı bozmuyor.
CubeRegion::snap_angles ve Camera::snap_to_angles yerini
snap_direction / snap_to_direction aldı.
snap_active_viewport_to_direction viewport'un view_direction'ını
günceller, twist_angle'a dokunmaz.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Two behaviour changes that bring the command line in line with
established CAD workflow:
1. **Always-on keyboard capture.** The input was blurred whenever no
command was running, so typed letters went nowhere until the user
clicked into the field. Replace the blur with a re-focus on every
command lifecycle transition (cmd_result.rs) and on viewport / tab
/ ribbon clicks (update.rs), focus the input at startup, and add
a global key subscription that catches any unhandled character key
or backspace and routes it into the buffer via two new messages
(CommandAppendChar / CommandBackspace) — so the command line keeps
accumulating keystrokes even if focus parks somewhere not
focusable. Removes the unused blur_cmd_input helper.
2. **Space submits.** Typing a space at the end of the input now
submits the buffer the same way Enter does. Only exception: an
active command that's currently collecting free-form prose
(TEXT / MTEXT / DDEDIT body, ATTDEF prompt / default value,
INSERT attribute value, ATTEDIT cell). Those advertise the new
`CadCommand::wants_text_with_spaces()` flag so the Space character
reaches the buffer as a literal.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Joining the OpenAEC Foundation (open-aec.com). Crate/struct become
OpenCADStudio, user-facing strings use the spaced "Open CAD Studio"
form, repo URLs point to HakanSeven12/open-cad-studio. Logo (window
icon + assets/logo.svg) redrawn with OCS initials. Drop examples/
binaries — unused.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Step 4 (final) of the wire pipeline cold-open optimization plan. Color
shrinks from `[f32; 4]` (16 B) to `[u8; 4]` (4 B) and the vertex attribute
format flips to `Unorm8x4`; the GPU still hands the shader a `vec4<f32>`
in [0, 1], so wire.wgsl needs no change. 8-bit per channel is below
display precision.
Net: 76 B per instance, ~14 % smaller than the Step-3 instance layout and
~7.6× smaller than the pre-Step-3 per-vertex format. Wire upload memory
and PCIe bandwidth on cold open scale down accordingly.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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 <[email protected]>
Step 2 of the wire pipeline cold-open optimization plan. Extracts the
per-wire vertex expansion into a free function and drives it from
`from_batch` via `par_iter().map(...).reduce(...)`. Per-wire emission is
already independent (no shared state, no order coupling — `block_cache`
groups wires by style upstream), so the parallel-then-concatenate shape
is correctness-preserving.
Cold-open wire upload now scales with available cores; a drawing with
many short wires shows the biggest win. Also collapses two near-identical
emission loops (`from_batch`, `build`) into one shared helper.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Step 1 of the wire pipeline cold-open optimization plan. Replaces
`create_buffer_init` (intermediate staging-buffer memcpy) with a
`mapped_at_creation` buffer the build path writes into directly.
Cuts one alloc + one memcpy per wire buffer; on cold open with hundreds
of MB of wire data this shows up immediately as lower peak memory and
faster upload.
Guarded against size-0 buffers by allocating a single-vertex placeholder
when the wire is empty (the renderer already skips draws on vertex_count
below the per-segment minimum).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
LWDISPLAY (Show Lineweight) flipping the toggle no longer triggers a full
retessellate. `render_style_for` always bakes the entity's resolved (layer-
inherited) lineweight, and the wire shader reads `Uniforms.lwdisplay_enable`
to either honour the per-vertex half_width or collapse the quad to a 1 px line.
Side effect: PDF export now uses the real lineweight regardless of LWDISPLAY,
matching AutoCAD's plot semantics (LWDISPLAY only affects screen render).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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 <[email protected]>
Adds a dedicated `visibility: array<u32>` storage buffer (binding 4)
alongside the existing batched hatch buffers. Vertex shader reads
`visibility[v.instance_index]` — 0 → emit an out-of-NDC clip
position so the GPU rasterizer clips the primitive before the
fragment stage runs.
`compute_hatch_lod` now drives this buffer instead of (or alongside,
for the per-hatch fallback) `hatch_skip_flags`. Walks a per-instance
CPU AABB mirror that the builder fills at upload time (`instance_aabbs`),
applies the same `aabb_below_pixel` (sub-pixel LOD) OR `aabb_offscreen`
(frustum cull) rule the legacy path used, and pushes the resulting
0/1 flags via `queue.write_buffer` on a separate 4 B-per-hatch buffer
— ~40 KB per pan tick for 10 k hatches, far cheaper than re-writing
the 112 B-per-instance data.
Step 5 will remove the legacy `gpu_hatches` Vec, the per-hatch draw
loop, and `hatch_skip_flags` once the batched path has soaked.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two issues in the step-3 batched path surfaced during testing:
1. WGSL rejects compile-time NaN literals (`0.0 / 0.0`), so the
visibility-skip path that emitted a NaN clip position failed
`Device::create_shader_module` validation. Replaced with an
out-of-frustum clip position `(2, 2, 2, 1)` — the GPU rasterizer
clips the primitive and the fragment shader never runs.
2. Pattern hatches rendered with the wrong spacing / direction. The
batched builder was rotating `perp_step` / `along_step` into the
global frame, but the per-hatch shader (`hatch.wgsl`) expects them
in QCAD PAT local-frame convention: `perp_step = family.dy`,
`along_step = family.dx`, with the cos_off/sin_off rotation applied
inside the shader. Now matches `build_family_batch` from
`hatch_gpu.rs` line-for-line. `line_width` also reset to 0 (the
shader picks 1 px from a screen-space derivative; the stored field
is unused).
3. Padding's `max_spacing` likewise dropped its bogus rotation and
now uses `|dy|` per family, matching the new perp_step convention.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Pipeline carries a new `hatch_batched_pipeline` + matching `bgl1`
alongside the existing per-hatch pipeline. `upload_hatches` now packs
the renderable hatch list into the single `HatchBatchedGpu` and
clears `gpu_hatches`; the draw loop prefers the batched path when
that resource is set, falling back to the per-hatch loop only when
build returned `None` (empty list).
One draw call replaces N — for a 100-hatch frame that's a 99-call
saving plus 99 fewer `set_bind_group(1, ...)` / `set_vertex_buffer`
pairs.
Caveats acknowledged for step 4 cleanup:
- `compute_hatch_lod` still writes into `hatch_skip_flags`, which the
batched path ignores. Step 4 will push the same flags into the
per-instance `visible` field on the GPU instance buffer so the
sub-pixel + frustum cull keeps working.
- Per-hatch viewport scissors (paper-space MSPACE) aren't ported yet;
the legacy per-hatch path still serves that case when the batched
build is skipped, but a model-space → paper-space transition keeps
the batched buffer intact and may render hatches outside the
intended viewport. Will be addressed alongside visibility.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Stand-alone module — no consumers yet. Packs an entire `&[HatchModel]`
into four storage buffers + a per-vertex (corner, instance_index)
stream:
- `InstanceBuffer` — `HatchInstance[]` (112 B each): color, mode,
gradient params, world_origin, local-space aabb, boundary range,
family range, visibility flag.
- `BoundaryBuffer` — flat `vec4<f32>[]` (NaN markers preserved as
per-path separators, just like the per-hatch shader expects).
- `FamilyBuffer` — flat `LineFamilyGpu[]` (matches the per-hatch
family layout one-for-one apart from `dash_offset` indexing into a
separate dash buffer).
- `DashBuffer` — flat `f32[]`.
The per-vertex buffer holds `6 × instance_count` records of
`(corner: u32, instance_index: u32)`. Step 2 will add the matching
WGSL shader that uses these to project a per-instance AABB quad and
run the in-polygon test against `BoundaryBuffer[offset..offset+count]`.
iced reports 8 storage buffers per stage, 128 MB binding cap, 256 MB
buffer cap — plenty for ~10 k hatches × 112 B + ~32 MB boundaries.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
`HatchGpu.world_aabb` was built by iterating `model.boundary` — but
those vertices are stored as f32 offsets from `model.world_origin`
(precision anchor for UTM-scale WCS). So the AABB was in
*origin-relative* space, not the local space `view_proj` projects
from. `aabb_offscreen` (Phase 2.3) then asked "is this rect near (0,0)
in NDC?" instead of "is this rect at the hatch's actual position?" —
which culled in-view hatches whenever the camera moved off the
origin and rendered out-of-view ones near it.
Sub-pixel LOD (`aabb_below_pixel`, Phase 3.3) survived because it
measures a projected diagonal, which is translation-invariant.
Fixes:
- Add `world_origin` back when building `HatchGpu.world_aabb`, so the
stored rect is in absolute local space.
- 25 % margin on the viewport rect in `aabb_offscreen`, matching the
wire path's `view_world_aabb` margin — keeps hatches at the edge
rendered while panning.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The Phase 2.1 step-4 attempt at culling hatches inside
`synced_hatch_models` was wrong: GPU hatch/wipeout buffers are
uploaded on geometry_epoch only (see render.rs — they're "static
buffers"), so any CPU-side cull at build time would freeze the
visible set at the geometry epoch boundary and never re-evaluate on
pan. Reverted that approach; the build-time function now returns the
full visible set again and the cache key is back to geometry_epoch.
The actual per-frame frustum cull moves into the existing draw-time
skip-flag machinery:
- `compute_hatch_lod` now also folds in `aabb_offscreen` (ORed with
the existing Phase 3.3 sub-pixel skip), so out-of-view hatches are
skipped at the draw call.
- New `compute_wipeout_lod` + `wipeout_skip_flags` mirror the hatch
path. The wipeout draw loop honors the flag.
- `Primitive::prepare` calls the new compute step alongside the rest.
Per-frame projection + skip is a few hundred microseconds on dense
docs; far cheaper than re-uploading GPU buffers per pan tick.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Roadmap calls for an Aabb3 octree, but H7CAD's mesh entities only
carry XY world_aabb (Phase 3.4 stored XY for the LOD selector and
never extended to Z) and the camera is orthographic top-down for
every realistic workflow. So Phase 2.2 here becomes a draw-side cull:
`compute_mesh_lod` now also populates a `mesh_visible: Vec<bool>`
flag using the same 4-corner projection it already does for LOD
picking, and the mesh render pass skips draws whose projected AABB
sits entirely outside the viewport rect.
Mesh quadtree-indexing is still in place from Phase 2.1 (Solid3D /
Region / Body are not in `is_unindexable_entity`, so they're inserted
into `entity_index`). When mesh entity counts grow large enough that
the upload-time scan dominates over per-frame draws, Phase 2.3 can
plug `query_rect` into `upload_meshes` too.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two precision problems were producing visibly wrong hatches on UTM-scale
(and similarly-offset) drawings even after world_offset was correct:
1. **Boundary tessellation** — `hatch_model_from_dxf`'s polyline-bulge
arc tessellation ran the whole arc-center computation in f32. With
vertices at WCS magnitude ~1e5 the f32 ULP is ~1 cm, which propagated
into the sampled arc points and showed up as visibly wavy / jagged
hatch boundaries on edges that crossed the long axis of the drawing.
Re-run the full bulge → arc math in f64; only cast at the to_xy
call that feeds the (also f64) boundary buffer.
2. **Pattern phase** — the per-fragment `perp / perp_step` modulo blew
precision when `xz` was at offset-rel WCS magnitude and perp_step
was sub-cm. Move the precision-critical bits out of f32 by keeping
each hatch's anchor in f64 on the model side: `HatchModel` now
carries a `world_origin: [f64; 2]` and the `boundary: [f32; 2]`
vertices are stored as small offsets from it. The GPU vertex shader
adds `origin` back inside the view_proj multiply for clip position
(sub-pixel-lossy at large hatches but invisible); the `xz` varying
that the fragment shader uses for in_polygon + pattern math stays
in hatch-local space at full f32 precision.
For pattern alignment across adjacent hatches with the same family,
the GPU pipeline snaps the anchor to the family's perp/along grid
in f64 before sending it down. Snap uses the same QCAD-PAT
convention (`fam.dy = perp_step`, `fam.dx = along_step`) the
shader's check_family uses — an earlier attempt used the world-frame
`(-dx*sin + dy*cos)` formula, which produced a meaningless grid and
no alignment.
Trade-off: pattern phase is "global modulo per-family-grid" rather
than tied to WCS (0,0). Same hatches still align with each other;
the absolute phase relative to the WCS origin may differ from the
pre-fix behaviour on small drawings.
Consumers that read `model.boundary` directly (hit_test, paper_canvas,
add_hatch round-trip) reconstruct WCS via `boundary[i] + world_origin`.
The hatch wgsl bind-group entry for binding 0 was bumped to
`VERTEX | FRAGMENT` because the vertex shader now reads `h.origin`.
The fragment-shader hatch LOD substitute + sub-pixel skip from e7675be
and the scissor plumbing from fbbac42 still work — those touched
different fields of the same struct.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Each item below was gated by #[allow(dead_code)] and has zero callers
across src/ — none of them are part of any public API consumed by
external crates (this is a binary, not a library):
* modules/insert/underlay_frames.rs + edit_aliases.rs — toolbar stubs
whose `tool()` registrars were never wired into the ribbon; the
parent mod.rs inlines its own include_bytes! for the SVGs, so the
files were orphan dead weight.
* ui/snap_popup.rs:snap_marker_color — never called.
* ui/ribbon/mod.rs:last_dropdown_cmd — never called.
* ui/scale_popup.rs:anno_scale_for_label — never called.
* ui/layers.rs:color_name_to_aci — inverse of aci_color_display
with zero call sites.
* linetypes.rs:name_list — never called.
* scene/pipeline/wire_gpu.rs:new_ghost — never called.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
ACIS SAT entities (Solid3D / Region / Body) were tessellated at one
fixed sampling density (CIRC_SEGS=48, GRID_U=32, GRID_V=16) regardless
of how far they were from the camera. Distant solids paid the full
triangle bill for fragments that mostly resolved to sub-pixel size.
Generate three pre-built LODs per entity at load time:
* LOD 0 (HIGH) — 48/32/16 segments. Use above 200 px projected diag.
* LOD 1 (MID) — 24/16/8. Use 50–200 px.
* LOD 2 (LOW) — 12/8/4. Use below 50 px.
Tessellation cost is paid 3× on load (cheap — typical files have few
solids). Render picks per-frame via `compute_mesh_lod` from each mesh's
`world_aabb` projected diagonal. When a level is missing (e.g. a solid
entity that only emits LOD 0), the selector walks down to the nearest
available slot.
API ripples:
* New `LodConfig` (HIGH/MID/LOW) drives `tess_sat`, `tess_cone_face`,
`tess_sphere_face`, `tess_torus_face`.
* `tessellate_solid3d / _region / _body` now return `MeshLodSet`.
* `MeshLodSet { lods: Vec<MeshModel>, world_aabb }` + `from_single`
constructor so interactive truck-based commands (BOX/CYLINDER/etc.)
that only generate one tessellation stay one-line.
* GPU side gets `MeshLodGpu { lods: Vec<MeshGpu>, world_aabb }` and
a `mesh_lod_levels` per-frame index vector.
* STL/STEP export pulls slot 0 explicitly so the on-disk geometry
isn't downgraded by the view-dependent ladder.
Background-thread lazy generation (per the roadmap) is deferred — the
synchronous eager build is fine for typical CAD files. Switch to a
queue + background tessellator when files with hundreds of solids
become a load-time bottleneck.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Hatch fills drew the full per-family pattern loop every frame regardless
of zoom. Two LOD tiers, neither of which existed:
* **Shader-side solid substitute.** When the densest line family's
spacing × scale projects to less than 2 px, individual lines blur
into a solid color and the per-fragment family loop is wasted ALU
(and produces moiré). Pass `world_per_pixel` through Uniforms; the
hatch fragment shader now returns `h.color` directly in that case.
* **CPU-side sub-pixel skip.** When the entire hatch AABB projects
to less than 2 px, drop the draw call. Adds `world_aabb` to
HatchGpu plus a per-frame `compute_hatch_lod` pass (mirrors the
existing scissor pass) and `hatch_skip_flags`. The render pass
skips flagged hatches.
Factored a shared `aabb_below_pixel` helper for future LOD callers.
The Uniforms layout is unchanged in size (96 B) — the trailing
`_pad: vec2<f32>` becomes `world_per_pixel: f32, _pad: f32`, so wgsl
modules that don't read the new field keep working with their old
`_pad: vec2<f32>` declarations.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Wire pass already uses `WireModel.vp_scissor` to clip viewport-projected
geometry to its paper-space viewport rect; hatch / image / wipeout
passes drew across the whole canvas. When a future paper-space path
projects fills/images into a viewport, they would bleed past the
viewport frame.
Add `vp_scissor: Option<[f32; 4]>` to HatchModel and ImageModel
(mirroring WireModel), forward it through HatchGpu / ImageGpu, and add
per-pass `compute_*_scissors` / `set_scissor_rect` machinery. Factored
the wire-side scissor-projection math into a shared `project_scissor`
helper. Field defaults to None at every constructor site — no producer
sets it yet, so this is plumbing for the paper-viewport fill projection
that Phase 1.4 unblocks without changing current visual output.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Greek rects rendered through fill_tris went through the face3d 0.45
dim path; the pre-boost (color × 1/0.45) compensated only until clamp
hit at 1.0, leaving bright user colors washed out. The earlier outline
workaround dodged the dim but lost the filled box visual that AutoCAD
uses for "text lives here".
- face3d pipeline now skips the 0.45 dim when `wire.points` is
empty. PolyfaceMesh / PolygonMesh keep their dim (both points and
fill_tris populated); greek and MultiLeader background-fill emit
`points: vec![]` so they render at the literal color.
- All three greek paths (top-level, block-defn, MultiLeader) emit a
2-triangle filled rect instead of an outline / boosted fill.
- Greek rects are clamped to single-line height so 5-line MText
doesn't blow the box up to the full text-block height. MultiLeader
emits one rect per line — keeps the per-row visual hint.
- BatchEntry/StyleKey gain an `is_fill_only` discriminator so greek
batches never collide with regular wire batches; otherwise the
finalized WireModel could carry both points and fill_tris and
defeat the pipeline-side empty-points check.
Xref fade still applies to the resolved color via the existing
`fade_toward_bg` path, so xref greek stays faded as expected.
Co-Authored-By: Claude Opus 4.7 <[email protected]>