Keep all solid geometry GPU-resident during navigation. Use vertex-rate instancing, compact untextured vertices, vertex-cache ordering, and cached silhouette sources without culling geometry.
Reuse full VPort records during camera and tile synchronization so
navigation cannot replace file-defined display state with defaults.
Honor image, ground, IBL, and sun-backed sky parameters in
background rendering.
Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive.
A paper layout painted its whole canvas in the page colour, so the sheet
had no visible edge — zooming out never reached one, because the surround
was the same colour as the page. Floating viewports then painted
themselves black over it.
Both come from the same idea, that a viewport's canvas is a colour it can
be handed. What lies behind a viewport depends on what the viewport is:
- The full-canvas sheet shows the desk. `paper_bg_color` is the page, and
the page is a bounded rectangle `paper_sheet_fill` already draws; using
it for the canvas too erased the boundary between them. The desk is now
its own colour, defined once in the scene and read by both the widget
and the renderer, so the two cannot drift apart.
- A floating viewport sits on that sheet and must paint no canvas at all.
Handing it a transparent one does not work: the background shader
returns an opaque colour whatever alpha it is given, so alpha zero came
out black. Skipping the pass is the only way to ask for see-through, so
`skip_background` now suppresses both the clear and the pass. It is part
of the frame signature too — without that, a cached frame would keep
showing the canvas the flag was just turned off for.
Shading and fog still blend toward what is behind a floating viewport,
which is the page, so the uniform keeps that colour even though nothing
paints it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Preload one language-aware font resource across the interface, drawing text, and navigation labels.\n\nRender the navigation cube, ring, outlines, and labels through a 4x MSAA target.
Keep storage fast paths on capable adapters and use packed or texture fallbacks only when device limits require them.\n\nCompact mesh-edge varyings to avoid WebGL shader rejection.
Cancel stale interaction state across space changes and keep live previews and zoom extents in their active paper/model context.\n\nRefs #539\nRefs #540\nRefs #541
Avoid full document, cache, and GPU rebuilds for entity deltas so add, delete, history, selection, and live polyline edits stay responsive on large drawings.
Keep resident wires, draw depths, GPU slabs, and edit history incremental across entity changes. Defer rollover work to avoid cursor jumps on dense drawings.
Replace full-document snapshots with Arc first-touch deltas and batch cache invalidation. Reuse interaction and GPU category caches across unrelated edits.
entity_wires() returns the memoized Arc instead of a per-call deep copy;
MESH entities over 50k vertices ship no per-vertex snap/key tables
(matching PolyfaceMesh/PolygonMesh); the Face3D/other render split
caches a None marker and uses the base set directly when there is no
Face3D wire; test fix for the depth-map BlockCache::build signature.
Reviewed: correctness sound — wire AABB comes from entity_aabb (not
key_vertices) so large meshes stay selectable; snap consumers are
empty-guarded; the Arc/strong_count reasoning for try_resident_patch
holds. One consistent pre-existing quirk noted (auto_fit_viewport
centroid excludes tableless meshes, as Poly*Mesh already do).
Fill triangles (DIMTFILL backgrounds, MLEADER masks, hatches) carry the
entity's draw-order rank, but SDF glyph quads were pushed at neutral 0.0
— any entity in the front half of the draw order had its own background
fill painted over its text. gather_text_verts now adds the host wire's
wire_draw_depth to each glyph: a text background lands at the same
biased depth (glyphs draw after fills and win the LessEqual test), and
text respects draw order against other entities' fills too.
The #403 glyph-reuse fast path gains a rank-stability gate: adding or
removing an entity shifts every sibling's bias (the rank denominator
changes), so reuse is only safe across all-Modified edits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-epoch Face3D/other split cloned every wire into two owned lists
even when the face3d list came out empty — for pure 2-D drawings and
mesh imports (#358) the 'other' list was a wire-for-wire copy of the
entire base set, held resident in split_cache alongside the original.
Cache None as an 'everything is other' marker and hand the render the
base set itself for that frame. The base Arc is deliberately not stored
in the cache: try_resident_patch can only apply an incremental patch
while the resident Arc is uniquely held, so pinning it there would
silently degrade every edit to a full rebuild.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paper layouts gather glyphs for the sheet and each content viewport in
one frame; the single-slot last_sdf_text handed the first source's
glyphs to the rest, hiding every model-space text entity inside
viewports (regression in aea1fd2). Key the reuse by render source, and
invalidate on Tolerance/ATTDEF edits and complex-linetype hosts too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Xref merge now inserts entities in the source block's chain order and
carries SortEntitiesTable objects across (remapped to merged handles).
draw_depth_map values become [depth, half] so exploded block/xref fills
and wide-polyline band wires both compose child depth as
insert_depth + in-block rank x half, recursing into nested inserts with
the same divisor - bands and fills from one block interleave by the
block's internal order instead of stacking at the insert's single depth.
Band batches split per rank via WireModel.depth_override; thin wires
stay merged. The wire arena falls back to a full rebuild when a slab
carries mixed depths so refreshes don't flatten band overrides.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rectangular paper viewports now go through the same GPU stencil clip as
non-rectangular ones: viewport_clip_boundary_ndc returns the four rect
corners (a full-target mask) instead of nothing, so the render path has
no rect/non-rect special case — every content viewport carries a clip
boundary and draws with stencil reference 0xFF.
With a single clip path, the old per-wire / -image / -wipeout scissor
mechanism is fully dead and removed: compute_*_scissors, the
*_pixel_scissors buffers, the project_scissor helper, the per-pass
set_scissor_rect blocks, the vp_scissor field threaded through
WireModel / ImageModel / HatchModel and their GPU mirrors (WireGpu /
ImageGpu / WipeoutGpu / HatchWebGpu), from_run's scissor parameter, the
scissor grouping in build_wire_buffers, and the is_arena_eligible gate.
Net -245 lines across 54 files.
Rectangular viewports still clip exactly via their own render rectangle;
the stencil mask is a redundant full-target no-op there, kept only so
the clip code path stays uniform. XCLIP keeps its CPU wire clip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Paper content viewports with a circle / spline / ellipse clip boundary
drew their model content to the full bounding rectangle instead of the
shape. Each paper content viewport is its own render into its own screen
rectangle, so clipping is done per viewport: the boundary polygon is
projected into that viewport's render-target NDC (paper shape mapped
through the same visible-sub-rect crop as the content, so it stays fixed
to the viewport frame) and stamped into the stencil buffer with an
even-odd Invert, which marks the interior of any shape, convex or not,
without triangulation. Every content pass then draws only where the
stencil is set, so hatch, wire, mesh, 3DFACE, text, image, wipeout and
selection are all clipped uniformly.
Because Invert turns the cleared 0x00 into 0xFF, clipped content draws
with stencil reference 0xFF (unclipped content keeps reference 0). The
shared depth buffer moves to Depth24PlusStencil8 across every pipeline.
Rectangular viewports keep clipping via their own render rectangle and
XCLIP keeps its existing CPU wire clip; neither needs the stencil path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the curved-face wireframe treatment from cones to spheres and tori, so
a solid built from those faces reads the way a cylinder already did.
ISOLINES: sphere faces get meridian lines across the face's colatitude span at
evenly spaced longitudes; torus faces get minor cross-section circles at evenly
spaced revolution angles. Both clip to the face's own parametric window (from
the boundary polygon via a shared `angular_span` gap-finder), so a partial
sphere or torus arc doesn't draw lines out over the missing part.
Silhouettes (DISPSILH): `CurvedGen` becomes an enum. A sphere silhouettes as
the great circle perpendicular to the view, clipped to the face window; a torus
as the two curves where the tube turns edge-on, traced around the revolution.
The silhouette now uses the camera's view *direction* alone, not the eye
position — a single parallel direction for the whole scene — so the outline
follows the view angle, holds still under pan, and doesn't foreshorten, which
is the orthographic silhouette a CAD wireframe expects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ISOLINES show a curved solid from any angle, but its outline still cut across
whichever fixed longitudinal lines happened to face the eye. DISPSILH draws the
true silhouette — the two lines where a cylinder (or cone) turns edge-on to the
view — and it moves with the camera, so it cannot be a baked edge.
Tessellation stores a world-space generator per cone/cylinder face on the mesh
set (axis, radial frame, radius, cone taper, extent), sharing the face-extent
extraction with the ISOLINES collector. Each frame `upload_silhouettes` walks
those generators, solves `du·cosθ + dv·sinθ = -tanα·da` for the two edge-on
angles (`φ ± acos(...)`, `φ ± π/2` for a cylinder), and builds a line list in
the mesh vertex format — double-single, so it stays precise at UTM. It draws
through the existing wireframe / edge pipelines in the wireframe and
shaded-with-edges passes.
Gated on the document's DISPSILH header flag, which defaults off — so the
per-frame pass is a no-op unless the drawing asks for it. Scoped to top-level
solids: a block instance's generators aren't transformed by the INSTANCE pose
yet, so they are cleared on the block path (its isolines and feature edges
still draw).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three cuts to the per-edit cost, on top of the incremental wire arena:
- Enable OCS_WIRE_GPU_PATCH by default (kill-switch OCS_WIRE_GPU_PATCH=0), so the
in-place / append-tombstone arena — validated on the reference drawing (wire
block ~300ms + 265MB upload -> ~2ms) — is on for everyone; a bail always falls
back to the identical batched path.
- Resident patch: an all-Modified edit never reorders, so reassemble by walking
the recorded assembled order (~10k handles) instead of scanning the whole
document (~800k) to re-derive draw order. Add/Remove still scan (positions
change). ~14ms -> <1ms on a move.
- SDF text: a new content id misses the glyph cache every edit, but if no
text-bearing entity (Text/MText/Dimension/MLeader/Leader/Table/attribute/INSERT)
changed since the last build the glyphs are identical, so reuse them. ~23ms -> 0
on a plain-geometry edit.
A move/edit on this 800k-entity drawing now spends ~50ms (dominated by the undo
document clone) instead of ~85ms, and the wire GPU upload is gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>