From 910caa6f67c1ed9862ccccf4d4f8808bf3c61723 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Jul 2026 15:56:56 +0300 Subject: [PATCH] fix(polyline): render width bands for polylines inside blocks (#222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wide LwPolyline / Polyline2D fills only drew for entities directly in the layout — the model-space loop skips block-internal ones, and the block-explode fill pass handled hatches but not wide polylines. So a polyline with a width nested in a block drew at zero width (its centreline came from the wire path, the band fill never materialized). Extend the block-explode fill walk (exploded_insert_hatch_models) to emit wide-polyline bands at world position, gated by a new block_has_wide_poly scan mirroring block_has_hatch, with a shared push_block_wide_fills helper that resolves block-child colour inheritance and applies XCLIP. Co-Authored-By: Claude Opus 4.8 --- src/scene/entity.rs | 105 +++++++++++++++++++++++++++++++++++++++++++- src/scene/mod.rs | 44 +++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/src/scene/entity.rs b/src/scene/entity.rs index a65fb7062..50846b27c 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -618,6 +618,11 @@ impl Scene { // contain a hatch still explode exactly as before, so output is unchanged. let mut hatch_block_memo: std::collections::HashMap = std::collections::HashMap::new(); + // Same gate for wide LwPolyline / Polyline2D fills — a block with a wide + // polyline (but no hatch) must still explode so its width band renders + // at world position, like a block-internal hatch. (#222) + let mut wide_block_memo: std::collections::HashMap = + std::collections::HashMap::new(); for entity in self.document.entities() { let EntityType::Insert(ins) = entity else { continue; @@ -632,7 +637,9 @@ impl Scene { ) { continue; } - if !self.block_has_hatch(&ins.block_name, &mut hatch_block_memo) { + if !self.block_has_hatch(&ins.block_name, &mut hatch_block_memo) + && !self.block_has_wide_poly(&ins.block_name, &mut wide_block_memo) + { continue; } let selected = tint_selected && self.selected.contains(&ins.common.handle); @@ -741,6 +748,45 @@ impl Scene { models.push(model); } } + // Wide LwPolyline / Polyline2D inside a block: materialize its + // width bands at world position, same as a block-internal + // hatch. Without this a wide polyline in a block drew at zero + // width (its centreline came from the wire path, the band + // fill only from the model-space loop that skips blocks). (#222) + EntityType::LwPolyline(pl) => { + if pl.common.invisible || layer_hidden(&pl.common.layer) { + continue; + } + let (origin, fills) = + crate::entities::lwpolyline::wide_fills(&pl); + self.push_block_wide_fills( + &mut models, + &EntityType::LwPolyline(pl), + origin, + fills, + sub_ins_color, + sub_l0, + &clip_poly, + selected, + ); + } + EntityType::Polyline2D(pl) => { + if pl.common.invisible || layer_hidden(&pl.common.layer) { + continue; + } + let (origin, fills) = + crate::entities::polyline::wide_fills(&pl); + self.push_block_wide_fills( + &mut models, + &EntityType::Polyline2D(pl), + origin, + fills, + sub_ins_color, + sub_l0, + &clip_poly, + selected, + ); + } _ => {} } } @@ -748,6 +794,63 @@ impl Scene { models } + /// Materialize a block-exploded wide polyline's width bands (from + /// `wide_fills`, already in world coords) as solid `HatchModel`s: resolve the + /// block-child colour inheritance (NO background adaptation — a width band is + /// the polyline's own colour, not a hatch) and apply any XCLIP. Shared by the + /// LwPolyline / Polyline2D arms of the block-explode fill walk. (#222) + #[allow(clippy::too_many_arguments)] + fn push_block_wide_fills( + &self, + models: &mut Vec, + sub_entity: &EntityType, + origin: [f64; 2], + fills: Vec>, + sub_ins_color: [f32; 4], + sub_l0: crate::scene::view::render::InheritStyle, + clip_poly: &Option>, + selected: bool, + ) { + if fills.is_empty() { + return; + } + let mut color = crate::scene::view::render::render_style_for_block_sub( + &self.document, + sub_entity, + sub_ins_color, + 0.0, + [0.0; 8], + 0.0, + sub_l0, + ) + .0; + if selected { + color = [0.15, 0.55, 1.00, color[3]]; + } + for boundary in fills { + let ring = if let Some(poly) = clip_poly { + let clipped = pick::xclip::clip_hatch_boundary(&boundary, origin, poly); + if clipped.is_empty() { + continue; + } + clipped + } else { + boundary + }; + models.push(HatchModel { + boundary: Arc::new(ring), + pattern: model::hatch_model::HatchPattern::Solid, + name: "SOLID".into(), + color, + angle_offset: 0.0, + scale: 1.0, + world_origin: origin, + vp_scissor: None, + draw_depth: 0.0, + }); + } + } + /// Wipeout fill models — rendered in a separate pass AFTER wires so that /// wipeouts correctly mask everything below them in the draw order. pub(crate) fn wipeout_models(&self) -> Vec { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 96de43de0..1739b2ce3 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2707,6 +2707,50 @@ impl Scene { result } + /// Transitively true when a block (or a block it nests) contains a wide + /// LwPolyline / Polyline2D — one carrying a non-zero width. Gate for the + /// block-explode wide-fill pass, mirroring [`Self::block_has_hatch`] so a + /// solid-only block is never exploded just to look for width bands. (#222) + fn block_has_wide_poly( + &self, + block_name: &str, + memo: &mut std::collections::HashMap, + ) -> bool { + if let Some(&v) = memo.get(block_name) { + return v; + } + // Seed `false` first so a cyclic block reference terminates. + memo.insert(block_name.to_string(), false); + let result = self + .document + .block_records + .get(block_name) + .map(|br| { + br.entity_handles.iter().any(|&h| match self.document.get_entity(h) { + Some(EntityType::LwPolyline(p)) => { + p.constant_width > 1e-9 + || p.vertices + .iter() + .any(|v| v.start_width > 1e-9 || v.end_width > 1e-9) + } + Some(EntityType::Polyline2D(p)) => { + p.start_width > 1e-9 + || p.end_width > 1e-9 + || p.vertices + .iter() + .any(|v| v.start_width > 1e-9 || v.end_width > 1e-9) + } + Some(EntityType::Insert(ins)) => { + self.block_has_wide_poly(&ins.block_name, memo) + } + _ => false, + }) + }) + .unwrap_or(false); + memo.insert(block_name.to_string(), result); + result + } + pub fn insert_hatches_for_click(&self) -> Arc> { { let c = self.insert_hatch_cache.borrow();