From e177f92ff585f492c1236edb37d0c10707c5415a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 15:53:28 +0300 Subject: [PATCH] fix(xref): blocks respect their internal draw order Xref merge now inserts entities in the source block's chain order and carries SortEntitiesTable objects across (remapped to merged handles). draw_depth_map values become [depth, half] so exploded block/xref fills and wide-polyline band wires both compose child depth as insert_depth + in-block rank x half, recursing into nested inserts with the same divisor - bands and fills from one block interleave by the block's internal order instead of stacking at the insert's single depth. Band batches split per rank via WireModel.depth_override; thin wires stay merged. The wire arena falls back to a full rebuild when a slab carries mixed depths so refreshes don't flatten band overrides. Co-Authored-By: Claude Fable 5 --- src/app/update/viewport.rs | 2 + src/entities/dimension.rs | 5 ++ src/entities/leader.rs | 2 + src/entities/multileader.rs | 4 ++ src/entities/table.rs | 1 + src/io/xref.rs | 81 ++++++++++++++++++++-- src/modules/annotate/aligned_dim.rs | 2 + src/modules/annotate/angular_dim.rs | 1 + src/modules/annotate/diameter_dim.rs | 1 + src/modules/annotate/dim_baseline.rs | 1 + src/modules/annotate/dim_continue.rs | 1 + src/modules/annotate/dimjogline.rs | 1 + src/modules/annotate/dimtedit.rs | 1 + src/modules/annotate/leader_cmd.rs | 1 + src/modules/annotate/linear_dim.rs | 1 + src/modules/annotate/mleader_cmd.rs | 1 + src/modules/annotate/mleader_edit.rs | 1 + src/modules/annotate/ordinate_dim.rs | 1 + src/modules/annotate/qdim.rs | 1 + src/modules/annotate/radius_dim.rs | 1 + src/modules/annotate/table_cmd.rs | 1 + src/modules/annotate/tolerance_cmd.rs | 1 + src/modules/draw/draw/attdef.rs | 1 + src/modules/draw/draw/mline.rs | 1 + src/modules/draw/draw/raster_image.rs | 1 + src/modules/draw/draw/ray.rs | 2 + src/modules/draw/draw/revcloud.rs | 1 + src/modules/draw/draw/wipeout.rs | 2 + src/modules/draw/inquiry/area.rs | 1 + src/modules/draw/inquiry/dist.rs | 1 + src/modules/draw/inquiry/measuregeom.rs | 1 + src/modules/draw/modify/trim.rs | 1 + src/modules/layout/mview.rs | 1 + src/modules/model/primitive_cmd.rs | 1 + src/modules/view/plot_window.rs | 1 + src/modules/view/zoom_window.rs | 1 + src/scene/cache/block_cache.rs | 89 +++++++++++++++++++++++-- src/scene/convert/tess.rs | 4 ++ src/scene/convert/tessellate.rs | 18 +++++ src/scene/entity.rs | 48 +++++++++++-- src/scene/mod.rs | 25 ++++--- src/scene/model/wire_model.rs | 10 +++ src/scene/pipeline/face3d_gpu.rs | 11 +-- src/scene/pipeline/mod.rs | 8 +-- src/scene/pipeline/wire_arena.rs | 22 ++++-- src/scene/pipeline/wire_gpu.rs | 20 ++++-- src/scene/text/complex_lt.rs | 2 + src/scene/view/render.rs | 2 +- 48 files changed, 337 insertions(+), 50 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 391be617e..526e96846 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1237,6 +1237,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let guide = crate::scene::WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1277,6 +1278,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { previews.push(crate::scene::WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 5facf81c8..a30819e60 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1354,6 +1354,7 @@ fn tessellate_dimension_inner( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1382,6 +1383,7 @@ fn tessellate_dimension_inner( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1410,6 +1412,7 @@ fn tessellate_dimension_inner( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1439,6 +1442,7 @@ fn tessellate_dimension_inner( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1484,6 +1488,7 @@ fn tessellate_dimension_inner( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/entities/leader.rs b/src/entities/leader.rs index a210e9b6e..770ea0a7e 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -592,6 +592,7 @@ impl LeaderTess for Leader { return WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -700,6 +701,7 @@ impl LeaderTess for Leader { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index bec019471..ba64adeb3 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1406,6 +1406,7 @@ impl MultiLeaderTess for MultiLeader { wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1697,6 +1698,7 @@ impl MultiLeaderTess for MultiLeader { wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1766,6 +1768,7 @@ impl MultiLeaderTess for MultiLeader { wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1803,6 +1806,7 @@ impl MultiLeaderTess for MultiLeader { wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/entities/table.rs b/src/entities/table.rs index d754f94f3..60cbe0283 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -755,6 +755,7 @@ pub fn tessellate_table( WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/io/xref.rs b/src/io/xref.rs index 6628c0808..11e58fb2d 100644 --- a/src/io/xref.rs +++ b/src/io/xref.rs @@ -5,7 +5,7 @@ use acadrust::entities::{Block, BlockEnd}; use acadrust::tables::TableEntry; use acadrust::types::{Handle, Vector3}; use acadrust::{CadDocument, EntityType}; -use rustc_hash::FxHashMap as HashMap; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::path::{Path, PathBuf}; /// Status of an external reference block. @@ -234,14 +234,40 @@ fn merge_xref_into_block( // ── Entities ──────────────────────────────────────────────────────── let xref_ms_handle = xref_doc.header.model_space_block_handle; - let entities: Vec = xref_doc - .entities() - .filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_))) - .cloned() + // Merge in each block's DISPLAY order: a block record's entity_handles + // chain is the draw order inside that block, while the flat entity list + // is stream order — merging in stream order scrambled the xref's own + // stacking (fills over outlines etc.), since the host ranks draw order + // by the freshly-allocated handles. Entities missing from every chain + // keep their stream order at the end. + let ordered: Vec = { + let mut ordered: Vec = Vec::new(); + let mut seen: HashSet = HashSet::default(); + for br in xref_doc.block_records.iter() { + for &h in &br.entity_handles { + if seen.insert(h) { + ordered.push(h); + } + } + } + for e in xref_doc.entities() { + let h = e.common().handle; + if seen.insert(h) { + ordered.push(h); + } + } + ordered + }; + let entities: Vec<(Handle, EntityType)> = ordered + .into_iter() + .filter_map(|h| xref_doc.get_entity(h).map(|e| (h, e.clone()))) + .filter(|(_, e)| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_))) .collect(); + // old xref-doc entity handle → merged host handle, for the sortents copy. + let mut entity_handle_map: HashMap = HashMap::default(); let mut dropped = 0usize; - for mut entity in entities { + for (old_h, mut entity) in entities { // Drop parser-garbage entities (bad normals / vertex counts / inf // coords) before they enter the host doc — they trigger huge // allocations and unbounded recursion in the wire pipeline. @@ -285,7 +311,48 @@ fn merge_xref_into_block( entity.common_mut().owner_handle = new_owner; // Clear the foreign handle so acadrust assigns a new one. set_handle(&mut entity, Handle::NULL); - let _ = doc.add_entity(entity); + if let Ok(new_h) = doc.add_entity(entity) { + entity_handle_map.insert(old_h, new_h); + } + } + + // ── Draw-order overrides ──────────────────────────────────────────── + // Re-point the xref's SortEntitiesTables at the merged blocks with the + // merged entity handles, so an explicit DRAWORDER inside the xref + // survives the merge. + { + use acadrust::objects::{ObjectType, SortEntitiesTable}; + for obj in xref_doc.objects.values() { + let ObjectType::SortEntitiesTable(t) = obj else { + continue; + }; + if t.is_empty() { + continue; + } + let new_block = if t.block_owner_handle == xref_ms_handle { + br_handle + } else if let Some(&h) = br_handle_map.get(&t.block_owner_handle) { + h + } else { + continue; + }; + let mut nt = SortEntitiesTable::new(); + nt.handle = doc.allocate_handle(); + nt.block_owner_handle = new_block; + for e in t.entries() { + let Some(&ne) = entity_handle_map.get(&e.entity_handle) else { + continue; + }; + let ns = entity_handle_map + .get(&e.sort_handle) + .copied() + .unwrap_or(e.sort_handle); + nt.add_entry(ne, ns); + } + if !nt.is_empty() { + doc.objects.insert(nt.handle, ObjectType::SortEntitiesTable(nt)); + } + } } dropped } diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 1ab06c4ea..9706ae98e 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -188,6 +188,7 @@ impl CadCommand for AlignedDimensionCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -241,6 +242,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 0efb9e25d..057abaf3b 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -132,6 +132,7 @@ fn preview_wire(points: Vec) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 902f838d9..5c772aaa4 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -182,6 +182,7 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index c36bcd766..f13d1801f 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -181,6 +181,7 @@ impl CadCommand for DimBaselineCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 7de6d2c9e..0aea7b55a 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -161,6 +161,7 @@ impl CadCommand for DimContinueCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index b9169d837..4374955d9 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -87,6 +87,7 @@ impl CadCommand for DimJogLineCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index b5169d7ca..b9bd23e37 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -102,6 +102,7 @@ impl CadCommand for DimTeditCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index e92a2e1f6..431708931 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -165,6 +165,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 5d9806306..b784124a7 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -220,6 +220,7 @@ fn preview_wire(points: Vec) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index 38ea27270..73345a500 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -152,6 +152,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index fa24b8ef3..91bc05e2d 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -460,6 +460,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index bef5171f3..6df46b389 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -123,6 +123,7 @@ fn preview_wire(points: Vec) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index 94faefd61..9ffd83df5 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -92,6 +92,7 @@ impl CadCommand for QdimCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 4018edae7..57b1d39de 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -187,6 +187,7 @@ fn preview_wire(points: Vec) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index 0dd6ea203..10c17ea76 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -141,6 +141,7 @@ impl CadCommand for TableCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 918512f0c..93ebf0d39 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -86,6 +86,7 @@ impl CadCommand for ToleranceCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index c7739b73b..e50c319bd 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -162,6 +162,7 @@ impl CadCommand for AttdefCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index ca035238f..f6dff9352 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -138,6 +138,7 @@ impl CadCommand for MlineCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index f1c8fb3e2..c0ec58a77 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -110,6 +110,7 @@ impl CadCommand for ImageCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index efb4187f9..24b00dfad 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -86,6 +86,7 @@ impl CadCommand for RayCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -183,6 +184,7 @@ impl CadCommand for XLineCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/draw/revcloud.rs b/src/modules/draw/draw/revcloud.rs index 5c0209faf..9b7883abc 100644 --- a/src/modules/draw/draw/revcloud.rs +++ b/src/modules/draw/draw/revcloud.rs @@ -91,6 +91,7 @@ impl CadCommand for RevCloudCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 1377782be..bce92b5b2 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -116,6 +116,7 @@ impl CadCommand for WipeoutCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -164,6 +165,7 @@ impl CadCommand for WipeoutCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 605e48cc8..9931fdcc1 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -71,6 +71,7 @@ impl CadCommand for AreaCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index ca00daab1..c77e7a0e2 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -65,6 +65,7 @@ impl CadCommand for DistCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index 569d40d59..6ae1e419e 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -69,6 +69,7 @@ impl MeasureGeomCommand { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index d00e1f36c..8575e550e 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3135,6 +3135,7 @@ fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index fa94c9eee..e493b3212 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -83,6 +83,7 @@ impl CadCommand for MviewCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 17ce79684..46c447746 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -307,6 +307,7 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index a9355d2aa..82fe34210 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -50,6 +50,7 @@ impl CadCommand for PlotWindowCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index e689b8a3b..85f896ef8 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -67,6 +67,7 @@ impl CadCommand for ZoomWindowCommand { Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index fc4da7723..b26fbc117 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -85,6 +85,11 @@ pub struct LocalWire { /// expand-time: transform corners by the Insert transform → world AABB /// → test against the camera's world-space view rect. pub aabb_local: [f32; 4], + /// This child's signed draw-order rank in (-1,1) within its owning block + /// (from the scene draw-depth map, so SortEntitiesTable overrides apply). + /// Composed at expand time into a `depth_override` for wide-polyline + /// bands so a band orders against its block siblings. + pub local_rank: f32, } #[derive(Clone, Debug)] @@ -119,6 +124,9 @@ pub struct NestedRef { /// expansion it is mapped to world by the accumulated transform and the /// nested wires are clipped to it. pub clip_poly: Option>, + /// The nested INSERT's own signed draw-order rank in (-1,1) within the + /// parent block — narrows the depth sub-range its children may occupy. + pub local_rank: f32, } #[derive(Clone, Debug)] @@ -135,6 +143,11 @@ pub struct BlockDefn { /// the wire renderer is 2D-dominant. Expressed in this defn's *offset* /// Absolute world-space XY (the double-single render path keeps it precise). pub aabb_local: [f32; 4], + /// Raw entity count of the source block record (`entity_handles.len()`). + /// Divisor for nested depth composition: a nested insert's children get a + /// sub-range of `parent_scale / (child_count + 1)` — the same formula the + /// exploded-fill path uses, so bands and fills stay on one depth scale. + pub child_count: usize, } #[derive(Default, Debug)] @@ -156,7 +169,15 @@ impl BlockCache { /// included too. The Model_Space / Paper_Space block_records are skipped /// because their entities are emitted as top-level wires, not via the /// cache. - pub fn build(doc: &CadDocument, anno_scale: f32, bg_color: [f32; 4]) -> Self { + pub fn build( + doc: &CadDocument, + anno_scale: f32, + bg_color: [f32; 4], + // Scene draw-depth map ([depth, half] per handle) — source of each + // block child's in-block rank, so band depth composition agrees with + // the SortEntitiesTable-aware ranking the fill explosion uses. + depth_map: &HashMap, + ) -> Self { use crate::par::prelude::*; let mut cache = Self::new(); let referenced = collect_referenced_blocks(doc); @@ -168,7 +189,12 @@ impl BlockCache { // post-pass (it resolves nested references and is comparatively cheap). cache.defns = referenced .par_iter() - .map(|name| (name.clone(), Arc::new(build_defn(doc, name, anno_scale, bg_color)))) + .map(|name| { + ( + name.clone(), + Arc::new(build_defn(doc, name, anno_scale, bg_color, depth_map)), + ) + }) .collect(); cache.compute_block_aabbs(&referenced); cache @@ -285,6 +311,7 @@ fn build_defn( block_name: &str, anno_scale: f32, bg_color: [f32; 4], + depth_map: &HashMap, ) -> BlockDefn { let br = match doc.block_records.get(block_name) { Some(br) => br, @@ -326,14 +353,16 @@ fn build_defn( continue } EntityType::Insert(nested_ins) => { - subs.push(LocalSub::Nested(build_nested_ref(nested_ins, doc, bg_color))); + subs.push(LocalSub::Nested(build_nested_ref( + nested_ins, doc, bg_color, depth_map, + ))); } _ => { // A wide polyline inside a block carries its `world_width` on // the LocalWire; `emit_wire` scales it by the insert transform // so the shader band matches the scaled geometry (same band the // top-level path draws — depth-tested + linetype-dashed). - for lw in tessellate_sub_local(doc, entity, anno_scale, bg_color) { + for lw in tessellate_sub_local(doc, entity, anno_scale, bg_color, depth_map) { subs.push(LocalSub::Wire(lw)); } } @@ -342,6 +371,7 @@ fn build_defn( BlockDefn { subs, aabb_local: [0.0; 4], + child_count: br.entity_handles.len(), } } @@ -349,6 +379,7 @@ fn build_nested_ref( nested_ins: &acadrust::entities::Insert, doc: &CadDocument, bg_color: [f32; 4], + depth_map: &HashMap, ) -> NestedRef { // Store the RAW colour — `adapt_to_bg` runs at emit time // (`Batches::finalize`) so the same cached defn can serve renders @@ -389,6 +420,9 @@ fn build_nested_ref( l0, instance_offsets: array_offsets(nested_ins), clip_poly, + local_rank: depth_map + .get(&nested_ins.common.handle.value()) + .map_or(0.0, |d| d[0]), } } @@ -397,6 +431,7 @@ fn tessellate_sub_local( sub: &EntityType, anno_scale: f32, bg_color: [f32; 4], + depth_map: &HashMap, ) -> Vec { let h = sub.common().handle; @@ -505,6 +540,7 @@ fn tessellate_sub_local( lt_l0, lw_l0, aabb_local, + local_rank: depth_map.get(&h.value()).map_or(0.0, |d| d[0]), }); } result @@ -693,7 +729,7 @@ pub fn expand_insert( is_xref, bg_color, }; - expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0); + expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0)); } Some(batches.finalize(&name, selected, bg_color)) } @@ -758,6 +794,10 @@ struct StyleKey { /// discriminator, so greek fills must stay in their own batches even /// when their color/style would otherwise collide with regular wires. is_fill_only: bool, + /// Bit-cast composed block-local depth for band wires (`0` = no override). + /// Keeps bands of different in-block draw ranks in separate batches so + /// each finalized WireModel carries one correct `depth_override`. + depth_bits: u32, } #[derive(Default, Debug)] @@ -769,6 +809,9 @@ struct BatchEntry { world_width: f32, aci: u8, plinegen: bool, + /// Composed block-local draw-order offset for a band batch (see + /// `WireModel::depth_override`). `None` for every other batch. + local_depth: Option, points: Vec<[f32; 3]>, points_low: Vec<[f32; 3]>, snap_pts: Vec<(glam::DVec3, SnapHint)>, @@ -866,6 +909,7 @@ impl Batches { WireModel { taper_widths: Vec::new(), world_width: b.world_width, + depth_override: b.local_depth, fill_is_3d: false, pick_tris: b.pick_tris, pick_tris_low: b.pick_tris_low, @@ -911,6 +955,7 @@ fn style_key( aci: u8, plinegen: bool, is_fill_only: bool, + local_depth: Option, ) -> StyleKey { StyleKey { color: [ @@ -935,6 +980,7 @@ fn style_key( aci, plinegen, is_fill_only, + depth_bits: local_depth.map_or(0, f32::to_bits), } } @@ -945,6 +991,10 @@ fn expand_defn( out: &mut Batches, visited: &mut Vec, depth: usize, + // Block-local depth sub-range `(base, scale)` accumulated through nested + // inserts: a child at rank r lands at `base + r * scale`, all within + // (-1,1) of the top-level insert. Seeded `(0.0, 1.0)` by `expand_insert`. + d_range: (f32, f32), ) { if depth > MAX_NESTING_DEPTH { eprintln!("block_cache: nested-block depth > {MAX_NESTING_DEPTH}, truncating"); @@ -977,7 +1027,7 @@ fn expand_defn( continue; } } - emit_wire(lw, accum_xform, ctx, out); + emit_wire(lw, accum_xform, ctx, out, d_range); } LocalSub::Nested(nref) => { if visited.iter().any(|n| n == &nref.block_name) { @@ -1055,6 +1105,12 @@ fn expand_defn( bg_color: ctx.bg_color, }; visited.push(nref.block_name.clone()); + // Children of this nested insert stack inside the slot its own + // rank owns — same composition the exploded-fill path applies. + let nested_range = ( + d_range.0 + nref.local_rank * d_range.1, + d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0), + ); if let Some(cp) = &nref.clip_poly { // XCLIP'd nested insert: expand into an isolated batch set, // finalize to whole wires, clip them to the boundary mapped @@ -1065,7 +1121,15 @@ fn expand_defn( // instance would differ). let composed = nref.xform.then(accum_xform); let mut sub = Batches::default(); - expand_defn(nested_defn, &composed, &inner_ctx, &mut sub, visited, depth + 1); + expand_defn( + nested_defn, + &composed, + &inner_ctx, + &mut sub, + visited, + depth + 1, + nested_range, + ); let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); let world_poly: Vec<[f64; 2]> = cp .iter() @@ -1093,6 +1157,7 @@ fn expand_defn( out, visited, depth + 1, + nested_range, ); } } @@ -1129,6 +1194,7 @@ fn emit_wire( accum_xform: &Transform, ctx: &ExpandCtx, out: &mut Batches, + d_range: (f32, f32), ) { if lw.points.is_empty() && lw.fill_tris.is_empty() @@ -1173,6 +1239,13 @@ fn emit_wire( 0.0 }; + // Only band wires take a per-child composed depth: their solid area is + // what covers siblings, and their width already splits them into their own + // batches — thin wires keep the shared whole-insert depth so same-style + // batches stay merged. + let local_depth = (lw.world_width > 0.0) + .then(|| d_range.0 + lw.local_rank * d_range.1); + let key = style_key( final_color, final_pat_len, @@ -1182,6 +1255,7 @@ fn emit_wire( lw.aci, lw.plinegen, lw.is_fill_only, + local_depth, ); // If the open batch for this style would exceed wgpu's per-buffer limit @@ -1205,6 +1279,7 @@ fn emit_wire( lw.is_fill_only, ) }); + entry.local_depth = local_depth; // NaN separator between previously-appended geometry and this wire so the // GPU shader treats them as disconnected polylines within one buffer. diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 9204562de..877b05dbc 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -656,6 +656,7 @@ pub(crate) fn tessellate_entity( return vec![WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris, pick_tris_low, @@ -995,6 +996,7 @@ pub(crate) fn tessellate_entity( let marker = WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1376,6 +1378,7 @@ fn lod_stub_wire( WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1464,6 +1467,7 @@ fn lod_stub_wire_3d( WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index ef3a56e1d..94679bcca 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -372,6 +372,7 @@ pub fn tessellate( elem_wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -664,6 +665,7 @@ pub fn tessellate( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -703,6 +705,7 @@ pub fn tessellate( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -749,6 +752,7 @@ pub fn tessellate( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -776,6 +780,7 @@ pub fn tessellate( wires.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -833,6 +838,7 @@ pub fn tessellate( out.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -872,6 +878,7 @@ pub fn tessellate( out.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -908,6 +915,7 @@ pub fn tessellate( out.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -937,6 +945,7 @@ pub fn tessellate( out.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -988,6 +997,7 @@ pub fn tessellate( return vec![WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1038,6 +1048,7 @@ pub fn tessellate( return vec![WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1081,6 +1092,7 @@ pub fn tessellate( return vec![WireModel { taper_widths: Vec::new(), world_width: polyline_band_width(entity), + depth_override: None, fill_is_3d: false, pick_tris, pick_tris_low, @@ -1173,6 +1185,7 @@ pub fn tessellate( out.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris, pick_tris_low, @@ -1233,6 +1246,7 @@ pub fn tessellate( fill_tris, fill_tris_low, fill_is_3d, + depth_override: None, }); } @@ -1240,6 +1254,7 @@ pub fn tessellate( out.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1282,6 +1297,7 @@ pub fn tessellate( return vec![WireModel { taper_widths: Vec::new(), world_width: polyline_band_width(entity), + depth_override: None, fill_is_3d: false, pick_tris, pick_tris_low, @@ -1325,6 +1341,7 @@ pub fn tessellate( return vec![WireModel { taper_widths: widths, world_width, + depth_override: None, fill_is_3d: false, pick_tris, pick_tris_low, @@ -1424,6 +1441,7 @@ pub fn tessellate( vec![WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris, pick_tris_low, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 22fd5f544..c9d9e0f94 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -754,7 +754,7 @@ impl Scene { if self.selected.contains(&handle) { m.color = [0.15, 0.55, 1.00, m.color[3]]; } - let d = depth_map.get(&handle.value()).copied().unwrap_or(0.0); + let d = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]); m.draw_depth = d; if let Some(b) = &mut backdrop { b.draw_depth = d; @@ -814,6 +814,21 @@ impl Scene { .unwrap_or(false) }; let mut models: Vec = Vec::new(); + // Draw-order composition (#408): every fill inherits its INSERT's + // draw-depth slot, offset by the child's rank INSIDE its block + // (draw_depth_map ranks per owning block), scaled to stay within the + // insert's half-gap in the host ordering. Without this every block / + // xref fill landed at depth 0 and the stack's LIFO pop reversed the + // block's internal stacking. + let depth_map = self.draw_depth_map(); + let block_count = |name: &str| -> usize { + self.document + .block_records + .get(name) + .map(|br| br.entity_handles.len()) + .unwrap_or(1) + .max(1) + }; // Exploding an INSERT materializes (clones) every child of its block — // including 3D solids that each carry megabytes of SAB geometry — just // to scan the result for hatch fills. On xref-heavy drawings whose @@ -874,17 +889,22 @@ impl Scene { &self.document, &ins.common.layer, ); + let [base_depth, half_gap] = depth_map + .get(&ins.common.handle.value()) + .copied() + .unwrap_or([0.0, 0.0]); let mut stack: Vec<( EntityType, usize, [f32; 4], crate::scene::view::render::InheritStyle, + (f32, f32), )> = ins .explode_from_document(&self.document) .into_iter() - .map(|e| (normalize(e), 0usize, ins_color, l0)) + .map(|e| (normalize(e), 0usize, ins_color, l0, (base_depth, half_gap))) .collect(); - while let Some((sub, depth, sub_ins_color, sub_l0)) = stack.pop() { + while let Some((sub, depth, sub_ins_color, sub_l0, (d_base, d_half))) = stack.pop() { match sub { EntityType::Insert(nins) => { if depth >= 32 { @@ -916,8 +936,22 @@ impl Scene { &nins.common.layer, ) }; + // Nested insert: its slot = parent slot + its rank in + // the parent block, subdivided by its own block size. + let nd_base = d_base + + depth_map + .get(&nins.common.handle.value()) + .map_or(0.0, |d| d[0]) + * d_half; + let nd_half = d_half / (block_count(&nins.block_name) as f32 + 1.0); for e in nins.explode_from_document(&self.document) { - stack.push((normalize(e), depth + 1, child_ins_color, child_l0)); + stack.push(( + normalize(e), + depth + 1, + child_ins_color, + child_l0, + (nd_base, nd_half), + )); } } EntityType::Hatch(dxf) => { @@ -942,6 +976,12 @@ impl Scene { if let Some(mut model) = Self::hatch_model_from_dxf(&dxf, color) { + // In-block rank → within the insert's depth slot. + model.draw_depth = d_base + + depth_map + .get(&dxf.common.handle.value()) + .map_or(0.0, |d| d[0]) + * d_half; if let Some(poly) = &clip_poly { let clipped = pick::xclip::clip_hatch_boundary( &model.boundary, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index caf931654..f3dcb0730 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -873,8 +873,13 @@ pub struct Scene { /// effective sort key (SortEntitiesTable override or own handle), then /// fed to the 2D pipelines as a small clip-z bias so entities of /// *different* types order correctly against each other. 3D meshes are - /// excluded (they keep real geometric depth). - draw_depth_cache: RefCell>)>>, + /// excluded (they keep real geometric depth). Each value is + /// `[depth, half]`: `depth` = the entity's signed rank in (-1,1), `half` = + /// half the gap to the neighbouring ranks (`1/(block_count+1)`) — the + /// sub-range a block INSERT's children may occupy without crossing the + /// insert's siblings. Fill explosion and band wires compose child depths + /// as `depth + child_rank * half`. + draw_depth_cache: RefCell>)>>, /// Cached hatch fill models, keyed by geometry_epoch. View culling /// is handled at draw time via `hatch_skip_flags` in the pipeline, /// not at build time — that lets the GPU buffer stay stable across @@ -1295,7 +1300,8 @@ impl Scene { // its internal geometry / text / attributes must NOT be scaled // individually (that would double-scale — AutoCAD even forbids // annotative attributes inside annotative blocks for this reason). - let built = cache::block_cache::BlockCache::build(&self.document, 1.0, bg); + let built = + cache::block_cache::BlockCache::build(&self.document, 1.0, bg, &self.draw_depth_map()); let arc = Arc::new(built); *self.block_defn_cache.borrow_mut() = Some((self.block_epoch, Arc::clone(&arc))); arc @@ -3314,7 +3320,7 @@ impl Scene { /// its owning block by effective sort key (SortEntitiesTable override or /// own handle). The result feeds the 2D pipelines as a clip-z bias so /// entities of different types order correctly against each other. - pub(super) fn draw_depth_map(&self) -> Arc> { + pub(super) fn draw_depth_map(&self) -> Arc> { { // Draw order depends on each block's entity COUNT (the rank // denominator) and the entities' handles / SortEntitiesTable — none of @@ -3384,17 +3390,20 @@ impl Scene { .unwrap_or(hv); by_block.entry(block).or_default().push((hv, eff)); } - let mut depth_map: HashMap = HashMap::default(); + let mut depth_map: HashMap = HashMap::default(); for (_block, mut v) in by_block { v.sort_by_key(|(_, eff)| *eff); let denom = (v.len() as f32) + 1.0; + // Adjacent ranks are 2/denom apart; a child sub-range of + // ±half = ±1/denom around the parent depth never crosses them. + let half = 1.0 / denom; for (rank, (hv, _)) in v.into_iter().enumerate() { // Signed (-1,1): back ranks → negative, front → positive, // mid → ~0. The shader applies `z -= draw_depth * BIAS`, so a // default/unranked 0.0 means "no bias" (neutral) — which keeps // 3D mesh faces and transient wires at their real depth. let norm = (rank as f32 + 1.0) / denom; // (0,1) - depth_map.insert(hv, (norm - 0.5) * 2.0); + depth_map.insert(hv, [(norm - 0.5) * 2.0, half]); } } let arc = Arc::new(depth_map); @@ -3527,7 +3536,7 @@ impl Scene { } } let mut m = model.clone(); - m.draw_depth = depth_map.get(&handle.value()).copied().unwrap_or(0.0); + m.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]); Some(m) }) .collect() @@ -3551,7 +3560,7 @@ impl Scene { return None; } let mut m = model.clone(); - m.draw_depth = depth_map.get(&handle.value()).copied().unwrap_or(0.0); + m.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]); Some(m) }) .collect(), diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index eb1044a9f..80ef3c33f 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -130,6 +130,14 @@ pub struct WireModel { /// exactly like `points` — no separate collector pass. The renderer /// gathers these across all wires into the text vertex buffer. pub text_verts: Vec, + /// Block-local composed draw-order offset in (-1, 1) for a wire that must + /// order against its *siblings inside the block* — currently wide-polyline + /// bands, whose solid area would otherwise cover later-drawn siblings. + /// `None` (all other wires) = the whole-insert depth resolved from `name`. + /// `Some(local)` composes at draw time: `insert_depth + local * insert_half`, + /// mirroring how exploded block fills seed their depth, so bands and fills + /// from the same block interleave by the block's internal draw order. + pub depth_override: Option, /// `true` when [`fill_tris`] is a real 3-D surface (PolyfaceMesh / /// PolygonMesh face) that must render with hidden-surface depth and only in /// shaded modes. `false` for a flat 2-D overlay fill (SOLID arrowhead, @@ -186,6 +194,7 @@ impl WireModel { Self { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -419,6 +428,7 @@ impl Default for WireModel { dash_align_end: None, fill_tris: Vec::new(), fill_tris_low: Vec::new(), + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index 4b23d7a6b..f4d55d668 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -114,15 +114,10 @@ impl Face3DGpu { face3d_wires: &[WireModel], all_wires: &[WireModel], keep_3d_mesh_fills: bool, - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, ) -> Self { - let depth_of = |w: &WireModel| -> f32 { - w.name - .parse::() - .ok() - .and_then(|h| depth_map.get(&h).copied()) - .unwrap_or(0.0) - }; + let depth_of = + |w: &WireModel| -> f32 { super::wire_gpu::wire_draw_depth(w, depth_map) }; let mut verts_3d: Vec = Vec::with_capacity(face3d_wires.len() * 6); let mut verts_2d: Vec = Vec::new(); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 9fa62889c..d8649c2f6 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -1477,7 +1477,7 @@ impl Pipeline { &self, device: &wgpu::Device, wires: &[WireModel], - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, ) -> ( std::sync::Arc>, std::sync::Arc>>, @@ -1543,7 +1543,7 @@ impl Pipeline { wires: &[WireModel], selected: &rustc_hash::FxHashSet, hover: Option, - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, ) { let hover = hover.filter(|h| !selected.contains(h)); if selected.is_empty() && hover.is_none() { @@ -1627,7 +1627,7 @@ impl Pipeline { &mut self, device: &wgpu::Device, wires: &[WireModel], - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, ) { self.gpu_preview_wires = if wires.is_empty() { vec![] @@ -1912,7 +1912,7 @@ impl Pipeline { face3d_wires: &[WireModel], all_wires: &[WireModel], wireframe_only: bool, - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, ) { // Edge buffer is always built from `face3d_wires`, so 3DFACE // outlines stay on the screen regardless of mode. diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index 7813489ce..ab50896fb 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -105,7 +105,7 @@ pub fn is_mesh_edge(w: &WireModel, mesh_names: &rustc_hash::FxHashSet) -> b /// Surface) are excluded from `draw_depth_map`, so their fallback edge wires /// get draw_depth 0.0. Two coincident opaque such wires share a z-bias and /// resolve by submission order, which a tail relocation would flip. -fn append_unsafe(wires: &[&WireModel], depth_map: &FxHashMap) -> bool { +fn append_unsafe(wires: &[&WireModel], depth_map: &FxHashMap) -> bool { wires.iter().any(|w| { w.color[3] < 0.999 || handle_of(w).map_or(true, |h| !depth_map.contains_key(&h.value())) @@ -202,7 +202,7 @@ impl WireArena { device: &wgpu::Device, queue: &wgpu::Queue, wires: &[&WireModel], - depth_map: &FxHashMap, + depth_map: &FxHashMap, const_bgl: &wgpu::BindGroupLayout, mesh_edge: bool, ) -> Option { @@ -288,7 +288,7 @@ impl WireArena { queue: &wgpu::Queue, changes: &[(Handle, ChangeKind)], wires: &[&WireModel], - depth_map: &FxHashMap, + depth_map: &FxHashMap, ) -> bool { let Some(ranges) = handle_ranges(wires) else { return false; @@ -405,13 +405,27 @@ impl WireArena { // entity's z-bias. Refresh each live slab's draw_depth from the new // depth map and re-upload the whole (small) const buffer; the instance // buffer is untouched. + // A slab whose consts carry DIFFERENT depths holds block-band wires + // with composed per-child offsets (`depth_override`); a flat refresh + // to the entity's base depth would erase them. Fall back to a full + // rebuild, which recomputes each wire's composed depth. + if !self.mesh_edge { + for slab in self.slabs.values() { + let first = self.consts_cpu[slab.const_off as usize].draw_depth; + for k in 1..slab.const_len { + if self.consts_cpu[(slab.const_off + k) as usize].draw_depth != first { + return false; + } + } + } + } for (h, slab) in &self.slabs { // Mesh-edge wires keep depth 0 (no draw-order bias); regular wires // take the re-normalised map value. let dd = if self.mesh_edge { 0.0 } else { - depth_map.get(&h.value()).copied().unwrap_or(0.0) + depth_map.get(&h.value()).map_or(0.0, |d| d[0]) }; for k in 0..slab.const_len { self.consts_cpu[(slab.const_off + k) as usize].draw_depth = dd; diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 8a4563012..13fe3bc5d 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -488,17 +488,23 @@ pub(crate) fn emit_wire_native( /// Looks up a wire's draw-order depth from the per-entity map using the /// handle encoded in its `name`. Falls back to 0.0 (transient / preview -/// wires that carry no document handle). +/// 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. pub(crate) fn wire_draw_depth( wire: &WireModel, - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, ) -> f32 { - wire + let base = wire .name .parse::() .ok() - .and_then(|h| depth_map.get(&h).copied()) - .unwrap_or(0.0) + .and_then(|h| depth_map.get(&h).copied()); + match (base, wire.depth_override) { + (Some([d, half]), Some(local)) => d + local * half, + (Some([d, _]), None) => d, + (None, _) => 0.0, + } } /// Build the shared per-wire `WireConst` storage buffer and its bind group @@ -546,7 +552,7 @@ impl WireGpu { pub fn from_run( device: &wgpu::Device, wires: &[WireModel], - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, mesh_edge: bool, const_bgl: Option<&wgpu::BindGroupLayout>, ) -> Vec { @@ -632,7 +638,7 @@ impl WireGpu { pub fn from_batch( device: &wgpu::Device, wires: &[WireModel], - depth_map: &rustc_hash::FxHashMap, + depth_map: &rustc_hash::FxHashMap, const_bgl: Option<&wgpu::BindGroupLayout>, ) -> Vec { let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum(); diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 95c1d954c..ec8721af4 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -307,6 +307,7 @@ pub fn apply_along( .map(|pts| WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -348,6 +349,7 @@ pub fn apply_along( out.push(WireModel { taper_widths: Vec::new(), world_width: 0.0, + depth_override: None, fill_is_3d: false, pick_tris: Vec::new(), pick_tris_low: Vec::new(), diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 7d140ec09..0a24606ef 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -60,7 +60,7 @@ pub struct ViewportData { /// by the wire / face3d pipelines as a clip-z bias. WireModels carry no /// depth field (84 construction sites); the bias is looked up by handle /// at GPU-upload time from this map instead. - pub(in crate::scene) draw_depths: Arc>, + pub(in crate::scene) draw_depths: Arc>, pub(in crate::scene) hatches: Arc>, /// Wipeout fills — rendered in a separate pass AFTER wires. pub(in crate::scene) wipeout_hatches: Arc>,