diff --git a/src/entities/common.rs b/src/entities/common.rs index 381c360c1..19bf81b13 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -334,6 +334,64 @@ pub(crate) fn wide_band_tris(origin: [f64; 2], fills: &[Vec<[f32; 2]>]) -> Vec<[ out } +/// Extrude a wide-polyline band (from `wide_fills`) into a solid tube for a DXF +/// thickness (code 39): a vertical wall between every band-boundary point and +/// its `thickness`-along-`normal` copy, plus triangulated bottom and top caps. +/// Returns `(fill_tris, edge_lines)` as flat WCS f64 lists — the caller wraps +/// them in a `TruckEntity` (object = `Lines(edge_lines)`, `fill_tris`, and +/// `pick_tris = fill_tris`). Shared by LwPolyline and Polyline2D so both wide +/// polyline kinds extrude the same solid instead of just their centre-line. +/// +/// `polyline_segment_fill` emits each band loop as the outer boundary forward +/// then the inner boundary back, so its two transition edges (`half-1 → half` +/// and `n-1 → 0`) are radial cap ends inside the band — no wall is drawn there. +pub(crate) fn thick_band_tube( + origin: [f64; 2], + fills: &[Vec<[f32; 2]>], + thickness: f64, + normal: (f64, f64, f64), + to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64), +) -> (Vec<[f64; 3]>, Vec<[f64; 3]>) { + let (nx, ny, nz) = normal; + let t = thickness; + let off = |p: [f64; 3]| -> [f64; 3] { [p[0] + t * nx, p[1] + t * ny, p[2] + t * nz] }; + let push_seg = |lines: &mut Vec<[f64; 3]>, a: [f64; 3], b: [f64; 3]| { + lines.push(a); + lines.push(b); + lines.push([f64::NAN; 3]); + }; + let mut lines: Vec<[f64; 3]> = Vec::new(); + let mut fill_tris: Vec<[f64; 3]> = Vec::new(); + for poly in fills { + let n = poly.len(); + if n < 4 { + continue; + } + let half = n / 2; + let bot: Vec<[f64; 3]> = poly + .iter() + .map(|&[x, y]| { + let (wx, wy, wz) = to_wcs(origin[0] + x as f64, origin[1] + y as f64); + [wx, wy, wz] + }) + .collect(); + let top: Vec<[f64; 3]> = bot.iter().map(|&p| off(p)).collect(); + for k in 0..n { + push_seg(&mut lines, bot[k], top[k]); + if k == half - 1 || k == n - 1 { + continue; + } + let kn = (k + 1) % n; + push_seg(&mut lines, bot[k], bot[kn]); + push_seg(&mut lines, top[k], top[kn]); + fill_tris.extend_from_slice(&[bot[k], bot[kn], top[kn], bot[k], top[kn], top[k]]); + } + fill_tris.extend(crate::entities::mesh::triangulate_planar(&bot)); + fill_tris.extend(crate::entities::mesh::triangulate_planar(&top)); + } + (fill_tris, lines) +} + /// Compute the filled boundary polygon for one polyline segment. /// For straight segments: a rectangle/trapezoid. /// For arc segments: an arc band (outer arc + reversed inner arc). diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index ae5f295c0..43b3f5242 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -115,57 +115,8 @@ fn thick_wide_band( tangents: Vec, ) -> TruckEntity { let (origin, fills) = wide_fills(pl); - let (nx, ny, nz) = normal; - let t = thickness; - let off = |p: [f64; 3]| -> [f64; 3] { [p[0] + t * nx, p[1] + t * ny, p[2] + t * nz] }; - let push_seg = |lines: &mut Vec<[f64; 3]>, a: [f64; 3], b: [f64; 3]| { - lines.push(a); - lines.push(b); - lines.push([f64::NAN; 3]); - }; - - let mut lines: Vec<[f64; 3]> = Vec::new(); - // Shaded surfaces: the vertical walls plus the top cap. The flat bottom cap - // is already drawn by the wide-fill hatch path, so it is not repeated here. - let mut fill_tris: Vec<[f64; 3]> = Vec::new(); - for poly in &fills { - let n = poly.len(); - if n < 4 { - continue; - } - // `polyline_segment_fill` returns the band loop as the outer boundary - // forward then the inner boundary back, so the two transition edges - // (half-1 → half and n-1 → 0) are the radial segment ends. - let half = n / 2; - let bot: Vec<[f64; 3]> = poly - .iter() - .map(|&[x, y]| { - let (wx, wy, wz) = to_wcs(origin[0] + x as f64, origin[1] + y as f64); - [wx, wy, wz] - }) - .collect(); - let top: Vec<[f64; 3]> = bot.iter().map(|&p| off(p)).collect(); - for k in 0..n { - // Vertical at every boundary point. - push_seg(&mut lines, bot[k], top[k]); - // Skip the two radial cap edges — they stay inside the band. - if k == half - 1 || k == n - 1 { - continue; - } - let kn = (k + 1) % n; - push_seg(&mut lines, bot[k], bot[kn]); // bottom boundary - push_seg(&mut lines, top[k], top[kn]); // top boundary - // Wall quad (two triangles). - fill_tris.extend_from_slice(&[ - bot[k], bot[kn], top[kn], bot[k], top[kn], top[k], - ]); - } - // Bottom + top caps — the band ring triangulated at each plane. The - // bottom cap is a real 3-D surface (with depth) so the walls occlude it - // correctly; the flat 2-D band fill is skipped for thickened polylines. - fill_tris.extend(crate::entities::mesh::triangulate_planar(&bot)); - fill_tris.extend(crate::entities::mesh::triangulate_planar(&top)); - } + let (fill_tris, lines) = + crate::entities::common::thick_band_tube(origin, &fills, thickness, normal, to_wcs); TruckEntity { pick_tris: fill_tris.clone(), diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 7be9af884..717ca80a1 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -238,6 +238,28 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { let (wbx, wby, wbz) = to_wcs(ox1, oy1); kv.push([wbx, wby, wbz]); } + // A wide Polyline2D extrudes its whole band into a solid tube (walls + + // caps), same as a thickened LwPolyline; a zero-width one falls through + // to the centre-line extrusion below. + let is_wide = pl.start_width > 1e-9 + || pl.end_width > 1e-9 + || pl + .vertices + .iter() + .any(|v| v.start_width > 1e-9 || v.end_width > 1e-9); + if is_wide { + let (origin, fills) = wide_fills(pl); + let (fill_tris, lines) = + crate::entities::common::thick_band_tube(origin, &fills, t, normal, &to_wcs); + return TruckEntity { + pick_tris: fill_tris.clone(), + object: TruckObject::Lines(lines), + snap_pts: vec![], + tangent_geoms: tgs, + key_vertices: kv, + fill_tris, + }; + } let mut pts: Vec<[f64; 3]> = Vec::with_capacity(path.len() * 2 + kv.len() * 3 + 4); pts.extend_from_slice(&path); pts.push([f64::NAN; 3]); diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 2a644e5dc..1f393c0f2 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -61,6 +61,10 @@ pub struct LocalWire { pub pattern_length: f32, pub pattern: [f32; 8], pub line_weight_px: f32, + /// World-space band width for a wide polyline (see `WireModel.world_width`). + /// Block-local; the expand-time transform scales it by the insert so the + /// shader band grows with a scaled insert. `0.0` = a normal wire. + pub world_width: f32, pub plinegen: bool, /// Set at construction; used to discriminate fill-only GPU batches from /// stroke batches in [`StyleKey`]. Derived from @@ -325,12 +329,10 @@ fn build_defn( subs.push(LocalSub::Nested(build_nested_ref(nested_ins, doc, bg_color))); } _ => { - // A wide polyline inside a block keeps its thin centre-line - // here (`LocalWire` carries no `world_width`, and `finalize` - // rebuilds every block wire with `world_width: 0.0`); its band - // is drawn by the hatch pipeline (`push_block_wide_fills`), - // which transforms with the block instance so the band scales - // correctly. The shader band is a top-level-entity path only. + // 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) { subs.push(LocalSub::Wire(lw)); } @@ -493,6 +495,7 @@ fn tessellate_sub_local( pattern_length: pat_len, pattern: pat, line_weight_px: lw_px, + world_width: wire.world_width, plinegen: wire.plinegen, is_fill_only, color_is_byblock: color_is_byblock && wire_on_base_color, @@ -744,6 +747,10 @@ struct StyleKey { pattern_length: u32, pattern: [u32; 8], line_weight_px: u32, + /// Wide-polyline band width (bit-cast). Keeps bands of different widths — and + /// bands vs thin wires of the same colour/style — in separate batches so the + /// finalized WireModel carries one correct `world_width`. + world_width: u32, aci: u8, plinegen: bool, /// Marks batches that emit only `fill_tris` with no wire `points`. The @@ -759,6 +766,7 @@ struct BatchEntry { pattern_length: f32, pattern: [f32; 8], line_weight_px: f32, + world_width: f32, aci: u8, plinegen: bool, points: Vec<[f32; 3]>, @@ -809,6 +817,7 @@ impl BatchEntry { pat_len: f32, pat: [f32; 8], lw_px: f32, + world_width: f32, aci: u8, plinegen: bool, _is_fill_only: bool, @@ -823,6 +832,7 @@ impl BatchEntry { pattern_length: pat_len, pattern: pat, line_weight_px: lw_px, + world_width, aci, plinegen, min_x: f32::INFINITY, @@ -854,7 +864,7 @@ impl Batches { // the cached defn. let color = crate::scene::view::render::adapt_to_bg(b.color, bg_color); WireModel { - world_width: 0.0, + world_width: b.world_width, fill_is_3d: false, pick_tris: b.pick_tris, pick_tris_low: b.pick_tris_low, @@ -896,6 +906,7 @@ fn style_key( pat_len: f32, pat: [f32; 8], lw_px: f32, + world_width: f32, aci: u8, plinegen: bool, is_fill_only: bool, @@ -919,6 +930,7 @@ fn style_key( pat[7].to_bits(), ], line_weight_px: lw_px.to_bits(), + world_width: world_width.to_bits(), aci, plinegen, is_fill_only, @@ -1145,11 +1157,27 @@ fn emit_wire( let final_pat_len = final_pat_len * ctx.pslt_factor; let final_pat = final_pat.map(|v| v * ctx.pslt_factor); + // A wide polyline's band width is baked in block-local units; scale it by + // the insert transform so the shader band matches the scaled geometry. + // Average the X and Y axis image lengths — exact for a uniform insert, a + // sensible mean for a non-uniform one (the band carries one width). + let final_world_width = if lw.world_width > 0.0 { + let o = accum_xform.apply(Vector3::new(0.0, 0.0, 0.0)); + let ax = accum_xform.apply(Vector3::new(1.0, 0.0, 0.0)); + let ay = accum_xform.apply(Vector3::new(0.0, 1.0, 0.0)); + let sx = ((ax.x - o.x).powi(2) + (ax.y - o.y).powi(2) + (ax.z - o.z).powi(2)).sqrt(); + let sy = ((ay.x - o.x).powi(2) + (ay.y - o.y).powi(2) + (ay.z - o.z).powi(2)).sqrt(); + lw.world_width * ((sx + sy) * 0.5) as f32 + } else { + 0.0 + }; + let key = style_key( final_color, final_pat_len, final_pat, final_lw_px, + final_world_width, lw.aci, lw.plinegen, lw.is_fill_only, @@ -1170,6 +1198,7 @@ fn emit_wire( final_pat_len, final_pat, final_lw_px, + final_world_width, lw.aci, lw.plinegen, lw.is_fill_only, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index b8a888c40..c883e50a6 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1068,7 +1068,7 @@ pub fn tessellate( // linetype dashes it); the band also backs pick. let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); return vec![WireModel { - world_width: lwpolyline_band_width(entity), + world_width: polyline_band_width(entity), fill_is_3d: false, pick_tris, pick_tris_low, @@ -1130,13 +1130,18 @@ pub fn tessellate( let mut out = Vec::new(); let mut is_first = true; - // A thickened wide polyline's extrusion — its corner / cap edges + // A thickened polyline's extrusion — its corner / cap edges // frame the solid tube and read black (like solid-with-edges - // outlines), while the tube fill keeps the entity colour. - let edge_color = if matches!( + // outlines), while the tube fill keeps the entity colour. Both + // wide polyline kinds (LwPolyline + Polyline2D) extrude tubes. + let is_thick_extrusion = matches!( entity, EntityType::LwPolyline(p) if p.thickness.abs() > 1e-10 - ) { + ) || matches!( + entity, + EntityType::Polyline2D(p) if p.thickness.abs() > 1e-10 + ); + let edge_color = if is_thick_extrusion { [0.0, 0.0, 0.0, 1.0] } else { color @@ -1260,7 +1265,7 @@ pub fn tessellate( // treatment as the Contour arm, restarting the dash per segment. let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); return vec![WireModel { - world_width: lwpolyline_band_width(entity), + world_width: polyline_band_width(entity), fill_is_3d: false, pick_tris, pick_tris_low, @@ -1509,26 +1514,35 @@ pub(crate) fn entity_z(entity: &EntityType) -> f32 { } } -/// Band width (drawing units) of a wide LwPolyline whose flat band is drawn by -/// expanding its centre-line wire in the shader. `0.0` = a zero-width polyline -/// or a non-LwPolyline (a normal screen-pixel wire). A thickness-extruded -/// polyline is excluded: its band becomes a 3-D tube (`thick_wide_band`), not a -/// flat shader band. A tapering polyline (per-vertex start/end widths) collapses -/// to its widest edge — the shader carries one width per wire. -fn lwpolyline_band_width(entity: &EntityType) -> f32 { - match entity { +/// Band width (drawing units) of a wide polyline whose flat band is drawn by +/// expanding its centre-line wire in the shader. `0.0` = a zero-width polyline, +/// a non-polyline (a normal screen-pixel wire), or a thickness-extruded one — an +/// LwPolyline thickens into a 3-D tube (`thick_wide_band`) and a Polyline2D +/// keeps its hatch band (only its centre-line extrudes; no tube yet), so neither +/// takes the flat shader band. A tapering polyline (per-vertex start/end widths) +/// collapses to its widest edge — the shader carries one width per wire. +fn polyline_band_width(entity: &EntityType) -> f32 { + let w = match entity { EntityType::LwPolyline(p) if p.thickness.abs() <= 1e-10 => { let mut w = p.constant_width; for v in &p.vertices { w = w.max(v.start_width).max(v.end_width); } - if w > 1e-9 { - w as f32 - } else { - 0.0 + w + } + EntityType::Polyline2D(p) if p.thickness.abs() <= 1e-10 => { + let mut w = p.start_width.max(p.end_width); + for v in &p.vertices { + w = w.max(v.start_width).max(v.end_width); } + w } _ => 0.0, + }; + if w > 1e-9 { + w as f32 + } else { + 0.0 } } diff --git a/src/scene/entity.rs b/src/scene/entity.rs index c006565fe..6741cbb84 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -688,53 +688,12 @@ impl Scene { // true applies the screen selection highlight.) models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true)); - // Wide LWPolyline and Polyline2D fills - for entity in self.document.entities() { - let (common, fill_origin, fills) = match entity { - // A wide LwPolyline no longer draws its band here: the shader - // expands the centre-line wire to `world_width` (a real - // depth-tested band that also carries the linetype dash), so a - // second flat 2-D band with only draw-order depth would double - // it and show through 3-D geometry. A thickened one is likewise - // a 3-D tube built by the tessellator. Its band still backs pick - // (`pick_tris`), just not this fill. - EntityType::LwPolyline(_) => continue, - EntityType::Polyline2D(pl) => { - let (o, f) = crate::entities::polyline::wide_fills(pl); - (&pl.common, o, f) - } - _ => continue, - }; - if fills.is_empty() { - continue; - } - if common.invisible || layer_hidden(&common.layer) { - continue; - } - if !self.belongs_to_visible_block(common.handle, common.owner_handle, layout_block) { - continue; - } - let base_color = self.render_style(entity).0; - let selected = self.selected.contains(&common.handle); - let color = if selected { - [0.15, 0.55, 1.00, 1.0] - } else { - base_color - }; - for boundary in fills { - models.push(HatchModel { - boundary: Arc::new(boundary), - boundary_wcs: None, - pattern: model::hatch_model::HatchPattern::Solid, - name: "SOLID".into(), - color, - angle_offset: 0.0, - scale: 1.0, - world_origin: fill_origin, - draw_depth: depth_map.get(&common.handle.value()).copied().unwrap_or(0.0), - }); - } - } + // Wide LwPolyline / Polyline2D bands are no longer hatch fills at + // model level: a flat band is drawn by expanding its centre-line wire + // to `world_width` in the shader (depth-tested, linetype-dashed), and a + // thickened one is extruded into a 3-D tube by the tessellator. Only + // block-internal wide polylines still band via the hatch path (see + // `push_block_wide_fills`), where it scales with the insert transform. models } @@ -907,45 +866,11 @@ 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, - ); - } + // Wide LwPolyline / Polyline2D bands inside a block are drawn + // by the shader wire path — block_cache carries each centre- + // line's `world_width` and scales it by the insert transform + // (`emit_wire`) — not as hatch fills here. Same depth-tested, + // linetype-dashed band as at model level. (#222) _ => {} } } @@ -953,63 +878,6 @@ 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), - boundary_wcs: None, - pattern: model::hatch_model::HatchPattern::Solid, - name: "SOLID".into(), - color, - angle_offset: 0.0, - scale: 1.0, - world_origin: origin, - 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 {