From 8a65f2e8beadee10321291ef8f072f4ded30fb9a Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:16:52 -0300 Subject: [PATCH 1/6] fix: preserve block-local draw order for ordinary line members alongside wipeouts --- src/scene/cache/block_cache.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 7e53ae706..167dfe8ca 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -2618,8 +2618,13 @@ fn emit_wire( // MTEXT background (a dimension's DIMTFILL box) left at the bare insert // level sat in front of its own text whenever that text's rank was // below zero. - let local_depth = (lw.world_width > 0.0 || lw.is_fill_only || !lw.text_verts.is_empty()) - .then(|| d_range.0 + lw.local_rank * d_range.1); + // Every block member must retain its block-local draw rank, including + // ordinary thin LINE/LWPOLYLINE wires. Previously these wires had no + // depth_override, so they were drawn at the INSERT's bare depth while + // wipeout/hatch fills were positioned at the member rank. COPY and grip + // moves rebuild instance batches and exposed this mismatch in model, + // paper-space and PDF output. + let local_depth = Some(d_range.0 + lw.local_rank * d_range.1); let plot_visible = ctx.plot_visible && lw.plot_visible && (!lw.plot_l0 || ctx.l0_plottable); // Batched wires share one dash distance, which runs on across the NaN From 9e8d07270e6f00ffe64a032f3ef33f086008ac8f Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:30:19 -0300 Subject: [PATCH 2/6] fix: avoid translated prototype reuse for wipeout-containing block inserts --- src/scene/cache/block_cache.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 167dfe8ca..c82fe304c 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -1038,7 +1038,20 @@ pub fn expand_insert( scale_policy, ); let name = ins_handle.value().to_string(); - let prototype_key = if !ins.is_array() && cache.prototype_blocks.contains(&ins.block_name) { + // Mask-bearing blocks must not reuse translated GPU prototypes: ordinary + // strokes and wipeout fills have different upload paths. Their shared + // source_id plus per-copy translated geometry can make a moved INSERT + // display stale wipeout geometry or disappear as the view changes. + // Expand these from their definition until both GPU paths share a + // verified identical instancing convention. + let has_masks = defn.subs.iter().any(|sub| match sub { + LocalSub::Wire(wire) => wire.canvas_color, + LocalSub::Nested(_) => false, + }); + let prototype_key = if !ins.is_array() + && !has_masks + && cache.prototype_blocks.contains(&ins.block_name) + { Some(expansion_prototype_key( ins, &xform, From 72e97c6f60d979a2492380d76bf56ffff49ece49 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:39:43 -0300 Subject: [PATCH 3/6] fix: render each transformed wipeout/hatch mask with own boundary instead of sharing source-id GPU batches --- src/scene/pipeline/wipeout_gpu.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/scene/pipeline/wipeout_gpu.rs b/src/scene/pipeline/wipeout_gpu.rs index 621304ad5..337287629 100644 --- a/src/scene/pipeline/wipeout_gpu.rs +++ b/src/scene/pipeline/wipeout_gpu.rs @@ -172,23 +172,16 @@ impl WipeoutGpu { models: &[HatchModel], bgl1: &wgpu::BindGroupLayout, ) -> Vec { - let mut slots = rustc_hash::FxHashMap::default(); - let mut groups: Vec> = Vec::new(); - for (index, model) in models.iter().enumerate() { - let key = model - .render_instance - .map(|instance| (true, instance.source_id)) - .unwrap_or((false, index as u64)); - let slot = *slots.entry(key).or_insert_with(|| { - let slot = groups.len(); - groups.push(Vec::new()); - slot - }); - groups[slot].push(model); - } - groups - .into_iter() - .map(|group| Self::new(device, queue, &group, bgl1)) + // A source_id only identifies reusable render geometry, not an + // interchangeable hatch boundary in world space. Grouping masks by + // source_id can incorrectly reuse the first model's boundary, origin, + // plane and uniform for later INSERT copies. Unlike wires, a mask's + // GPU shader clips against its own boundary, so build each mask from + // its actual transformed model until we have a geometry-local, + // translation-safe instancing path. + models + .iter() + .map(|model| Self::new(device, queue, &[*model], bgl1)) .collect() } From 93a54d66e3795b7ebd95ad60bc7fe393adf5c44b Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:42:54 -0300 Subject: [PATCH 4/6] fix: pass HatchModel references to per-mask GPU builder --- src/scene/pipeline/wipeout_gpu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scene/pipeline/wipeout_gpu.rs b/src/scene/pipeline/wipeout_gpu.rs index 337287629..ae1b058cf 100644 --- a/src/scene/pipeline/wipeout_gpu.rs +++ b/src/scene/pipeline/wipeout_gpu.rs @@ -181,7 +181,7 @@ impl WipeoutGpu { // translation-safe instancing path. models .iter() - .map(|model| Self::new(device, queue, &[*model], bgl1)) + .map(|model| Self::new(device, queue, &[model], bgl1)) .collect() } From 1074c81f524f65cc0c4108bc0c6e28e80ca8cf32 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:07:48 -0300 Subject: [PATCH 5/6] fix: increase bounded screen draw-order depth resolution for overlapping block wipeouts --- src/shaders/draw_order.wgsl | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/shaders/draw_order.wgsl b/src/shaders/draw_order.wgsl index d0d243d85..d7f90e122 100644 --- a/src/shaders/draw_order.wgsl +++ b/src/shaders/draw_order.wgsl @@ -1,13 +1,12 @@ -// Positive order draws in front; zero retains geometric depth. Keep the usual -// 0.001 NDC offset away from the clipping planes, but reduce it near either -// plane: perspective depth can be much closer to 1 than this fixed offset. -// -// Using order / (1 + abs(order)) for the limited region keeps ordering monotonic -// (including the paper background's -2). Less than half the remaining depth is -// consumed, so valid vertices stay valid. Out-of-range/behind-eye vertices get -// no offset and remain subject to the real near/far clipping planes. +// Positive draw order moves geometry towards the camera. In dense blocks, +// child ranks occupy a narrow fraction of the INSERT depth band: a 0.001 +// NDC bias can quantize two overlapping lines/masks to the same depth value, +// especially after changing view/zoom. Use a wider, yet bounded range for +// CAD 2D draw order. Limit displacement to less than half the available +// clip-space room, so we never push valid geometry across near/far planes. +// This is a screen-only GPU change; PDF order uses its own stable sorting. fn apply_draw_order(clip: vec4, order: f32) -> vec4 { let room = 0.5 * max(0.0, min(clip.z, clip.w - clip.z)); - let scale = min(0.001 * max(clip.w, 0.0), room / (1.0 + abs(order))); + let scale = min(0.02 * max(clip.w, 0.0), room / (1.0 + abs(order))); return vec4(clip.xy, clip.z - order * scale, clip.w); } From fe0edb619f880fad9ddc1286c121fe77be2c442c Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:14:13 -0300 Subject: [PATCH 6/6] fix: invalidate wipeout render cache on INSERT copy and grip moves --- src/scene/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 978a780b6..497bba455 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -7917,15 +7917,19 @@ impl Scene { let cache = self.wipeout_cache.borrow(); match cache.get(&key) { Some((cached_epoch, arc)) - // wipeout_models scans the whole document for Wipeout - // entities; relevance = the changed handle is a Wipeout. + // The displayed wipeout set also contains masks + // expanded from INSERTs. COPY and grip edits modify or + // add INSERT handles without changing the child WIPEOUT + // entity, so the old predicate reused stale positions + // until REGEN. Track both categories, including + // removed INSERTs via the recorded removal bits. if self.category_cache_valid( *cached_epoch, - CACHE_CATEGORY_WIPEOUT, + CACHE_CATEGORY_WIPEOUT | CACHE_CATEGORY_INSERT_HATCH, |h| { matches!( self.document.get_entity(h), - Some(EntityType::Wipeout(_)) + Some(EntityType::Wipeout(_) | EntityType::Insert(_)) ) }, ) =>