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>
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
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>
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 <noreply@anthropic.com>
The model-space wire geometry is camera-independent, yet every pan/zoom
re-tessellated the whole model and re-uploaded it to the GPU. On a
block/text-heavy drawing that is millions of segments, costing a ~100ms
re-tessellation plus a ~150ms GPU re-upload per camera change (~258ms
freeze) while the GPU itself draws the full set comfortably in real time.
Tessellate the full, un-culled model once per geometry epoch, hold it
resident, and return it for every camera and tile. A pan/zoom now only
changes the view uniform; no frustum cull, no zoom LOD, no re-tess or
re-upload on camera moves. Pan/zoom frame time drops to ~7ms (~140fps)
with no stutter.
Removes the now-dead per-tile cull cache, pan-invariant hash, AABB
containment helper, and the reuse-during-motion / settle-regen machinery
that an earlier iteration introduced. Also exempts Face3D from the
sub-pixel stub LOD: a flat face is trivially cheap to tessellate, so it
is drawn full at any zoom and no longer pops a cube stub at the threshold.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A dot in an LTYPE pattern is a zero-length element. The pattern resolver
encoded it as a tiny `0.01 * scale` world-length dash, which had two bad
effects: it was sub-pixel at normal zoom so the dash LOD saw the pattern's
smallest feature drop below one pixel and collapsed the whole line to
solid, and at larger LTSCALE it left only invisible sub-pixel dots between
big gaps. Either way dotted and dash-dot linetypes were never visible;
only plain dashed (no zero elements) worked.
Keep dots as literal 0.0 so they no longer drag `min_elem` down, and
render each as a fixed ~1.5 px mark in the wire shader (pixel-snapped via
world_per_pixel, mirroring the hatch shader's dot handling). The shader
tells a real dot from trailing array padding by element count — the index
of the last non-zero element plus one — so no producer needs a sentinel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lineweights were rendered at their true physical size (mm × 96/25.4 px),
which is ~2 px for a 0.5 mm line — too timid to tell weights apart, and
every weight ≤ 0.25 mm floored to 1 px (no visible difference). Other DWG
editors display model-space lineweights larger than true size for screen
legibility; apply the same 2× boost so weights are pronounced and
distinguishable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The wire shader now subtracts the eye (high+low f32) per endpoint and
transforms by the rotation-only view-projection instead of multiplying
absolute positions by a view-projection that bakes in a large eye
translation. The math is identical (proj·R·(pos−eye)) but the large eye
no longer enters the f32 matrix, so lines stop jittering on
pan/zoom/rotate at large (UTM) coordinates and far from world_offset.
Geometry low bits are intentionally dropped: (pos − eye_high) is exact in
f32 for same-magnitude operands (Sterbenz) and eye_low corrects the eye,
so the only residual is a static sub-f32 position error, not motion
jitter — no per-vertex low buffer needed. view_rot is cropped in lock-step
with view_proj for tiled panes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ViewCube showed world Front/Top/Right. It now composes the camera
rotation with the active UCS (scene.viewcube_ucs, synced from the tab's
active_ucs), so its faces follow the user's coordinate system in model
space. Render uniform, hover/click hit-test, and the click→snap direction
all go through scene.viewcube_ucs_mat() so they stay in lock-step;
identity outside model space leaves paper-viewport navigation unchanged.
Co-Authored-By: Claude Opus 4.8 <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>
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>