diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index cf474df48..ad4ccf5b4 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -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, diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index a61ea9da5..5c1eb1826 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -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; diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 6d8e1c925..3982957f4 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -194,7 +194,10 @@ struct ExpansionPrototypeKey { #[derive(Debug)] struct CachedExpansion { translation: [f64; 3], + /// The wires every further reference repeats one by one. wires: Arc>, + /// The part a further reference carries as one stub wire, and its XY box. + stubbed: Option<(Arc>, [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 = 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 = 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>, + /// The part a further copy carries as one stub wire, and its XY box. + stubbed: Option<(Arc>, [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, }, ); } diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index ff9e2b0cb..24d6c8ab8 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -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, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index c9162f0b0..b7604f004 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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, 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> { - 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 { @@ -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>, + 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>, + 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 { diff --git a/src/scene/model/instance_model.rs b/src/scene/model/instance_model.rs index c4a19f41a..7213f646f 100644 --- a/src/scene/model/instance_model.rs +++ b/src/scene/model/instance_model.rs @@ -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>, + 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 = 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>, + 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) { + 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(©, 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, + }); + } +} diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 8ee9fbc0c..d0b7f6917 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -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, + /// 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>, } 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(), } diff --git a/src/scene/pick/interaction_index.rs b/src/scene/pick/interaction_index.rs index 1cb8049b8..1b887dde8 100644 --- a/src/scene/pick/interaction_index.rs +++ b/src/scene/pick/interaction_index.rs @@ -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; diff --git a/src/scene/pipeline/gpu_tests.rs b/src/scene/pipeline/gpu_tests.rs index 5812166dd..54e0f8ea0 100644 --- a/src/scene/pipeline/gpu_tests.rs +++ b/src/scene/pipeline/gpu_tests.rs @@ -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, ); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 8341f562f..1b1f26c52 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -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, block_geometry: &mut wire_gpu::BlockGeometryCache, ) -> Vec { @@ -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 = Vec::new(); - let mut block_wires: Vec<&WireModel> = Vec::new(); + let mut block_wires: Vec> = 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, Vec, Vec<&'a WireModel>, - Vec<&'a WireModel>, + Vec>, ) { 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 = Vec::new(); let mut ellipses: Vec = Vec::new(); let mut regular: Vec<&'a WireModel> = Vec::new(); - let mut blocks: Vec<&'a WireModel> = Vec::new(); + let mut blocks: Vec> = 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(); diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index 91c331f3c..0c7178041 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -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>, pub circle_instances: Vec, pub ellipse_instances: Vec, /// 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. diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 54a560397..2a64f1357 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -1043,6 +1043,83 @@ pub fn fast_parse_u64(s: &str) -> Option { /// 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) -> 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>) { + fn walk<'a>( + wire: &'a WireModel, + delta: [f64; 3], + depth_delta: f32, + owner: &'a WireModel, + out: &mut Vec>, + ) { + 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, @@ -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, color_override: Option<[f32; 4]>, target: BlockWireTarget<'_>, @@ -1129,9 +1206,12 @@ impl BlockWireGpu { ) -> Vec { 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>)> = 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, ) -> Vec { 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, ], diff --git a/src/scene/project.rs b/src/scene/project.rs index 2fb8bfd45..a2433eadf 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -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; diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 56a3c6f92..e285497f4 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -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,