perf(render): unify model/paper/block spaces on one resident wire path
Model space held a full, camera-independent, un-culled wire set with a stable content id, so pan/zoom re-drew without re-tessellating or re-uploading and idle frames collapsed to a single blit. Paper did none of that: the sheet was camera-keyed (full re-tess every pan/zoom), content viewports re-baked per zoom step with their own frustum cull + LOD, and a per-frame wire_force_nonce forced a GPU re-upload every frame AND poisoned render_signature so paper never hit the single-blit scene cache. Route every space through one Scene::resident_wires_for(block, anno, frozen): the full, un-culled, LOD-free set per (block, ambient bg, anno, frozen) key, held resident and stamped with a stable WIRE_CONTENT_GEN id. Model tiles, BEDIT, the paper sheet (a thin border-drop + printable-area dressing over the resident base) and paper content viewports all share it. When annotation is inert (PSLTSCALE off and no annotative entity, cached per epoch) the per- viewport scale is normalized away so every viewport reuses one tessellation. Result: paper pan/zoom does zero re-tessellation and zero wire re-upload, and paper now hits the single-blit scene cache exactly like model. Deletes wire_force_nonce, the per-viewport height/view wire cache, and view_cull_aabb. Tradeoff (deliberate, matches model): no cull/LOD anywhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6208b15cdb
commit
f39a2d3126
3 changed files with 223 additions and 230 deletions
269
src/scene/mod.rs
269
src/scene/mod.rs
|
|
@ -665,46 +665,6 @@ fn transform_block_mesh_lod_set(
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// World-XY rectangle the model camera currently sees, expanded by `margin`
|
|
||||||
/// (1.0 = tight), as `[min_x, min_y, max_x, max_y]` for the entity R-tree cull.
|
|
||||||
///
|
|
||||||
/// The screen is a rectangle in the camera's right/up basis, not the world
|
|
||||||
/// axes — under a view twist or yaw that rectangle is rotated in world XY, so
|
|
||||||
/// projecting its four corners and taking their bounds gives the correct
|
|
||||||
/// enclosing box. A naive `target ± (w, h)` box (world-axis aligned) under-
|
|
||||||
/// covers a rotated view and culls the geometry that lands in the rotated
|
|
||||||
/// corners.
|
|
||||||
///
|
|
||||||
/// Returns `None` for a tilted (non-plan) view, where the view direction is
|
|
||||||
/// not vertical and a flat XY box cannot bound the visible region (depth
|
|
||||||
/// collapses onto the plane); callers then skip the frustum cull rather than
|
|
||||||
/// wrongly hide geometry.
|
|
||||||
fn view_cull_aabb(cam: &Camera, aspect: f32, margin: f32) -> Option<[f32; 4]> {
|
|
||||||
// Plan view ⇔ line of sight is (near) vertical. Anything else can't be
|
|
||||||
// bounded by a single world-XY rectangle.
|
|
||||||
let fwd = cam.rotation * glam::Vec3::Z;
|
|
||||||
if fwd.z.abs() < 0.999 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let h = cam.ortho_size();
|
|
||||||
let w = h * aspect.max(0.01);
|
|
||||||
let right = cam.rotation * glam::Vec3::X;
|
|
||||||
let up = cam.rotation * glam::Vec3::Y;
|
|
||||||
let c = cam.target.as_vec3();
|
|
||||||
let (mut min_x, mut min_y) = (f32::INFINITY, f32::INFINITY);
|
|
||||||
let (mut max_x, mut max_y) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
|
|
||||||
for (sw, sh) in [(-w, -h), (w, -h), (-w, h), (w, h)] {
|
|
||||||
let p = c + right * sw + up * sh;
|
|
||||||
min_x = min_x.min(p.x);
|
|
||||||
max_x = max_x.max(p.x);
|
|
||||||
min_y = min_y.min(p.y);
|
|
||||||
max_y = max_y.max(p.y);
|
|
||||||
}
|
|
||||||
let (cx, cy) = ((min_x + max_x) * 0.5, (min_y + max_y) * 0.5);
|
|
||||||
let (hw, hh) = ((max_x - min_x) * 0.5 * margin, (max_y - min_y) * 0.5 * margin);
|
|
||||||
Some([cx - hw, cy - hh, cx + hw, cy + hh])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
pub camera: Rc<RefCell<Camera>>,
|
pub camera: Rc<RefCell<Camera>>,
|
||||||
/// Model-space tiled viewport layout. One full-window tile by default;
|
/// Model-space tiled viewport layout. One full-window tile by default;
|
||||||
|
|
@ -718,8 +678,12 @@ pub struct Scene {
|
||||||
/// not re-walked every frame while the wire set is unchanged. Keyed on the
|
/// not re-walked every frame while the wire set is unchanged. Keyed on the
|
||||||
/// wire content id so it re-gathers exactly when the wires (geometry or
|
/// wire content id so it re-gathers exactly when the wires (geometry or
|
||||||
/// selection) change. See [`Scene::gather_text_verts`].
|
/// selection) change. See [`Scene::gather_text_verts`].
|
||||||
sdf_text_cache:
|
/// Small map (not a single slot): a paper frame renders several wire
|
||||||
RefCell<Option<(u64, std::sync::Arc<Vec<crate::scene::pipeline::text_gpu::TextVertex>>)>>,
|
/// sources (sheet + content viewports), each with its own stable content
|
||||||
|
/// id — one slot would thrash between them every frame.
|
||||||
|
sdf_text_cache: RefCell<
|
||||||
|
HashMap<u64, std::sync::Arc<Vec<crate::scene::pipeline::text_gpu::TextVertex>>>,
|
||||||
|
>,
|
||||||
/// pane_grid layout tree for the Model tab — the source of truth for the
|
/// pane_grid layout tree for the Model tab — the source of truth for the
|
||||||
/// tile split layout, resize and focus. `model_tiles` (the renderer's
|
/// tile split layout, resize and focus. `model_tiles` (the renderer's
|
||||||
/// per-pane data: camera / render-mode / grid) is kept in lock-step with
|
/// per-pane data: camera / render-mode / grid) is kept in lock-step with
|
||||||
|
|
@ -780,7 +744,8 @@ pub struct Scene {
|
||||||
/// Keyed by `(geometry_epoch, camera_generation)` so a camera change
|
/// Keyed by `(geometry_epoch, camera_generation)` so a camera change
|
||||||
/// invalidates the cull-dependent wire list as well as a geometry change.
|
/// invalidates the cull-dependent wire list as well as a geometry change.
|
||||||
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
|
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
|
||||||
wire_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
|
/// The middle `u64` is the set's [`WIRE_CONTENT_GEN`] content id.
|
||||||
|
wire_cache: RefCell<Option<((u64, u64), u64, Arc<Vec<WireModel>>)>>,
|
||||||
/// Spatial grid over the model hit-test wire set, keyed on `geometry_epoch`
|
/// Spatial grid over the model hit-test wire set, keyed on `geometry_epoch`
|
||||||
/// so it is rebuilt only when geometry changes (not per cursor move). Lets
|
/// so it is rebuilt only when geometry changes (not per cursor move). Lets
|
||||||
/// snap / hover query a cursor-local wire subset instead of scanning the
|
/// snap / hover query a cursor-local wire subset instead of scanning the
|
||||||
|
|
@ -821,14 +786,11 @@ pub struct Scene {
|
||||||
/// hover. The set is geometry-derived, so a camera move / hover never
|
/// hover. The set is geometry-derived, so a camera move / hover never
|
||||||
/// invalidates it.
|
/// invalidates it.
|
||||||
insert_hatch_cache: RefCell<Option<(u64, Arc<Vec<(Handle, HatchModel)>>)>>,
|
insert_hatch_cache: RefCell<Option<(u64, Arc<Vec<(Handle, HatchModel)>>)>>,
|
||||||
/// Per-viewport wire cache for paper-space rendering.
|
/// Sheet "dressing" cache over the unified resident set: the paper sheet
|
||||||
/// Maps vp_handle → (geometry_epoch, Arc<Vec<WireModel>>).
|
/// drops its own border wire and appends the printable-area guide.
|
||||||
viewport_wire_cache: RefCell<HashMap<Handle, ((u64, u32, u64), Arc<Vec<WireModel>>)>>,
|
/// `(geometry_epoch, content gen, wires)` — the base is camera-independent
|
||||||
/// Cached tessellation of paper-space layout block entities (title block, annotations, etc.).
|
/// (no cull / LOD anywhere), so paper pan/zoom never re-tessellates it.
|
||||||
/// Separate from `wire_cache` so the GPU sheet viewport doesn't re-tessellate every frame.
|
paper_sheet_cache: RefCell<Option<(u64, u64, Arc<Vec<WireModel>>)>>,
|
||||||
/// Keyed by `(geometry_epoch, camera_generation)` — paper view changes
|
|
||||||
/// on zoom too, so culled wire output depends on camera.
|
|
||||||
paper_sheet_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
|
|
||||||
/// Per-viewport projected wire cache for paper-space content viewports.
|
/// Per-viewport projected wire cache for paper-space content viewports.
|
||||||
/// Stores projected + clipped wires in paper-space coordinates.
|
/// Stores projected + clipped wires in paper-space coordinates.
|
||||||
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
||||||
|
|
@ -885,6 +847,13 @@ pub struct Scene {
|
||||||
/// Multiplier applied to Text/MText/Dimension sizes during tessellation.
|
/// Multiplier applied to Text/MText/Dimension sizes during tessellation.
|
||||||
/// 1.0 = no scaling. 50.0 = "1:50" drawing scale.
|
/// 1.0 = no scaling. 50.0 = "1:50" drawing scale.
|
||||||
pub annotation_scale: f32,
|
pub annotation_scale: f32,
|
||||||
|
/// Cached per-epoch: does annotation/viewport scale actually change wire
|
||||||
|
/// output? True iff PSLTSCALE is on (linetype dashes scale with the
|
||||||
|
/// viewport) or the drawing has an annotative entity (text/dim/mleader
|
||||||
|
/// sizing). When false the per-viewport anno scale is inert, so every
|
||||||
|
/// content viewport collapses onto ONE shared resident set instead of a
|
||||||
|
/// full re-tessellation per distinct viewport scale. `(epoch, flag)`.
|
||||||
|
annotation_affects_wires: std::cell::Cell<Option<(u64, bool)>>,
|
||||||
/// Cached model-space bounding box, keyed by geometry_epoch.
|
/// Cached model-space bounding box, keyed by geometry_epoch.
|
||||||
/// Avoids re-tessellating all entities on every ZOOM E / auto-fit call.
|
/// Avoids re-tessellating all entities on every ZOOM E / auto-fit call.
|
||||||
model_extents_cache: RefCell<Option<(u64, Option<(glam::Vec3, glam::Vec3)>)>>,
|
model_extents_cache: RefCell<Option<(u64, Option<(glam::Vec3, glam::Vec3)>)>>,
|
||||||
|
|
@ -933,24 +902,24 @@ pub struct Scene {
|
||||||
/// scene-render cache). See [`Scene::navigating_lod`].
|
/// scene-render cache). See [`Scene::navigating_lod`].
|
||||||
nav_last_gen: std::cell::Cell<u64>,
|
nav_last_gen: std::cell::Cell<u64>,
|
||||||
nav_changed_at: std::cell::Cell<Option<iced::time::Instant>>,
|
nav_changed_at: std::cell::Cell<Option<iced::time::Instant>>,
|
||||||
/// Static-hold cache for the Model layout: the FULL, un-culled tessellation
|
/// Unified static-hold wire cache — ONE infrastructure for EVERY space
|
||||||
/// held resident and reused for every camera (the geometry is
|
/// (Model tiles, BEDIT block editor, the paper sheet block, and paper
|
||||||
/// camera-independent — see `model_tile_wires_arc`). `(epoch, gen, wires)`;
|
/// content viewports): the FULL, un-culled, LOD-free tessellation per
|
||||||
/// rebuilt when `geometry_epoch` changes.
|
/// `(block, ambient bg, anno scale, vp-frozen set)` key, held resident and
|
||||||
model_static_wires: RefCell<Option<(u64, u64, Arc<Vec<WireModel>>)>>,
|
/// reused for every camera. Each entry carries a stable
|
||||||
/// Monotonic per-build nonce for wire sources that must NOT be skipped by
|
/// [`WIRE_CONTENT_GEN`] id so the GPU upload gate and `render_signature`
|
||||||
/// the upload gate — the paper / per-viewport wire paths and any
|
/// can tell "same content" from "changed" — no per-frame re-upload and no
|
||||||
/// frame carrying live preview / interim wires. High bit set so it can
|
/// camera-keyed re-tessellation anywhere. Rebuilt per key when
|
||||||
/// never collide with a real [`WIRE_CONTENT_GEN`] id; incremented every
|
/// `geometry_epoch` changes; stale entries evicted on insert.
|
||||||
/// use so the GPU always sees a fresh id and re-uploads.
|
#[allow(clippy::type_complexity)]
|
||||||
pub(crate) wire_force_nonce: std::cell::Cell<u64>,
|
resident_wire_sets: RefCell<HashMap<u64, (u64, u64, Arc<Vec<WireModel>>)>>,
|
||||||
/// Memoized `(face3d, other)` split of the Model-tile wire set, keyed by
|
/// Memoized `(face3d, other)` split of a resident wire set, keyed by its
|
||||||
/// its [`WIRE_CONTENT_GEN`] id. `split_face3d_wires` is an O(N) per-wire
|
/// [`WIRE_CONTENT_GEN`] id. `split_face3d_wires` is an O(N) per-wire
|
||||||
/// handle lookup + clone that otherwise re-runs every frame; a pan that
|
/// handle lookup + clone that otherwise re-runs every frame. A map, not a
|
||||||
/// reuses the tessellation (same id) reuses this split too.
|
/// slot: a paper frame walks several sources (sheet + viewports).
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
split_cache:
|
split_cache:
|
||||||
RefCell<Option<(u64, Arc<Vec<WireModel>>, Arc<Vec<WireModel>>)>>,
|
RefCell<HashMap<u64, (Arc<Vec<WireModel>>, Arc<Vec<WireModel>>)>>,
|
||||||
/// Cached `selected ∪ hover` handle set for the GPU xray overlay, keyed by
|
/// Cached `selected ∪ hover` handle set for the GPU xray overlay, keyed by
|
||||||
/// `selection_generation`. Rebuilt only when the selection changes so
|
/// `selection_generation`. Rebuilt only when the selection changes so
|
||||||
/// `build_primitive` doesn't clone the set every frame.
|
/// `build_primitive` doesn't clone the set every frame.
|
||||||
|
|
@ -994,7 +963,7 @@ impl Scene {
|
||||||
snap_on: false,
|
snap_on: false,
|
||||||
}]),
|
}]),
|
||||||
active_model_tile: std::cell::Cell::new(0),
|
active_model_tile: std::cell::Cell::new(0),
|
||||||
sdf_text_cache: RefCell::new(None),
|
sdf_text_cache: RefCell::new(HashMap::default()),
|
||||||
// One pane mapped to tile 0 — matches the single default tile above.
|
// One pane mapped to tile 0 — matches the single default tile above.
|
||||||
model_panes: iced::widget::pane_grid::State::new(0).0,
|
model_panes: iced::widget::pane_grid::State::new(0).0,
|
||||||
selection: Rc::new(RefCell::new(SelectionState::default())),
|
selection: Rc::new(RefCell::new(SelectionState::default())),
|
||||||
|
|
@ -1021,7 +990,6 @@ impl Scene {
|
||||||
image_cache: RefCell::new(None),
|
image_cache: RefCell::new(None),
|
||||||
mesh_cache: RefCell::new(None),
|
mesh_cache: RefCell::new(None),
|
||||||
insert_hatch_cache: RefCell::new(None),
|
insert_hatch_cache: RefCell::new(None),
|
||||||
viewport_wire_cache: RefCell::new(HashMap::default()),
|
|
||||||
paper_sheet_cache: RefCell::new(None),
|
paper_sheet_cache: RefCell::new(None),
|
||||||
paper_projected_cache: RefCell::new(HashMap::default()),
|
paper_projected_cache: RefCell::new(HashMap::default()),
|
||||||
current_layout: "Model".to_string(),
|
current_layout: "Model".to_string(),
|
||||||
|
|
@ -1038,6 +1006,7 @@ impl Scene {
|
||||||
local_extent_max: 1e9,
|
local_extent_max: 1e9,
|
||||||
local_center: [0.0, 0.0],
|
local_center: [0.0, 0.0],
|
||||||
annotation_scale: 1.0,
|
annotation_scale: 1.0,
|
||||||
|
annotation_affects_wires: std::cell::Cell::new(None),
|
||||||
model_extents_cache: RefCell::new(None),
|
model_extents_cache: RefCell::new(None),
|
||||||
entity_block_map_cache: RefCell::new(None),
|
entity_block_map_cache: RefCell::new(None),
|
||||||
block_defn_cache: RefCell::new(None),
|
block_defn_cache: RefCell::new(None),
|
||||||
|
|
@ -1050,9 +1019,8 @@ impl Scene {
|
||||||
last_model_wire_gen: std::cell::Cell::new(0),
|
last_model_wire_gen: std::cell::Cell::new(0),
|
||||||
nav_last_gen: std::cell::Cell::new(0),
|
nav_last_gen: std::cell::Cell::new(0),
|
||||||
nav_changed_at: std::cell::Cell::new(None),
|
nav_changed_at: std::cell::Cell::new(None),
|
||||||
model_static_wires: RefCell::new(None),
|
resident_wire_sets: RefCell::new(HashMap::default()),
|
||||||
wire_force_nonce: std::cell::Cell::new(0),
|
split_cache: RefCell::new(HashMap::default()),
|
||||||
split_cache: RefCell::new(None),
|
|
||||||
tess_memo: RefCell::new(HashMap::default()),
|
tess_memo: RefCell::new(HashMap::default()),
|
||||||
tess_memo_guard: std::cell::Cell::new(0),
|
tess_memo_guard: std::cell::Cell::new(0),
|
||||||
resident_tess_memo: RefCell::new(HashMap::default()),
|
resident_tess_memo: RefCell::new(HashMap::default()),
|
||||||
|
|
@ -2300,7 +2268,10 @@ impl Scene {
|
||||||
vp.view_height = vp.height / scale;
|
vp.view_height = vp.height / scale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.viewport_wire_cache.borrow_mut().remove(&handle);
|
// A viewport scale change alters its anno key in the unified
|
||||||
|
// resident map; drop everything so the abandoned entry can't
|
||||||
|
// linger for the rest of the epoch.
|
||||||
|
self.resident_wire_sets.borrow_mut().clear();
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2403,10 +2374,87 @@ impl Scene {
|
||||||
_cam_aspect: f32,
|
_cam_aspect: f32,
|
||||||
_tile_pixel_height: f32,
|
_tile_pixel_height: f32,
|
||||||
) -> Arc<Vec<WireModel>> {
|
) -> Arc<Vec<WireModel>> {
|
||||||
// Reuse the resident full set if it's already built for this epoch.
|
// In a BEDIT block editor the resident set is the edited block's own
|
||||||
|
// (block-local) entities; otherwise model space. (#261)
|
||||||
|
let block = self
|
||||||
|
.block_edit_block
|
||||||
|
.unwrap_or_else(|| self.model_space_block_handle());
|
||||||
|
self.resident_wires_for(block, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unified static-hold wire builder — the ONE tessellation path every
|
||||||
|
/// space renders through (Model tiles, BEDIT, the paper sheet block, and
|
||||||
|
/// paper content viewports): the FULL, un-culled, LOD-free set of `block`,
|
||||||
|
/// held resident per `(block, ambient bg, anno, frozen)` key and rebuilt
|
||||||
|
/// only when `geometry_epoch` changes. Mints a stable
|
||||||
|
/// [`WIRE_CONTENT_GEN`] id per build (relayed via `last_model_wire_gen`)
|
||||||
|
/// so the GPU upload gate and `render_signature` skip unchanged content.
|
||||||
|
/// Whether the per-viewport annotation scale changes wire output at all
|
||||||
|
/// (PSLTSCALE on, or any annotative entity). Cached per geometry epoch —
|
||||||
|
/// the scan short-circuits on the first annotative entity. When false, a
|
||||||
|
/// viewport's `1/vp_scale` anno override is inert and normalized away so
|
||||||
|
/// all viewports reuse ONE resident tessellation.
|
||||||
|
fn annotation_affects_wires(&self) -> bool {
|
||||||
|
if let Some((epoch, v)) = self.annotation_affects_wires.get() {
|
||||||
|
if epoch == self.geometry_epoch {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let v = self.document.header.paper_space_linetype_scaling
|
||||||
|
|| self
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.any(|e| crate::scene::annotative::is_annotative(&self.document, e));
|
||||||
|
self.annotation_affects_wires
|
||||||
|
.set(Some((self.geometry_epoch, v)));
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resident_wires_for(
|
||||||
|
&self,
|
||||||
|
block: Handle,
|
||||||
|
anno_scale_override: Option<f32>,
|
||||||
|
frozen_layers: Option<&HashSet<Handle>>,
|
||||||
|
) -> Arc<Vec<WireModel>> {
|
||||||
|
// Normalize an inert anno override away so distinct viewport scales
|
||||||
|
// share one resident set when annotation can't change the wires.
|
||||||
|
let anno_scale_override = if self.annotation_affects_wires() {
|
||||||
|
anno_scale_override
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
// Ambient bg is part of the key: the same block tessellated against
|
||||||
|
// the model vs paper background adapts wire colors differently.
|
||||||
|
let bg = if self.current_layout == "Model" {
|
||||||
|
self.bg_color
|
||||||
|
} else {
|
||||||
|
self.paper_bg_color
|
||||||
|
};
|
||||||
|
let key = {
|
||||||
|
let mut k: u64 = 0xcbf2_9ce4_8422_2325;
|
||||||
|
let mut mix = |x: u64| k = k.rotate_left(17) ^ x.wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||||
|
mix(block.value());
|
||||||
|
for c in bg {
|
||||||
|
mix(c.to_bits() as u64);
|
||||||
|
}
|
||||||
|
mix(anno_scale_override.map(|a| a.to_bits() as u64).unwrap_or(u64::MAX));
|
||||||
|
match frozen_layers {
|
||||||
|
Some(f) => {
|
||||||
|
// Order-independent fold of the frozen-layer set.
|
||||||
|
let mut acc: u64 = 0;
|
||||||
|
for h in f {
|
||||||
|
acc ^= h.value().wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||||
|
}
|
||||||
|
mix(acc);
|
||||||
|
mix(f.len() as u64);
|
||||||
|
}
|
||||||
|
None => mix(u64::MAX - 1),
|
||||||
|
}
|
||||||
|
k
|
||||||
|
};
|
||||||
{
|
{
|
||||||
let held = self.model_static_wires.borrow();
|
let sets = self.resident_wire_sets.borrow();
|
||||||
if let Some((epoch, gen, arc)) = held.as_ref() {
|
if let Some((epoch, gen, arc)) = sets.get(&key) {
|
||||||
if *epoch == self.geometry_epoch {
|
if *epoch == self.geometry_epoch {
|
||||||
self.last_model_wire_gen.set(*gen);
|
self.last_model_wire_gen.set(*gen);
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
|
|
@ -2414,41 +2462,46 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Build once: full tessellation, no cull (region = None), no zoom LOD
|
// Build once: full tessellation, no cull (region = None), no zoom LOD
|
||||||
// (wpp = None). Held for the life of this geometry epoch.
|
// (wpp = None) — for every space, exactly like the Model static-hold.
|
||||||
// In a BEDIT block editor the resident set is the edited block's own
|
|
||||||
// (block-local) entities; otherwise model space. (#261)
|
|
||||||
let block = self
|
|
||||||
.block_edit_block
|
|
||||||
.unwrap_or_else(|| self.model_space_block_handle());
|
|
||||||
let t_tess = iced::time::Instant::now();
|
let t_tess = iced::time::Instant::now();
|
||||||
let mut wires = self.wires_for_block_culled(block, None, None, None, None);
|
let mut wires =
|
||||||
self.apply_refedit_fade(&mut wires, self.bg_color);
|
self.wires_for_block_culled(block, None, None, frozen_layers, anno_scale_override);
|
||||||
|
self.apply_refedit_fade(&mut wires, bg);
|
||||||
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
|
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
|
||||||
self.last_tess_wires.set(wires.len());
|
self.last_tess_wires.set(wires.len());
|
||||||
let arc = Arc::new(wires);
|
let arc = Arc::new(wires);
|
||||||
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||||
self.last_model_wire_gen.set(gen);
|
self.last_model_wire_gen.set(gen);
|
||||||
*self.model_static_wires.borrow_mut() = Some((self.geometry_epoch, gen, Arc::clone(&arc)));
|
let mut sets = self.resident_wire_sets.borrow_mut();
|
||||||
|
// Evict stale entries (older epochs / abandoned keys) so switching
|
||||||
|
// spaces or re-scaling a viewport can't accumulate dead full sets.
|
||||||
|
let cur_epoch = self.geometry_epoch;
|
||||||
|
sets.retain(|_, (e, ..)| *e == cur_epoch);
|
||||||
|
if sets.len() > 8 {
|
||||||
|
sets.clear();
|
||||||
|
}
|
||||||
|
sets.insert(key, (cur_epoch, gen, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cached tessellation of the current layout block's paper-space entities,
|
/// Cached tessellation of the current layout block's paper-space entities,
|
||||||
/// shared by `entity_wires_arc()` and the GPU sheet viewport.
|
/// shared by `entity_wires_arc()` and the GPU sheet viewport. A thin
|
||||||
|
/// "dressing" layer over the unified resident set (border drop +
|
||||||
|
/// printable-area guide) — camera-independent, epoch-keyed, so paper
|
||||||
|
/// pan/zoom never re-tessellates the sheet.
|
||||||
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
||||||
let key = (self.geometry_epoch, self.camera_generation);
|
|
||||||
{
|
{
|
||||||
let cache = self.paper_sheet_cache.borrow();
|
let cache = self.paper_sheet_cache.borrow();
|
||||||
if let Some((cached_key, ref arc)) = *cache {
|
if let Some((epoch, gen, ref arc)) = *cache {
|
||||||
if cached_key == key {
|
if epoch == self.geometry_epoch {
|
||||||
|
self.last_model_wire_gen.set(gen);
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
let t_tess = iced::time::Instant::now();
|
let base = self.resident_wires_for(layout_block, None, None);
|
||||||
let mut wires = self.wires_for_block(layout_block);
|
let mut wires = (*base).clone();
|
||||||
self.last_tess_ms.set(t_tess.elapsed().as_secs_f32() * 1000.0);
|
|
||||||
self.last_tess_wires.set(wires.len());
|
|
||||||
// The overall "sheet" viewport now IS the paper view itself, so its own
|
// The overall "sheet" viewport now IS the paper view itself, so its own
|
||||||
// border rectangle must not be drawn as an entity on the sheet.
|
// border rectangle must not be drawn as an entity on the sheet.
|
||||||
let sheet = self.current_layout_sheet_viewport_handle();
|
let sheet = self.current_layout_sheet_viewport_handle();
|
||||||
|
|
@ -2456,18 +2509,15 @@ impl Scene {
|
||||||
let sheet_name = sheet.value().to_string();
|
let sheet_name = sheet.value().to_string();
|
||||||
wires.retain(|w| w.name != sheet_name);
|
wires.retain(|w| w.name != sheet_name);
|
||||||
}
|
}
|
||||||
let bg = if self.current_layout == "Model" {
|
|
||||||
self.bg_color
|
|
||||||
} else {
|
|
||||||
self.paper_bg_color
|
|
||||||
};
|
|
||||||
self.apply_refedit_fade(&mut wires, bg);
|
|
||||||
// Printable-area guide (paper inset by plot margins), paper space only.
|
// Printable-area guide (paper inset by plot margins), paper space only.
|
||||||
if let Some(pa) = self.printable_area_wire() {
|
if let Some(pa) = self.printable_area_wire() {
|
||||||
wires.push(pa);
|
wires.push(pa);
|
||||||
}
|
}
|
||||||
let arc = Arc::new(wires);
|
let arc = Arc::new(wires);
|
||||||
*self.paper_sheet_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
|
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.last_model_wire_gen.set(gen);
|
||||||
|
*self.paper_sheet_cache.borrow_mut() =
|
||||||
|
Some((self.geometry_epoch, gen, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2478,8 +2528,9 @@ impl Scene {
|
||||||
let key = (self.geometry_epoch, self.camera_generation);
|
let key = (self.geometry_epoch, self.camera_generation);
|
||||||
{
|
{
|
||||||
let cache = self.wire_cache.borrow();
|
let cache = self.wire_cache.borrow();
|
||||||
if let Some((cached_key, ref arc)) = *cache {
|
if let Some((cached_key, gen, ref arc)) = *cache {
|
||||||
if cached_key == key {
|
if cached_key == key {
|
||||||
|
self.last_model_wire_gen.set(gen);
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2499,10 +2550,14 @@ impl Scene {
|
||||||
return self.model_tile_wires_arc(0, &cam, 1.0, 1.0);
|
return self.model_tile_wires_arc(0, &cam, 1.0, 1.0);
|
||||||
}
|
}
|
||||||
// Paper space: extend sheet wires with projected viewport content.
|
// Paper space: extend sheet wires with projected viewport content.
|
||||||
|
// This composite is the CPU pick/snap set; the projection follows the
|
||||||
|
// paper camera, so the key keeps `camera_generation`.
|
||||||
let mut wires = (*self.paper_sheet_wires_arc()).clone();
|
let mut wires = (*self.paper_sheet_wires_arc()).clone();
|
||||||
wires.extend(self.viewport_content_wires(layout_block, None, None));
|
wires.extend(self.viewport_content_wires(layout_block, None, None));
|
||||||
let arc = Arc::new(wires);
|
let arc = Arc::new(wires);
|
||||||
*self.wire_cache.borrow_mut() = Some((key, Arc::clone(&arc)));
|
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.last_model_wire_gen.set(gen);
|
||||||
|
*self.wire_cache.borrow_mut() = Some((key, gen, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3589,8 +3644,12 @@ impl Scene {
|
||||||
// Frozen-layer / anno-override viewport paths bypass (their params would
|
// Frozen-layer / anno-override viewport paths bypass (their params would
|
||||||
// thrash a shared memo). Hit-test also passes `view_aabb == None` but is
|
// thrash a shared memo). Hit-test also passes `view_aabb == None` but is
|
||||||
// not the Model block, so it never lands on the resident branch.
|
// not the Model block, so it never lands on the resident branch.
|
||||||
let base_ok =
|
// An empty vp-frozen set and an anno override are memo-compatible: the
|
||||||
is_model_block && frozen_layers.is_none() && anno_scale_override.is_none();
|
// guard hash below mixes anno/bg, so a param change invalidates rather
|
||||||
|
// than corrupts. Only a NON-empty frozen set (entity subset changes)
|
||||||
|
// bypasses the memo.
|
||||||
|
let frozen_empty = frozen_layers.map(|f| f.is_empty()).unwrap_or(true);
|
||||||
|
let base_ok = is_model_block && frozen_empty;
|
||||||
// Kill-switch: `OCS_NO_RESIDENT_MEMO` reverts the resident set to the old
|
// Kill-switch: `OCS_NO_RESIDENT_MEMO` reverts the resident set to the old
|
||||||
// full re-tessellation on every edit, in case a mutation site is ever
|
// full re-tessellation on every edit, in case a mutation site is ever
|
||||||
// found that edits geometry without dropping its handle from the memo.
|
// found that edits geometry without dropping its handle from the memo.
|
||||||
|
|
@ -3963,7 +4022,7 @@ impl Scene {
|
||||||
// Prefer the already-computed wire AABB cache when available — avoids re-tessellating.
|
// Prefer the already-computed wire AABB cache when available — avoids re-tessellating.
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
let cache = self.wire_cache.borrow();
|
let cache = self.wire_cache.borrow();
|
||||||
if let Some(((epoch, _cam_gen), ref arc)) = *cache {
|
if let Some(((epoch, _cam_gen), _gen, ref arc)) = *cache {
|
||||||
if epoch == self.geometry_epoch {
|
if epoch == self.geometry_epoch {
|
||||||
for wire in arc.iter() {
|
for wire in arc.iter() {
|
||||||
let [ax, ay, bx, by] = wire.aabb;
|
let [ax, ay, bx, by] = wire.aabb;
|
||||||
|
|
|
||||||
|
|
@ -392,11 +392,18 @@ impl Scene {
|
||||||
fn model_wires_for_viewport(
|
fn model_wires_for_viewport(
|
||||||
&self,
|
&self,
|
||||||
vp_handle: Handle,
|
vp_handle: Handle,
|
||||||
screen_height_px: f32,
|
_screen_height_px: f32,
|
||||||
) -> Vec<WireModel> {
|
) -> Arc<Vec<WireModel>> {
|
||||||
use rustc_hash::FxHashSet as HSet;
|
use rustc_hash::FxHashSet as HSet;
|
||||||
|
|
||||||
let (frozen, vp_anno_scale, vp_aspect) = match self.document.get_entity(vp_handle) {
|
// Only content-REAL per-viewport parameters remain: the viewport's own
|
||||||
|
// frozen-layer set and its annotation/PSLTSCALE scale. Frustum cull and
|
||||||
|
// zoom LOD are gone — every space (Model tiles, BEDIT, the sheet, and
|
||||||
|
// content viewports) shares the same un-culled, LOD-free resident
|
||||||
|
// infrastructure, so viewports with default parameters all reuse ONE
|
||||||
|
// static-hold entry (and its stable content id) instead of re-baking
|
||||||
|
// per viewport per zoom step.
|
||||||
|
let (frozen, vp_anno_scale) = match self.document.get_entity(vp_handle) {
|
||||||
Some(EntityType::Viewport(vp)) => {
|
Some(EntityType::Viewport(vp)) => {
|
||||||
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
||||||
let vp_scale =
|
let vp_scale =
|
||||||
|
|
@ -406,95 +413,29 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
1.0_f32
|
1.0_f32
|
||||||
};
|
};
|
||||||
let aspect = if vp.height > 1e-9 {
|
(f, anno)
|
||||||
(vp.width / vp.height) as f32
|
|
||||||
} else {
|
|
||||||
1.0_f32
|
|
||||||
};
|
|
||||||
(f, anno, aspect)
|
|
||||||
}
|
}
|
||||||
_ => (HSet::default(), 1.0_f32, 1.0_f32),
|
_ => (HSet::default(), 1.0_f32),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drive the per-viewport view_aabb / wpp from the *effective* camera
|
self.resident_wires_for(
|
||||||
// `camera_for_viewport` produces — it folds in the auto-fit
|
|
||||||
// fallback for UTM-style files whose saved `view_target` sits at
|
|
||||||
// empty WCS. Without that, the GPU pass would frustum-cull every
|
|
||||||
// entity (saved-view rect doesn't overlap the offset-subtracted
|
|
||||||
// model cluster) and the viewport would render blank.
|
|
||||||
let Some(cam) = self.camera_for_viewport(vp_handle) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let vp_ortho_h = cam.ortho_size();
|
|
||||||
// Rotation-aware cull box: a twisted/rotated viewport sees a rotated
|
|
||||||
// rectangle in world XY, so derive the box from the camera basis.
|
|
||||||
// `None` (tilted view) disables the cull — render everything.
|
|
||||||
let view_aabb = view_cull_aabb(&cam, vp_aspect, 1.25);
|
|
||||||
// World units per on-screen pixel for LOD substitution + curve
|
|
||||||
// tolerance. Tracks the paper-zoom-driven pixel height the
|
|
||||||
// viewport currently occupies.
|
|
||||||
let wpp = if screen_height_px > 1.0 {
|
|
||||||
Some((2.0 * vp_ortho_h) / screen_height_px)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
self.wires_for_block_culled(
|
|
||||||
self.model_space_block_handle(),
|
self.model_space_block_handle(),
|
||||||
view_aabb,
|
|
||||||
wpp,
|
|
||||||
Some(&frozen),
|
|
||||||
Some(vp_anno_scale),
|
Some(vp_anno_scale),
|
||||||
|
Some(&frozen),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cached per-paper-viewport tessellation. Each viewport's wpp tracks
|
/// Resident model wires for a paper content viewport. Just the unified
|
||||||
/// the on-paper pixel height (paper-zoom dependent), so the cache key
|
/// static-hold (`resident_wires_for`) keyed on the viewport's frozen set +
|
||||||
/// includes a quantized form of that height in addition to the
|
/// annotation scale — no per-viewport height/view cache anymore: the set is
|
||||||
/// geometry epoch — every paper zoom step that actually changes the
|
/// camera-independent (no cull, no LOD), so paper zoom and MSPACE frustum
|
||||||
/// LOD bucket invalidates this viewport's entry.
|
/// changes reuse it as-is, and viewports with default parameters all share
|
||||||
|
/// one entry (and one stable content id).
|
||||||
pub(crate) fn model_wires_for_viewport_arc(
|
pub(crate) fn model_wires_for_viewport_arc(
|
||||||
&self,
|
&self,
|
||||||
vp_handle: Handle,
|
vp_handle: Handle,
|
||||||
screen_height_px: f32,
|
screen_height_px: f32,
|
||||||
) -> Arc<Vec<WireModel>> {
|
) -> Arc<Vec<WireModel>> {
|
||||||
// Drop sub-pixel noise so trivial paper-zoom jitter does not
|
self.model_wires_for_viewport(vp_handle, screen_height_px)
|
||||||
// re-tessellate a 100k-entity drawing every frame; round to an
|
|
||||||
// integer pixel.
|
|
||||||
let height_key = screen_height_px.max(1.0).round() as u32;
|
|
||||||
// Hash the viewport's own view (pan + zoom + orbit) into the key.
|
|
||||||
// Editing inside the viewport (MSPACE) changes its frustum but does NOT
|
|
||||||
// bump geometry_epoch, so without this the stale frustum-culled subset
|
|
||||||
// is returned and newly-revealed lines stay invisible until the layout
|
|
||||||
// re-tessellates. Quantize to ~1 px / fine steps to ignore jitter.
|
|
||||||
let view_key = {
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
|
||||||
if let Some(EntityType::Viewport(vp)) = self.document.get_entity(vp_handle) {
|
|
||||||
let vh = vp.view_height.abs().max(1e-6);
|
|
||||||
let q = vh / (screen_height_px.max(1.0) as f64); // model units / px
|
|
||||||
(((vp.view_target.x + vp.view_center.x) / q).round() as i64).hash(&mut h);
|
|
||||||
(((vp.view_target.y + vp.view_center.y) / q).round() as i64).hash(&mut h);
|
|
||||||
((vh * 1000.0).round() as i64).hash(&mut h);
|
|
||||||
((vp.view_direction.x * 1000.0).round() as i64).hash(&mut h);
|
|
||||||
((vp.view_direction.y * 1000.0).round() as i64).hash(&mut h);
|
|
||||||
((vp.view_direction.z * 1000.0).round() as i64).hash(&mut h);
|
|
||||||
}
|
|
||||||
h.finish()
|
|
||||||
};
|
|
||||||
let key = (self.geometry_epoch, height_key, view_key);
|
|
||||||
{
|
|
||||||
let cache = self.viewport_wire_cache.borrow();
|
|
||||||
if let Some((cached_key, ref arc)) = cache.get(&vp_handle) {
|
|
||||||
if *cached_key == key {
|
|
||||||
return Arc::clone(arc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let arc = Arc::new(self.model_wires_for_viewport(vp_handle, screen_height_px));
|
|
||||||
self.viewport_wire_cache
|
|
||||||
.borrow_mut()
|
|
||||||
.insert(vp_handle, (key, Arc::clone(&arc)));
|
|
||||||
arc
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -848,12 +848,10 @@ impl Scene {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
{
|
{
|
||||||
let cache = self.sdf_text_cache.borrow();
|
let cache = self.sdf_text_cache.borrow();
|
||||||
if let Some((id, verts)) = cache.as_ref() {
|
if let Some(verts) = cache.get(&wire_content_id) {
|
||||||
if *id == wire_content_id {
|
|
||||||
return verts.clone();
|
return verts.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut out: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
let mut out: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||||||
for w in wires {
|
for w in wires {
|
||||||
if !w.text_verts.is_empty() {
|
if !w.text_verts.is_empty() {
|
||||||
|
|
@ -861,7 +859,12 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let verts = Arc::new(out);
|
let verts = Arc::new(out);
|
||||||
*self.sdf_text_cache.borrow_mut() = Some((wire_content_id, verts.clone()));
|
let mut cache = self.sdf_text_cache.borrow_mut();
|
||||||
|
// Ids change on rebuild, so old keys die naturally; cap bounds churn.
|
||||||
|
if cache.len() > 8 {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
cache.insert(wire_content_id, verts.clone());
|
||||||
verts
|
verts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1003,64 +1006,54 @@ impl Scene {
|
||||||
let us = (visible_w / full.width).clamp(0.0, 1.0);
|
let us = (visible_w / full.width).clamp(0.0, 1.0);
|
||||||
let vs = (visible_h / full.height).clamp(0.0, 1.0);
|
let vs = (visible_h / full.height).clamp(0.0, 1.0);
|
||||||
|
|
||||||
// Model tiles all share one resident, camera-independent wire set
|
// EVERY wire source is resident + camera-independent now (unified
|
||||||
// (`model_tile_wires_arc` holds it static). `tile_wire_gen` is that
|
// static-hold, `resident_wires_for`) and stamps its stable
|
||||||
// set's content id: stable across camera moves, so the GPU wire upload
|
// [`WIRE_CONTENT_GEN`] id into `last_model_wire_gen` — Model tiles,
|
||||||
// and the Face3D split below are skipped every frame the geometry is
|
// the paper sheet, content viewports and the pick composite alike. So
|
||||||
// unchanged. The paper-space sources have no stable id and force a
|
// the GPU wire upload, the Face3D split and the SDF text gather below
|
||||||
// re-upload.
|
// are all skipped while the content is unchanged, and
|
||||||
let (base_arc, tile_wire_gen) = if let Some(tile_idx) = inst.tile_idx {
|
// `render_signature` stays stable so paper hits the single-blit scene
|
||||||
|
// cache exactly like Model.
|
||||||
|
let base_arc = if let Some(tile_idx) = inst.tile_idx {
|
||||||
let aspect = if full.height > 0.0 {
|
let aspect = if full.height > 0.0 {
|
||||||
full.width / full.height
|
full.width / full.height
|
||||||
} else {
|
} else {
|
||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
let arc = self.model_tile_wires_arc(tile_idx, &inst.camera, aspect, full.height);
|
self.model_tile_wires_arc(tile_idx, &inst.camera, aspect, full.height)
|
||||||
(arc, Some(self.last_model_wire_gen.get()))
|
|
||||||
} else if inst.paper_sheet {
|
} else if inst.paper_sheet {
|
||||||
// The sheet renders the paper block's own entities + viewport
|
// The sheet renders the paper block's own entities + viewport
|
||||||
// borders — NOT the projected viewport content (the GPU content
|
// borders — NOT the projected viewport content (the GPU content
|
||||||
// viewports draw that themselves).
|
// viewports draw that themselves).
|
||||||
(self.paper_sheet_wires_arc(), None)
|
self.paper_sheet_wires_arc()
|
||||||
} else if inst.handle == acadrust::Handle::NULL {
|
} else if inst.handle == acadrust::Handle::NULL {
|
||||||
(self.entity_wires_arc(), None)
|
self.entity_wires_arc()
|
||||||
} else {
|
} else {
|
||||||
(self.model_wires_for_viewport_arc(inst.handle, full.height), None)
|
self.model_wires_for_viewport_arc(inst.handle, full.height)
|
||||||
};
|
};
|
||||||
// Wire-buffer content id for the upload gate. Preview / interim wires
|
// Wire-buffer content id for the upload gate. Preview / interim wires
|
||||||
// are NOT part of this buffer anymore (they go in a separate per-frame
|
// are NOT part of this buffer anymore (they go in a separate per-frame
|
||||||
// overlay buffer below), so the base id is the stable tile content gen
|
// overlay buffer below), so the base id is the source's stable content
|
||||||
// — a drag no longer re-uploads the whole base wire set every move.
|
// gen — a drag or camera move never re-uploads the base wire set.
|
||||||
// Non-tile paths have no stable id and force a re-upload.
|
let wire_content_id = self.last_model_wire_gen.get();
|
||||||
let wire_content_id = match tile_wire_gen {
|
|
||||||
Some(g) => g,
|
|
||||||
None => {
|
|
||||||
let n = self.wire_force_nonce.get().wrapping_add(1);
|
|
||||||
self.wire_force_nonce.set(n);
|
|
||||||
n | (1u64 << 63)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Split Face3D wires from the rest. The split is content-only (keyed
|
// Split Face3D wires from the rest. The split is content-only (keyed
|
||||||
// by the wire-set content id), so while the geometry is unchanged it's
|
// by the wire-set content id), so while the geometry is unchanged it's
|
||||||
// memoized rather than re-walking every wire (handle lookup + clone)
|
// memoized rather than re-walking every wire (handle lookup + clone)
|
||||||
// each frame. Non-tile paths have no stable id and split inline.
|
// each frame — for every source, since all ids are stable now.
|
||||||
let (face3d_wires, other_arc) = match tile_wire_gen {
|
let (face3d_wires, other_arc) = {
|
||||||
Some(gen) => {
|
let cached = { self.split_cache.borrow().get(&wire_content_id).cloned() };
|
||||||
let cached = {
|
|
||||||
let c = self.split_cache.borrow();
|
|
||||||
c.as_ref().filter(|(g, ..)| *g == gen).map(|(_, f, o)| (f.clone(), o.clone()))
|
|
||||||
};
|
|
||||||
cached.unwrap_or_else(|| {
|
cached.unwrap_or_else(|| {
|
||||||
let (f, o) = split_face3d_wires(&base_arc, &self.document);
|
let (f, o) = split_face3d_wires(&base_arc, &self.document);
|
||||||
let (fa, oa) = (Arc::new(f), Arc::new(o));
|
let (fa, oa) = (Arc::new(f), Arc::new(o));
|
||||||
*self.split_cache.borrow_mut() = Some((gen, fa.clone(), oa.clone()));
|
let mut c = self.split_cache.borrow_mut();
|
||||||
|
// Ids change on rebuild, so old keys die naturally; the cap
|
||||||
|
// just bounds pathological churn.
|
||||||
|
if c.len() > 8 {
|
||||||
|
c.clear();
|
||||||
|
}
|
||||||
|
c.insert(wire_content_id, (fa.clone(), oa.clone()));
|
||||||
(fa, oa)
|
(fa, oa)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let (f, o) = split_face3d_wires(&base_arc, &self.document);
|
|
||||||
(Arc::new(f), Arc::new(o))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
// Base wire set — the cached `other` Arc directly, never cloned to
|
// Base wire set — the cached `other` Arc directly, never cloned to
|
||||||
// append overlays. Preview / interim wires ride in their own small
|
// append overlays. Preview / interim wires ride in their own small
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue