perf(blocks): keep repeated block copies as one stub wire each

Every further reference to an already expanded block (a translated copy
of the same definition, at the top level or nested) was materialised as
a full copy of every wire, although the GPU already draws those copies
from one shared source with a per-copy offset. Such a copy is now a
single stub wire holding the shared prototype and its offset (stubs nest
inside prototypes, so nested repeats collapse too).

- The block renderer expands stubs into instance references while it
  groups by source, so drawing is unchanged.
- Interaction queries expand only the stubs near the query, so snapping,
  picking, window and crossing selection see each copy's geometry.
- Plot, boundary detection, viewport projection and entity_wires() get
  fully expanded wires; count and reference-edit recolouring expand the
  set while they are active.
- OCS_NO_BLOCK_STUBS turns it off.

A drawing with 480 references to a block of 100 nested 50-line blocks
(2.4M lines) now opens with 11k wires instead of 2.4M: peak memory
5.4 GB -> 0.37 GB, wire assembly 2.4 s -> 36 ms, interaction index
3.7 s -> 23 ms. The viewport, a plotted PDF, snaps and selections are
identical with and without stubs, and on five real drawings the view
renders the same.

Refs #1698, #1542

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 14:06:27 +03:00
1 parent 8d287decd8
commit 29c0d1df9a
14 files changed
+445 -52

No files matched your search

+1
View File
@@ -2348,6 +2348,7 @@ impl MultiLeaderTess for MultiLeader {
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
stub: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
+1
View File
@@ -1004,6 +1004,7 @@ impl Scene {
} else {
self.paper_sheet_wires_arc()
};
let wires = crate::scene::model::instance_model::expand_stubs(&wires);
for wire in wires.iter() {
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
continue;
+78 -13
View File
@@ -194,7 +194,10 @@ struct ExpansionPrototypeKey {
#[derive(Debug)]
struct CachedExpansion {
translation: [f64; 3],
/// The wires every further reference repeats one by one.
wires: Arc<Vec<WireModel>>,
/// The part a further reference carries as one stub wire, and its XY box.
stubbed: Option<(Arc<Vec<WireModel>>, [f32; 4])>,
}
impl BlockCache {
@@ -1113,13 +1116,23 @@ pub fn expand_insert(
translation[1] - cached.translation[1],
translation[2] - cached.translation[2],
];
return Some(
cached
.wires
.iter()
.map(|wire| translated_prototype_wire(wire, &name, delta))
.collect(),
);
let mut wires: Vec<WireModel> = cached
.wires
.iter()
.map(|wire| translated_prototype_wire(wire, &name, delta))
.collect();
if let Some((prototype, aabb)) = &cached.stubbed {
let mut stub = crate::scene::model::instance_model::stub_wire(
Arc::clone(prototype),
*aabb,
delta,
0.0,
);
stub.name = name.to_string();
stub.selected = selected;
wires.push(stub);
}
return Some(wires);
}
let mut batches = Batches::default();
let mut visited: Vec<String> = vec![ins.block_name.clone()];
@@ -1243,9 +1256,20 @@ pub fn expand_insert(
});
}
}
// Further references carry the block-drawn part as one stub instead
// of a wire per primitive (#1698).
let (stubbed, repeated): (Vec<_>, Vec<_>) = wires
.iter()
.cloned()
.partition(crate::scene::model::instance_model::stubbable);
let stubbed = (!stubbed.is_empty()).then(|| {
let aabb = crate::scene::model::instance_model::prototype_aabb(&stubbed);
(Arc::new(stubbed), aabb)
});
let cached = Arc::new(CachedExpansion {
translation,
wires: Arc::new(wires.clone()),
wires: Arc::new(repeated),
stubbed,
});
**guard = Some(cached);
}
@@ -1312,9 +1336,24 @@ fn expansion_prototype_key(
}
}
fn translated_prototype_wire(source: &WireModel, name: &str, delta: [f64; 3]) -> WireModel {
pub(crate) fn translated_prototype_wire(
source: &WireModel,
name: &str,
delta: [f64; 3],
) -> WireModel {
let mut wire = source.clone();
wire.name = name.to_string();
if let Some(stub) = &source.stub {
wire.stub = Some(Arc::new(crate::scene::model::instance_model::InstanceStub {
prototype: Arc::clone(&stub.prototype),
delta: [
stub.delta[0] + delta[0],
stub.delta[1] + delta[1],
stub.delta[2] + delta[2],
],
depth_delta: stub.depth_delta,
}));
}
if let Some(instance) = wire.render_instance.as_mut() {
for axis in 0..3 {
instance.translation[axis] += delta[axis];
@@ -1623,7 +1662,10 @@ struct NestedPrototypeKey {
struct NestedPrototype {
translation: [f64; 3],
depth_base: f32,
/// The wires every further copy repeats one by one.
wires: Arc<Vec<WireModel>>,
/// The part a further copy carries as one stub wire, and its XY box.
stubbed: Option<(Arc<Vec<WireModel>>, [f32; 4])>,
}
/// Hard cap on point count for a single batched WireModel. Above this the
@@ -1779,6 +1821,7 @@ impl Batches {
fill_is_3d: b.fill_is_3d,
fill_is_2d_solid: b.fill_is_2d_solid,
render_instance: None,
stub: None,
pick_tris: b.pick_tris,
pick_tris_low: b.pick_tris_low,
dash_from_start: false,
@@ -2113,11 +2156,22 @@ fn expand_defn(
let depth_delta = nested_range.0 - cached.depth_base;
out.extra_wires.extend(cached.wires.iter().map(|wire| {
let mut wire = translated_prototype_wire(wire, "", delta);
if let Some(depth) = wire.depth_override.as_mut() {
*depth += depth_delta;
}
crate::scene::model::instance_model::shift_depth(
&mut wire,
depth_delta,
);
wire
}));
if let Some((prototype, aabb)) = &cached.stubbed {
out.extra_wires.push(
crate::scene::model::instance_model::stub_wire(
Arc::clone(prototype),
*aabb,
delta,
depth_delta,
),
);
}
} else {
let mut sub = Batches::default();
expand_defn(
@@ -2145,12 +2199,23 @@ fn expand_defn(
}
}
out.extra_wires.extend(wires.iter().cloned());
// Further copies carry the block-drawn part as one
// stub instead of a wire per primitive (#1698).
let (stubbed, repeated): (Vec<_>, Vec<_>) = wires
.into_iter()
.partition(crate::scene::model::instance_model::stubbable);
let stubbed = (!stubbed.is_empty()).then(|| {
let aabb =
crate::scene::model::instance_model::prototype_aabb(&stubbed);
(Arc::new(stubbed), aabb)
});
out.nested_prototypes.insert(
key,
NestedPrototype {
translation,
depth_base: nested_range.0,
wires: Arc::new(wires),
wires: Arc::new(repeated),
stubbed,
},
);
}
+2
View File
@@ -1812,6 +1812,7 @@ pub fn tessellate(
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
stub: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -1977,6 +1978,7 @@ pub fn tessellate(
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
stub: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
+58 -8
View File
@@ -4495,7 +4495,14 @@ impl Scene {
/// keep set (no-op when not editing). The geometry is untouched, so
/// hit-testing still works on faded entities. In count mode the counted
/// references and the duplicates take the count colours instead.
fn apply_refedit_fade(&self, wires: &mut [WireModel], bg: [f32; 4]) {
fn apply_refedit_fade(&self, wires: &mut Vec<WireModel>, bg: [f32; 4]) {
// Block copies kept as stubs draw in their prototype's colour; while
// copies are recoloured one by one they are expanded instead.
if (self.count_display.is_some() || self.refedit_keep.is_some())
&& wires.iter().any(|wire| wire.stub.is_some())
{
*wires = crate::scene::model::instance_model::expand_stubs(wires).into_owned();
}
if let Some(count) = &self.count_display {
for w in wires.iter_mut() {
let (color, class) = match Self::handle_from_wire_name(&w.name) {
@@ -7819,7 +7826,11 @@ impl Scene {
/// the previous per-call deep clone was a full copy of every wire buffer
/// (hundreds of MB on large mesh imports, #358).
pub fn entity_wires(&self) -> Arc<Vec<WireModel>> {
self.entity_wires_arc()
let wires = self.entity_wires_arc();
match crate::scene::model::instance_model::expand_stubs(&wires) {
std::borrow::Cow::Borrowed(_) => wires,
std::borrow::Cow::Owned(expanded) => Arc::new(expanded),
}
}
/// Return paper entities and projected model-viewport entities separately.
@@ -7843,7 +7854,9 @@ impl Scene {
}
};
if self.current_layout == "Model" {
let mut wires = self.entity_wires_arc().as_ref().clone();
let mut wires =
crate::scene::model::instance_model::expand_stubs(&self.entity_wires_arc())
.into_owned();
apply_mode(
&mut wires,
render_mode_override.unwrap_or_else(|| self.active_model_tile_render_mode()),
@@ -7865,7 +7878,9 @@ impl Scene {
apply_mode(&mut wires, mode);
model_wires.extend(wires);
}
(self.paper_sheet_wires_arc().as_ref().clone(), model_wires)
let paper = crate::scene::model::instance_model::expand_stubs(&self.paper_sheet_wires_arc())
.into_owned();
(paper, model_wires)
}
pub(crate) fn plot_wire_depths(&self, wires: &[WireModel]) -> Vec<f32> {
@@ -10182,6 +10197,17 @@ impl Scene {
aabb: [f64; 4],
allow_pending_empty: bool,
area_only: bool,
) -> crate::scene::pick::interaction_index::InteractionCandidates {
self.indexed_interaction_candidates_xy_stubbed(wires, aabb, allow_pending_empty, area_only)
.materialize_stubs()
}
fn indexed_interaction_candidates_xy_stubbed(
&self,
wires: Arc<Vec<WireModel>>,
aabb: [f64; 4],
allow_pending_empty: bool,
area_only: bool,
) -> crate::scene::pick::interaction_index::InteractionCandidates {
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
let perf = crate::perf::enabled();
@@ -10251,6 +10277,26 @@ impl Scene {
eye: glam::DVec3,
bounds: iced::Rectangle,
allow_pending_empty: bool,
) -> crate::scene::pick::interaction_index::InteractionCandidates {
self.indexed_interaction_candidates_screen_stubbed(
wires,
screen_rect,
view_rot,
eye,
bounds,
allow_pending_empty,
)
.materialize_stubs()
}
fn indexed_interaction_candidates_screen_stubbed(
&self,
wires: Arc<Vec<WireModel>>,
screen_rect: [f32; 4],
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
allow_pending_empty: bool,
) -> crate::scene::pick::interaction_index::InteractionCandidates {
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
let perf = crate::perf::enabled();
@@ -10507,7 +10553,8 @@ impl Scene {
if !self.interaction_source_is_resident(&wires, bounds.height)
|| !self.interaction_index_worthwhile(&wires)
{
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires);
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
.materialize_stubs();
}
let radius_px = self.indexed_interaction_pick_radius(
&wires,
@@ -10521,7 +10568,8 @@ impl Scene {
if !flat_ortho {
let clip = view_rot * (cursor - eye).as_vec3().extend(1.0);
if !clip.is_finite() || clip.w <= 1e-6 {
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires);
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
.materialize_stubs();
}
let ndc = clip.truncate() / clip.w;
let screen = iced::Point::new(
@@ -10551,7 +10599,8 @@ impl Scene {
.sqrt();
let s = world_x_px.min(world_y_px);
if s <= 1e-6 {
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires);
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
.materialize_stubs();
}
let radius = radius_px.max(0.0) as f64 / s as f64;
let query = [
@@ -10587,7 +10636,8 @@ impl Scene {
if !self.interaction_source_is_resident(&wires, bounds.height)
|| !self.interaction_index_worthwhile(&wires)
{
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires);
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires)
.materialize_stubs();
}
let pad_px = self.indexed_interaction_pick_radius(&wires, 0.0, bounds.height, true);
if flat_ortho {
+127
View File
@@ -1,4 +1,7 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use super::wire_model::WireModel;
static NEXT_SOURCE_ID: AtomicU64 = AtomicU64::new(1);
@@ -11,3 +14,127 @@ pub struct RenderInstance {
pub fn next_source_id() -> u64 {
NEXT_SOURCE_ID.fetch_add(1, Ordering::Relaxed)
}
/// Translated copies of one nested block expansion, kept as a single wire
/// instead of one wire per copied primitive (#1698): the copy is `prototype`
/// moved by `delta`, its draw depths shifted by `depth_delta`. Prototype
/// wires are block-instanced (or stubs themselves), so the GPU draws them
/// from the shared source geometry and CPU users expand them on demand.
#[derive(Debug)]
pub struct InstanceStub {
pub prototype: Arc<Vec<WireModel>>,
pub delta: [f64; 3],
pub depth_delta: f32,
}
/// Whether a prototype wire may be represented by a stub: one the block
/// renderer draws from its source geometry alone (no glyphs, fills or
/// markers of its own), or a stub already.
pub fn stubbable(wire: &WireModel) -> bool {
// Kill-switch: `OCS_NO_BLOCK_STUBS` keeps every copy as its own wires.
static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if *OFF.get_or_init(|| std::env::var("OCS_NO_BLOCK_STUBS").is_ok()) {
return false;
}
wire.stub.is_some()
|| (wire.render_instance.is_some()
&& wire.text_verts.is_empty()
&& wire.fill_tris.is_empty()
&& wire.pick_tris.is_empty()
&& !wire.fill_is_3d
&& wire.point_marker.is_none()
&& wire.searchable_text.is_empty())
}
/// The XY box a prototype's wires cover, before any copy's offset.
pub fn prototype_aabb(prototype: &[WireModel]) -> [f32; 4] {
prototype.iter().fold(
[f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY],
|acc, wire| {
[
acc[0].min(wire.aabb[0]),
acc[1].min(wire.aabb[1]),
acc[2].max(wire.aabb[2]),
acc[3].max(wire.aabb[3]),
]
},
)
}
/// A stub wire standing for `prototype` moved by `delta`.
pub fn stub_wire(
prototype: Arc<Vec<WireModel>>,
prototype_aabb: [f32; 4],
delta: [f64; 3],
depth_delta: f32,
) -> WireModel {
let aabb = [
(prototype_aabb[0] as f64 + delta[0]) as f32,
(prototype_aabb[1] as f64 + delta[1]) as f32,
(prototype_aabb[2] as f64 + delta[0]) as f32,
(prototype_aabb[3] as f64 + delta[1]) as f32,
];
WireModel {
aabb,
stub: Some(Arc::new(InstanceStub {
prototype,
delta,
depth_delta,
})),
..Default::default()
}
}
/// Every wire `wires` stands for, stubs expanded into translated copies
/// named and selected like the stub that holds them.
pub fn expand_stubs(wires: &[WireModel]) -> std::borrow::Cow<'_, [WireModel]> {
if !wires.iter().any(|wire| wire.stub.is_some()) {
return std::borrow::Cow::Borrowed(wires);
}
let mut out = Vec::with_capacity(wires.len());
for wire in wires {
if wire.stub.is_some() {
expand_stub_into(wire, &mut out);
} else {
out.push(wire.clone());
}
}
std::borrow::Cow::Owned(out)
}
/// Append the translated copies a stub wire stands for.
pub fn expand_stub_into(stub_wire: &WireModel, out: &mut Vec<WireModel>) {
let Some(stub) = stub_wire.stub.as_ref() else {
out.push(stub_wire.clone());
return;
};
for wire in stub.prototype.iter() {
let mut copy = crate::scene::cache::block_cache::translated_prototype_wire(
wire,
&stub_wire.name,
stub.delta,
);
shift_depth(&mut copy, stub.depth_delta);
copy.selected = stub_wire.selected;
copy.display_visible &= stub_wire.display_visible;
if copy.stub.is_some() {
expand_stub_into(&copy, out);
} else {
out.push(copy);
}
}
}
/// Shift a block-local draw depth by `delta`, through a stub as well.
pub fn shift_depth(wire: &mut WireModel, delta: f32) {
if let Some(depth) = wire.depth_override.as_mut() {
*depth += delta;
}
if let Some(stub) = wire.stub.as_mut() {
*stub = Arc::new(InstanceStub {
prototype: Arc::clone(&stub.prototype),
delta: stub.delta,
depth_delta: stub.depth_delta + delta,
});
}
}
+5
View File
@@ -836,6 +836,9 @@ pub struct WireModel {
pub fill_is_2d_solid: bool,
/// Shared block-render source and this placement's translation.
pub render_instance: Option<super::instance_model::RenderInstance>,
/// Set on a wire that stands for a translated copy of a nested block
/// expansion; its own geometry is empty (see `InstanceStub`).
pub stub: Option<std::sync::Arc<super::instance_model::InstanceStub>>,
}
impl WireModel {
@@ -911,6 +914,7 @@ impl WireModel {
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
stub: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
text_verts: Vec::new(),
@@ -1448,6 +1452,7 @@ impl Default for WireModel {
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
stub: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
}
+47
View File
@@ -1646,6 +1646,45 @@ impl InteractionCandidates {
.map(|(rect, (view, eye, bounds))| (rect, view, eye, bounds))
}
/// The candidates with every block-copy stub among them expanded into
/// the wires it stands for, so snapping and picking see each copy. Only
/// the copies' wires near an XY query are kept.
pub(crate) fn materialize_stubs(self) -> Self {
if !self.iter().any(|wire| wire.stub.is_some()) {
return self;
}
let near = |wire: &WireModel, query: [f64; 4]| {
let [x0, y0, x1, y1] = wire.aabb.map(f64::from);
!(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite())
|| (x1 >= query[0] && x0 <= query[2] && y1 >= query[1] && y0 <= query[3])
};
let mut wires = Vec::new();
for wire in self.iter() {
if wire.stub.is_none() {
wires.push(wire.clone());
continue;
}
let start = wires.len();
crate::scene::model::instance_model::expand_stub_into(wire, &mut wires);
if let Some(query) = self.query_aabb {
let mut keep = start;
for index in start..wires.len() {
if near(&wires[index], query) {
wires.swap(keep, index);
keep += 1;
}
}
wires.truncate(keep);
}
}
Self {
query_aabb: self.query_aabb,
screen_rect: self.screen_rect,
screen_view: self.screen_view,
..Self::all(Arc::new(wires))
}
}
/// Leave out every wire `drop` matches, with the segments, points and
/// triangles that belong to it.
pub(crate) fn exclude_wires(&mut self, drop: impl Fn(&WireModel) -> bool) {
@@ -1912,6 +1951,14 @@ fn finite_wire_aabb3(wire: &WireModel) -> Option<[f64; 6]> {
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
]);
}
if let Some(stub) = &wire.stub {
for inner in stub.prototype.iter() {
if let Some(inner) = finite_wire_aabb3(inner) {
aabb[2] = aabb[2].min(inner[2] + stub.delta[2]);
aabb[5] = aabb[5].max(inner[5] + stub.delta[2]);
}
}
}
if !aabb[2].is_finite() {
aabb[2] = 0.0;
aabb[5] = 0.0;
+17 -7
View File
@@ -71,35 +71,45 @@ fn block_edits_preserve_cache_coordinates_and_arena_partition() {
let original = pipeline.upload_block_wires(
&device,
&queue,
&[&first, &second],
&[(&first).into(), (&second).into()],
&depth,
&mut block_geometry,
);
let unchanged = pipeline.upload_block_wires(
&device,
&queue,
&[&first, &second],
&[(&first).into(), (&second).into()],
&depth,
&mut block_geometry,
);
assert_eq!(original[0].geometry_id(), unchanged[0].geometry_id());
assert_eq!(original[0].instance_count, 2);
let removed =
pipeline.upload_block_wires(&device, &queue, &[&second], &depth, &mut block_geometry);
let removed = pipeline.upload_block_wires(
&device,
&queue,
&[(&second).into()],
&depth,
&mut block_geometry,
);
assert_ne!(
original[0].geometry_id(),
removed[0].geometry_id(),
"removing the base instance must replace vertices baked at its old position"
);
let moved = block(2, 30.0);
let moved_gpu =
pipeline.upload_block_wires(&device, &queue, &[&moved], &depth, &mut block_geometry);
let moved_gpu = pipeline.upload_block_wires(
&device,
&queue,
&[(&moved).into()],
&depth,
&mut block_geometry,
);
assert_ne!(removed[0].geometry_id(), moved_gpu[0].geometry_id());
let restored = pipeline.upload_block_wires(
&device,
&queue,
&[&first, &second],
&[(&first).into(), (&second).into()],
&depth,
&mut block_geometry,
);
+7 -7
View File
@@ -2486,7 +2486,7 @@ impl Pipeline {
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[&WireModel],
wires: &[wire_gpu::BlockRef<'_>],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
block_geometry: &mut wire_gpu::BlockGeometryCache,
) -> Vec<BlockWireGpu> {
@@ -2620,7 +2620,7 @@ impl Pipeline {
// and alpha blending both depend on it. Scissor and mesh-edge stay
// grouping keys because the draw loop sets one scissor per batch and
let mut batches: Vec<WireGpu> = Vec::new();
let mut block_wires: Vec<&WireModel> = Vec::new();
let mut block_wires: Vec<wire_gpu::BlockRef<'_>> = Vec::new();
let mut i = 0;
while i < wires.len() {
let kind = wire_arena::classify_wire(&wires[i]);
@@ -2633,7 +2633,7 @@ impl Pipeline {
continue;
}
wire_arena::WireKind::Block => {
block_wires.push(&wires[i]);
wire_gpu::push_block_refs(&wires[i], &mut block_wires);
i += 1;
continue;
}
@@ -2721,7 +2721,7 @@ impl Pipeline {
Vec<CircleInstance>,
Vec<EllipseInstance>,
Vec<&'a WireModel>,
Vec<&'a WireModel>,
Vec<wire_gpu::BlockRef<'a>>,
) {
use crate::par::prelude::*;
// Pure per-wire work, so it fans out; `collect` on an indexed
@@ -2734,7 +2734,7 @@ impl Pipeline {
)> = wires
.par_iter()
.map(|&wire| {
if wire.render_instance.is_some() {
if wire.render_instance.is_some() || wire.stub.is_some() {
return (None, None, true);
}
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
@@ -2763,10 +2763,10 @@ impl Pipeline {
let mut circles: Vec<CircleInstance> = Vec::new();
let mut ellipses: Vec<EllipseInstance> = Vec::new();
let mut regular: Vec<&'a WireModel> = Vec::new();
let mut blocks: Vec<&'a WireModel> = Vec::new();
let mut blocks: Vec<wire_gpu::BlockRef<'a>> = Vec::new();
for (&wire, (wire_circles, wire_ellipses, is_block)) in wires.iter().zip(per) {
if is_block {
blocks.push(wire);
wire_gpu::push_block_refs(wire, &mut blocks);
continue;
}
let shaped = wire_circles.is_some() || wire_ellipses.is_some();
+4 -4
View File
@@ -146,7 +146,7 @@ pub fn classify_wire(wire: &WireModel) -> WireKind {
if !wire.display_visible {
return WireKind::Invisible;
}
if wire.render_instance.is_some() {
if wire.render_instance.is_some() || wire.stub.is_some() {
return WireKind::Block;
}
if !wire.tangent_geoms.is_empty()
@@ -176,7 +176,7 @@ pub fn classify_wire(wire: &WireModel) -> WireKind {
pub struct PartitionedWires<'a> {
pub regular: Vec<&'a WireModel>,
pub mesh: Vec<&'a WireModel>,
pub instanced: Vec<&'a WireModel>,
pub instanced: Vec<super::wire_gpu::BlockRef<'a>>,
pub circle_instances: Vec<super::circle_gpu::CircleInstance>,
pub ellipse_instances: Vec<super::ellipse_gpu::EllipseInstance>,
/// Handles that contributed to the retained analytical uploads.
@@ -213,11 +213,11 @@ pub fn partition_wires<'a>(
if !wire.display_visible {
continue;
}
if wire.render_instance.is_some() {
if wire.render_instance.is_some() || wire.stub.is_some() {
if let Some(handle) = handle_of(wire) {
contributors.insert(handle);
}
instanced.push(wire);
super::wire_gpu::push_block_refs(wire, &mut instanced);
continue;
}
// Resolve draw depth only for wires eligible for analytical extraction.
+96 -13
View File
@@ -1043,6 +1043,83 @@ pub fn fast_parse_u64(s: &str) -> Option<u64> {
/// wires that carry no document handle). A wire carrying a block-local
/// `depth_override` (a wide-polyline band inside a block) composes it into
/// the insert's own sub-range so the band orders against its block siblings.
/// One block-instanced wire to draw: a wire of the set, or a prototype wire
/// reached through a stub, moved by `delta` with its depth shifted by
/// `depth_delta` and owned (named, depth-ordered) by the stub `owner`.
#[derive(Clone, Copy)]
pub struct BlockRef<'a> {
pub wire: &'a WireModel,
pub delta: [f64; 3],
pub depth_delta: f32,
pub owner: Option<&'a WireModel>,
}
impl<'a> From<&'a WireModel> for BlockRef<'a> {
fn from(wire: &'a WireModel) -> Self {
Self {
wire,
delta: [0.0; 3],
depth_delta: 0.0,
owner: None,
}
}
}
impl<'a> BlockRef<'a> {
/// The wire whose name and visibility stand for this copy.
pub fn named(&self) -> &'a WireModel {
self.owner.unwrap_or(self.wire)
}
/// The copy's draw depth: its owner's slot, the prototype wire's
/// block-local depth shifted for this copy.
pub fn draw_depth(&self, depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>) -> f32 {
let base = fast_parse_u64(&self.named().name).and_then(|h| depth_map.get(&h).copied());
match (base, self.wire.depth_override) {
(Some([d, half]), Some(local)) => d + (local + self.depth_delta) * half,
(Some([d, _]), None) => d,
(None, _) => 0.0,
}
}
}
/// Push the block-instanced wires `wire` stands for: itself, or every copy a
/// stub holds (stubs inside stubs included).
pub fn push_block_refs<'a>(wire: &'a WireModel, out: &mut Vec<BlockRef<'a>>) {
fn walk<'a>(
wire: &'a WireModel,
delta: [f64; 3],
depth_delta: f32,
owner: &'a WireModel,
out: &mut Vec<BlockRef<'a>>,
) {
match &wire.stub {
Some(stub) => {
let delta = [
delta[0] + stub.delta[0],
delta[1] + stub.delta[1],
delta[2] + stub.delta[2],
];
for inner in stub.prototype.iter() {
walk(inner, delta, depth_delta + stub.depth_delta, owner, out);
}
}
None if wire.render_instance.is_some() => out.push(BlockRef {
wire,
delta,
depth_delta,
owner: Some(owner),
}),
None => {}
}
}
if wire.stub.is_some() {
walk(wire, [0.0; 3], 0.0, wire, out);
} else if wire.render_instance.is_some() {
out.push(BlockRef::from(wire));
}
}
pub(crate) fn wire_draw_depth(
wire: &WireModel,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
@@ -1121,7 +1198,7 @@ impl BlockWireGpu {
pub fn from_wires(
device: &wgpu::Device,
queue: &wgpu::Queue,
wires: &[&WireModel],
wires: &[BlockRef<'_>],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
color_override: Option<[f32; 4]>,
target: BlockWireTarget<'_>,
@@ -1129,9 +1206,12 @@ impl BlockWireGpu {
) -> Vec<Self> {
let BlockWireTarget { const_bgl, mode } = target;
let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> = rustc_hash::FxHashMap::default();
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
for &wire in wires {
if color_override.is_none() && !wire.display_visible {
let mut groups: Vec<(bool, Vec<BlockRef<'_>>)> = Vec::new();
for &block in wires {
let wire = block.wire;
if color_override.is_none()
&& !(wire.display_visible && block.named().display_visible)
{
continue;
}
let Some(instance) = wire.render_instance else {
@@ -1147,7 +1227,7 @@ impl BlockWireGpu {
groups.push((mesh_edge, Vec::new()));
slot
});
groups[slot].1.push(wire);
groups[slot].1.push(block);
}
let mut arena = BlockArenaBuilder::new(device, mode);
@@ -1157,7 +1237,10 @@ impl BlockWireGpu {
// visible rather than assumed.
let mut emitted_segments = 0usize;
for (mesh_edge, group) in groups {
let Some(&source) = group.first() else {
// A copy reached through a stub carries the prototype's
// geometry at the prototype's placement, which is what `base`
// means; its own `delta` only moves the instance.
let Some(source) = group.first().map(|block| block.wire) else {
continue;
};
let Some(base) = source.render_instance else {
@@ -1460,19 +1543,19 @@ impl BlockArenaBuilder {
/// Placements and draw depths relative to the group's first instance.
fn block_instances(
group: &[&WireModel],
group: &[BlockRef<'_>],
base: crate::scene::model::instance_model::RenderInstance,
mesh_edge: bool,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Vec<BlockWireInstance> {
group
.iter()
.filter_map(|wire| {
let instance = wire.render_instance?;
.filter_map(|block| {
let instance = block.wire.render_instance?;
let delta = [
instance.translation[0] - base.translation[0],
instance.translation[1] - base.translation[1],
instance.translation[2] - base.translation[2],
instance.translation[0] + block.delta[0] - base.translation[0],
instance.translation[1] + block.delta[1] - base.translation[1],
instance.translation[2] + block.delta[2] - base.translation[2],
];
let high = delta.map(|value| value as f32);
Some(BlockWireInstance {
@@ -1486,7 +1569,7 @@ fn block_instances(
if mesh_edge {
0.0
} else {
wire_draw_depth(wire, depth_map)
block.draw_depth(depth_map)
},
0.0,
],
+1
View File
@@ -84,6 +84,7 @@ impl Scene {
// path wants the full-fidelity (no-LOD-stub) wire list,
// regardless of paper zoom.
let model_wires = self.model_wires_for_viewport_arc(vp_handle, 0.0);
let model_wires = crate::scene::model::instance_model::expand_stubs(&model_wires);
// ── Project and clip wires into viewport ──────────────────────
let vp_x0 = pcx - hw;
+1
View File
@@ -386,6 +386,7 @@ pub fn apply_along(
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
stub: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,