diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 58cfc940c..391be617e 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1235,6 +1235,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let far_pos = base + dir * far; let far_neg = base - dir * far; let guide = crate::scene::WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1274,6 +1275,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let far_pos = h.base + h.dir * far; let far_neg = h.base - h.dir * far; previews.push(crate::scene::WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/entities/common.rs b/src/entities/common.rs index 19bf81b13..f3d634169 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -392,6 +392,47 @@ pub(crate) fn thick_band_tube( (fill_tris, lines) } +/// Build a continuous WCS point list + a per-point FULL band width for a +/// tapered wide polyline, so the wire shader can interpolate each segment's two +/// endpoint widths. Each `verts` entry is `(location_xy, bulge_to_next, +/// start_width, end_width)` — the effective full widths at that vertex's segment +/// start and end (already resolved against the polyline's constant width). Arcs +/// are sampled in 16 steps with the width interpolated linearly along the arc. +/// A shared vertex is emitted once (carrying the previous segment's end width), +/// which is exact for the usual continuous taper. +pub(crate) fn tapered_band_points( + verts: &[([f64; 2], f64, f64, f64)], + is_closed: bool, + to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64), +) -> (Vec<[f64; 3]>, Vec) { + let n = verts.len(); + let seg_count = if is_closed { n } else { n.saturating_sub(1) }; + let mut pts: Vec<[f64; 3]> = Vec::new(); + let mut widths: Vec = Vec::new(); + let mut push = |x: f64, y: f64, w: f32| { + let (wx, wy, wz) = to_wcs(x, y); + pts.push([wx, wy, wz]); + widths.push(w); + }; + for i in 0..seg_count { + let (p0, bulge, sw0, ew0) = verts[i]; + let (p1, _, _, _) = verts[(i + 1) % n]; + if i == 0 { + push(p0[0], p0[1], sw0 as f32); + } + if bulge.abs() < 1e-9 { + push(p1[0], p1[1], ew0 as f32); + } else if let Some(arc) = BulgeArc::from_bulge(p0, p1, bulge) { + for j in 1..=16usize { + let t = j as f64 / 16.0; + let s = arc.sample(t); + push(s[0], s[1], (sw0 + (ew0 - sw0) * t) as f32); + } + } + } + (pts, widths) +} + /// 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/dimension.rs b/src/entities/dimension.rs index 21c49483a..5facf81c8 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1352,6 +1352,7 @@ fn tessellate_dimension_inner( let (ext1, ext2) = split_ext_lines(&geom.ext_lines); if !ext1.is_empty() { wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1379,6 +1380,7 @@ fn tessellate_dimension_inner( } if !ext2.is_empty() { wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1406,6 +1408,7 @@ fn tessellate_dimension_inner( } } else { wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1434,6 +1437,7 @@ fn tessellate_dimension_inner( } wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1478,6 +1482,7 @@ fn tessellate_dimension_inner( aci_to_rgba(&c) }; wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/entities/leader.rs b/src/entities/leader.rs index ab8981800..a210e9b6e 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -590,6 +590,7 @@ impl LeaderTess for Leader { if verts.len() < 2 { return WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -697,6 +698,7 @@ impl LeaderTess for Leader { } WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 43b3f5242..06802fc0f 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -213,6 +213,51 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { return thick_segments(&seg_data, &path, pline.thickness, normal, kv, tgs); } + // A wide polyline whose per-vertex widths VARY renders a smooth taper — + // handled here, before the PLINEGEN split, so both cases get it. A + // uniform-width polyline falls through to the constant-band paths below. + if let Some(band_verts) = tapered_band_verts(pline) { + let mut kv: Vec<[f64; 3]> = Vec::new(); + let mut tgs: Vec = Vec::new(); + for i in 0..seg_count { + let v0 = &verts[i]; + let v1 = &verts[(i + 1) % count]; + let p0 = to_pt(v0); + let p1 = to_pt(v1); + if i == 0 { + kv.push([p0.x, p0.y, p0.z]); + } + kv.push([p1.x, p1.y, p1.z]); + if v0.bulge.abs() < 1e-9 { + tgs.push(TangentGeom::Line { + p1: [p0.x as f32, p0.y as f32, p0.z as f32], + p2: [p1.x as f32, p1.y as f32, p1.z as f32], + }); + } else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge( + [v0.location.x, v0.location.y], + [v1.location.x, v1.location.y], + v0.bulge, + ) { + let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); + tgs.push(TangentGeom::Circle { + center: [wcx as f32, wcy as f32, wcz as f32], + radius: arc.radius as f32, + }); + } + } + let (pts, widths) = + crate::entities::common::tapered_band_points(&band_verts, pline.is_closed, &to_wcs); + let (fill_origin, fills) = wide_fills(pline); + return TruckEntity { + pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills), + object: TruckObject::TaperedLines(pts, widths), + snap_pts: vec![], + tangent_geoms: tgs, + key_vertices: kv, + fill_tris: vec![], + }; + } + // plinegen=false: NaN-separated segments so the linetype pattern restarts per vertex. if !pline.plinegen { let mut pts: Vec<[f64; 3]> = Vec::new(); @@ -321,6 +366,32 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { } } +/// The per-vertex `(location, bulge, start_width, end_width)` band description +/// for a wide LwPolyline whose width VARIES — `None` when the width is uniform +/// (a constant band) so the caller keeps the cheaper constant-width path. +fn tapered_band_verts(pline: &LwPolyline) -> Option> { + let c = pline.constant_width; + let band: Vec<([f64; 2], f64, f64, f64)> = pline + .vertices + .iter() + .map(|v| { + let sw = if v.start_width > 1e-9 { v.start_width } else { c }; + let ew = if v.end_width > 1e-9 { v.end_width } else { c }; + ([v.location.x, v.location.y], v.bulge, sw, ew) + }) + .collect(); + let w0 = band.first().map_or(0.0, |v| v.2); + let varies = band + .iter() + .any(|&(_, _, sw, ew)| (sw - w0).abs() > 1e-9 || (ew - w0).abs() > 1e-9); + // Only a genuine, non-zero-width taper takes the per-point path. + if varies && w0.max(band.iter().map(|v| v.3).fold(0.0, f64::max)) > 1e-9 { + Some(band) + } else { + None + } +} + fn grips(pline: &LwPolyline) -> Vec { let elev = pline.elevation; let n = pline.vertices.len(); diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 7759e9c34..bec019471 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1404,6 +1404,7 @@ impl MultiLeaderTess for MultiLeader { // WireModels so the renderer respects per-piece coloring. let mut wires: Vec = Vec::new(); wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1694,6 +1695,7 @@ impl MultiLeaderTess for MultiLeader { xy = xy.max(y); } wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1762,6 +1764,7 @@ impl MultiLeaderTess for MultiLeader { wcs_corners[3], ]; wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1798,6 +1801,7 @@ impl MultiLeaderTess for MultiLeader { wcs_corners[0], ]; wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 717ca80a1..5515be7bd 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -326,9 +326,22 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { } let (fill_origin, fills) = wide_fills(pl); + // A wide Polyline2D whose per-vertex widths VARY renders a smooth taper; a + // uniform-width one keeps the constant-band Contour. + let object = match tapered_band_verts_2d(pl) { + Some(band_verts) => { + let (pts, widths) = crate::entities::common::tapered_band_points( + &band_verts, + pl.is_closed(), + &to_wcs, + ); + TruckObject::TaperedLines(pts, widths) + } + None => TruckObject::Contour(edges.into_iter().collect::()), + }; TruckEntity { pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills), - object: TruckObject::Contour(edges.into_iter().collect::()), + object, snap_pts: vec![], tangent_geoms: tangents, key_vertices: key_verts, @@ -336,6 +349,32 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { } } +/// Per-vertex `(location, bulge, start_width, end_width)` band description for a +/// wide Polyline2D whose width VARIES — `None` when the width is uniform. +fn tapered_band_verts_2d( + pl: &acadrust::entities::Polyline2D, +) -> Option> { + let d = pl.start_width.max(pl.end_width); + let band: Vec<([f64; 2], f64, f64, f64)> = pl + .vertices + .iter() + .map(|v| { + let sw = if v.start_width > 1e-9 { v.start_width } else { d }; + let ew = if v.end_width > 1e-9 { v.end_width } else { d }; + ([v.location.x, v.location.y], v.bulge, sw, ew) + }) + .collect(); + let w0 = band.first().map_or(0.0, |v| v.2); + let varies = band + .iter() + .any(|&(_, _, sw, ew)| (sw - w0).abs() > 1e-9 || (ew - w0).abs() > 1e-9); + if varies && w0.max(band.iter().map(|v| v.3).fold(0.0, f64::max)) > 1e-9 { + Some(band) + } else { + None + } +} + impl TruckConvertible for Polyline2D { fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { Some(tessellate_polyline2d(self)) diff --git a/src/entities/table.rs b/src/entities/table.rs index fef5197ff..d754f94f3 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -753,6 +753,7 @@ pub fn tessellate_table( let mk = |color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index f21f0a9c5..1ab06c4ea 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -186,6 +186,7 @@ impl CadCommand for AlignedDimensionCommand { let p1 = p1.as_vec3(); let p2 = p2.as_vec3(); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -238,6 +239,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { let d1 = d1.as_vec3(); let d2 = d2.as_vec3(); WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index ab8a07732..0efb9e25d 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -130,6 +130,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 19cbcd985..902f838d9 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -180,6 +180,7 @@ fn v3(p: DVec3) -> Vector3 { fn preview_line(a: Vec3, b: Vec3) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index a27fa3280..c36bcd766 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -179,6 +179,7 @@ impl CadCommand for DimBaselineCommand { let dim_line_pt2 = dim_line_pt2.as_vec3(); let pt = pt.as_vec3(); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 1a5547f0a..7de6d2c9e 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -159,6 +159,7 @@ impl CadCommand for DimContinueCommand { let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp)); let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp)); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index 0189c8d7e..b9169d837 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -85,6 +85,7 @@ impl CadCommand for DimJogLineCommand { } let d = 0.3_f32; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index a20e59322..b5169d7ca 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -100,6 +100,7 @@ impl CadCommand for DimTeditCommand { } let d = 0.2_f32; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index 8bc8d29e9..e92a2e1f6 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -163,6 +163,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { points.push([w2.x, w2.y, w2.z]); } WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 29e39faf6..5d9806306 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -218,6 +218,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index d4bc07f4f..38ea27270 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -150,6 +150,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { points.push([w2.x, w2.y, w2.z]); } WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index 0e8368996..fa24b8ef3 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -458,6 +458,7 @@ impl CadCommand for MLeaderCollectCommand { fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 8c374cf44..bef5171f3 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -121,6 +121,7 @@ fn ordinate_elbow(feature: DVec3, leader: DVec3, is_x: bool) -> DVec3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index 95685f7d4..94faefd61 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -90,6 +90,7 @@ impl CadCommand for QdimCommand { } let d = 0.5_f32; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 368ad1650..4018edae7 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -185,6 +185,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index b1629c548..0dd6ea203 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -139,6 +139,7 @@ impl CadCommand for TableCommand { let y = pt.y; let z = pt.z; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index fa03e52ea..918512f0c 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -84,6 +84,7 @@ impl CadCommand for ToleranceCommand { } let d = 0.15_f32; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index 701ba552d..c7739b73b 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -160,6 +160,7 @@ impl CadCommand for AttdefCommand { // Show a small cross at the insertion point. let d = 0.15_f32; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index d35a8013d..ca035238f 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -136,6 +136,7 @@ impl CadCommand for MlineCommand { .collect(); pts.push([pt.x, pt.y, pt.z]); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index 84535321b..f1c8fb3e2 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -108,6 +108,7 @@ impl CadCommand for ImageCommand { let p3 = [origin.x, origin.y + world_height, origin.z]; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index d394827a3..efb4187f9 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -84,6 +84,7 @@ impl CadCommand for RayCommand { let dir = (pt - base).normalize_or_zero(); let far = base + dir * DISPLAY_EXTENT; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -180,6 +181,7 @@ impl CadCommand for XLineCommand { let far_pos = base + dir * DISPLAY_EXTENT; let far_neg = base - dir * DISPLAY_EXTENT; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/draw/revcloud.rs b/src/modules/draw/draw/revcloud.rs index 1d1573303..5c0209faf 100644 --- a/src/modules/draw/draw/revcloud.rs +++ b/src/modules/draw/draw/revcloud.rs @@ -89,6 +89,7 @@ impl CadCommand for RevCloudCommand { self.points[0].z as f32, ]); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 1b748d0f5..1377782be 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -114,6 +114,7 @@ impl CadCommand for WipeoutCommand { let min = p1.min(pt); let max = p1.max(pt); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -161,6 +162,7 @@ impl CadCommand for WipeoutCommand { self.poly_pts[0].z as f32, ]); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 885b302cb..605e48cc8 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -69,6 +69,7 @@ impl CadCommand for AreaCommand { pts.push(f(pt)); pts.push(f(self.points[0])); Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index 83463324c..ca00daab1 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -63,6 +63,7 @@ impl CadCommand for DistCommand { let p1 = self.first?; // The preview wire is purely visual, so f32 vertices are fine here. Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index 64e889dab..569d40d59 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -67,6 +67,7 @@ impl MeasureGeomCommand { /// Build a cyan preview wire connecting the picked points and the cursor. fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 1d0562ae4..9c4dc5dfa 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3059,6 +3059,7 @@ fn collect_runs(pts: &[[f64; 2]], take: &dyn Fn([f64; 2]) -> bool, out: &mut Vec /// Build a preview wire from a NaN-break point list. fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index 21ac1b263..fa94c9eee 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -81,6 +81,7 @@ impl CadCommand for MviewCommand { let pt = pt.as_vec3(); let c1 = self.corner1?; Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 78566374f..17ce79684 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -305,6 +305,7 @@ inventory::submit!(crate::command::CommandRegistration { fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index 8e1c6a4e7..a9355d2aa 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -48,6 +48,7 @@ impl CadCommand for PlotWindowCommand { let p1 = self.p1?.as_vec3(); // Draw the selection rectangle. Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index 79271d5e3..e689b8a3b 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -65,6 +65,7 @@ impl CadCommand for ZoomWindowCommand { let max = p1.max(pt); // Draw a rectangle preview Some(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 1f393c0f2..fc4da7723 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -864,6 +864,7 @@ impl Batches { // the cached defn. let color = crate::scene::view::render::adapt_to_bg(b.color, bg_color); WireModel { + taper_widths: Vec::new(), world_width: b.world_width, fill_is_3d: false, pick_tris: b.pick_tris, diff --git a/src/scene/convert/acad_to_truck.rs b/src/scene/convert/acad_to_truck.rs index 543012694..cfdf9c977 100644 --- a/src/scene/convert/acad_to_truck.rs +++ b/src/scene/convert/acad_to_truck.rs @@ -56,6 +56,11 @@ pub enum TruckObject { Lines(Vec<[f64; 3]>), /// Like Lines but linetype pattern restarts at each NaN-separated segment (plinegen=false). SegmentedLines(Vec<[f64; 3]>), + /// A wide polyline whose band width VARIES (a taper): a continuous WCS point + /// list paired index-for-index with a per-point full band width. The wire + /// shader interpolates the two endpoint widths of each segment so the band + /// tapers smoothly. Points hold no NaN breaks (one continuous band). + TaperedLines(Vec<[f64; 3]>, Vec), Volume(Solid), } diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index db375308a..12d33385e 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -650,6 +650,7 @@ pub(crate) fn tessellate_entity( (ins.insert_point.z) as f32, ); let marker = WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1030,6 +1031,7 @@ fn lod_stub_wire( // LOD boundary. #19. let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1117,6 +1119,7 @@ fn lod_stub_wire_3d( } let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index c883e50a6..4a1c45fc3 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -370,6 +370,7 @@ pub fn tessellate( None }; elem_wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -661,6 +662,7 @@ pub fn tessellate( ftl.push(lo); } wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -699,6 +701,7 @@ pub fn tessellate( fpl.push(lo); } wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -744,6 +747,7 @@ pub fn tessellate( low.push(ll); } wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -770,6 +774,7 @@ pub fn tessellate( }); } wires.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -826,6 +831,7 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -864,6 +870,7 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -899,6 +906,7 @@ pub fn tessellate( // are empty → the early-return path above). if !sdf_verts.is_empty() { out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -927,6 +935,7 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -977,6 +986,7 @@ pub fn tessellate( .map(|[kx, ky, kz]| [kx, ky, kz]) .collect(); return vec![WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1026,6 +1036,7 @@ pub fn tessellate( .map(|[x, y, z]| [x, y, z]) .collect(); return vec![WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1068,6 +1079,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 { + taper_widths: Vec::new(), world_width: polyline_band_width(entity), fill_is_3d: false, pick_tris, @@ -1159,6 +1171,7 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris, @@ -1196,6 +1209,7 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, pick_tris: Vec::new(), pick_tris_low: Vec::new(), @@ -1224,6 +1238,7 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -1265,6 +1280,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 { + taper_widths: Vec::new(), world_width: polyline_band_width(entity), fill_is_3d: false, pick_tris, @@ -1291,6 +1307,49 @@ pub fn tessellate( }]; } + TruckObject::TaperedLines(points, widths) => { + // A wide polyline whose width varies: one continuous band wire + // carrying a per-point width; the shader interpolates each + // segment's two endpoint widths. `world_width` (the widest edge) + // stays as the constant fallback for PDF export + a hairline + // floor when zoomed out. + let (local_pts, local_pts_low) = points_to_ds(points); + let snap_pts = te.snap_pts; + let key_vertices: Vec<[f64; 3]> = te + .key_vertices + .into_iter() + .map(|[x, y, z]| [x, y, z]) + .collect(); + let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); + let world_width = widths.iter().copied().fold(0.0f32, f32::max); + return vec![WireModel { + taper_widths: widths, + world_width, + fill_is_3d: false, + pick_tris, + pick_tris_low, + dash_from_start: false, + dash_align_end: None, + text_verts: Vec::new(), + name, + points: local_pts, + points_low: local_pts_low, + color, + selected, + pattern_length, + pattern, + line_weight_px, + snap_pts, + tangent_geoms: te.tangent_geoms, + aci: 0, + key_vertices, + plinegen: true, + aabb: WireModel::UNBOUNDED_AABB, + fill_tris: vec![], + fill_tris_low: Vec::new(), + }]; + } + TruckObject::Volume(_) => { // Solid3D / Region / Body → mesh tessellation lives in // `solid3d_tess`. As a wire fallback, render the pre-computed @@ -1334,6 +1393,7 @@ pub fn tessellate( let snap_pts: Vec<(glam::DVec3, SnapHint)> = snap_pts.into_iter().map(|(p, h)| (p.as_dvec3(), h)).collect(); vec![WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 2057a32e0..eb1044a9f 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -60,6 +60,11 @@ pub struct WireModel { /// units (scaling with zoom) so the band IS the wire — the linetype dash /// pattern then applies to the band instead of a separate hatch fill. pub world_width: f32, + /// Per-point full band width (drawing units), aligned index-for-index with + /// [`points`], for a polyline whose width VARIES (a taper). Empty = a + /// constant band of `world_width`. The wire shader reads the two endpoint + /// widths of each segment and interpolates, so the band tapers smoothly. + pub taper_widths: Vec, /// ACI color index (1-255). 0 means true-color or unknown (no CTB lookup). pub aci: u8, /// Pre-baked snap candidates (Center, Node, Quadrant, Insertion). @@ -179,6 +184,7 @@ impl WireModel { /// Create a solid wire (no dash pattern, 1px weight). pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self { Self { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -402,6 +408,7 @@ impl Default for WireModel { pattern: [0.0; 8], line_weight_px: 1.0, world_width: 0.0, + taper_widths: Vec::new(), aci: 0, snap_pts: Vec::new(), tangent_geoms: Vec::new(), diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index 43beea70d..7813489ce 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -189,6 +189,8 @@ fn blank_instance() -> WireInstance { distance_a: 0.0, distance_b: 0.0, wire_id: 0, + world_hw_a: 0.0, + world_hw_b: 0.0, } } diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index cffdbcb23..651fdd463 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -80,6 +80,11 @@ pub struct WireInstance { pub distance_b: f32, /// Index into the per-wire `WireConst` storage buffer (group 1). pub wire_id: u32, + /// World-space half-width at each endpoint for a TAPERED band. `0.0` = + /// use the per-wire constant (`WireConst.world_half_width`). The shader + /// interpolates between the two so the band tapers across the segment. + pub world_hw_a: f32, + pub world_hw_b: f32, } #[cfg(not(target_arch = "wasm32"))] @@ -94,6 +99,8 @@ impl WireInstance { wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, wire_id) as u64, shader_location: 6, format: wgpu::VertexFormat::Uint32 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_a) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_b) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32 }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, @@ -178,6 +185,10 @@ pub struct WireInstance { /// World-space half-width for a wide-polyline band (see `WireConst`). `0.0` /// = a normal wire (uses `half_width`, screen pixels). pub world_half_width: f32, + /// Per-endpoint world half-width for a tapered band (0 = use the constant + /// `world_half_width`). The shader interpolates across the segment. + pub world_hw_a: f32, + pub world_hw_b: f32, } #[cfg(target_arch = "wasm32")] @@ -202,6 +213,8 @@ impl WireInstance { wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, align_end) as u64, shader_location: 12, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, align_total) as u64, shader_location: 13, format: wgpu::VertexFormat::Float32 }, wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_half_width) as u64, shader_location: 14, format: wgpu::VertexFormat::Float32 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_a) as u64, shader_location: 15, format: wgpu::VertexFormat::Float32 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_b) as u64, shader_location: 16, format: wgpu::VertexFormat::Float32 }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, @@ -389,6 +402,7 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve } let (dists, align_end, align_total) = wire_distances(wire); let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) }; + let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 }; let mut instances: Vec = Vec::with_capacity(seg_count); for i in 0..seg_count { let a = wire.points[i]; @@ -412,6 +426,8 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve align_end, align_total, world_half_width: wire.world_width * 0.5, + world_hw_a: tw(i), + world_hw_b: tw(i + 1), }); } instances @@ -446,6 +462,9 @@ pub(crate) fn emit_wire_native( return (Vec::new(), cst); } let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) }; + // Per-point half-width for a tapered band (empty ⇒ 0 ⇒ shader uses the + // per-wire constant width). + let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 }; let mut instances: Vec = Vec::with_capacity(seg_count); for i in 0..seg_count { let a = wire.points[i]; @@ -461,6 +480,8 @@ pub(crate) fn emit_wire_native( distance_a: dists[i], distance_b: dists[i + 1], wire_id, + world_hw_a: tw(i), + world_hw_b: tw(i + 1), }); } (instances, cst) diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 814963fb4..95c1d954c 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -305,6 +305,7 @@ pub fn apply_along( .into_iter() .filter(|s| s.len() >= 2) .map(|pts| WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), @@ -345,6 +346,7 @@ pub fn apply_along( xy = xy.max(y); } out.push(WireModel { + taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, pick_tris: Vec::new(), diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index 0de11db05..aae6855c2 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -61,6 +61,9 @@ struct InstanceIn { @location(13) align_total: f32, // World-space half-width for a wide-polyline band. 0 = normal wire. @location(14) world_half_width: f32, + // Per-endpoint world half-width for a tapered band (0 = use the constant). + @location(15) world_hw_a: f32, + @location(16) world_hw_b: f32, } // Draw-order depth bias: shifts clip-space z so 2D entities of different @@ -135,10 +138,14 @@ struct VertexOut { // `world_half_width / world_per_pixel` (pixels) so the band tracks zoom. A // normal wire (world_half_width == 0) uses the screen-pixel half-width, // honouring the LWDISPLAY toggle (off → collapse to a 1-pixel line). + // A tapered band interpolates a per-endpoint world half-width across the + // segment; a constant band uses `world_half_width`. Both clamp to a + // half-pixel so a zoomed-out band stays a hairline instead of vanishing. + let taper_hw = mix(in.world_hw_a, in.world_hw_b, which_end); var hw: f32; - if in.world_half_width > 0.0 { - // Clamp to a half-pixel so a zoomed-out band never drops below a - // hairline (its centre-line) instead of vanishing. + if taper_hw > 0.0 { + hw = max(taper_hw / u.world_per_pixel, 0.5); + } else if in.world_half_width > 0.0 { hw = max(in.world_half_width / u.world_per_pixel, 0.5); } else { hw = select(0.5, in.half_width, u.lwdisplay_enable > 0.5); diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl index 4f44344c3..87d16e461 100644 --- a/src/shaders/wire_indexed.wgsl +++ b/src/shaders/wire_indexed.wgsl @@ -44,6 +44,10 @@ struct InstanceIn { @location(4) distance_a: f32, @location(5) distance_b: f32, @location(6) wire_id: u32, + // Per-endpoint world half-width for a tapered band (0 = use the per-wire + // constant `world_half_width`). + @location(7) world_hw_a: f32, + @location(8) world_hw_b: f32, } const DRAW_ORDER_BIAS: f32 = 0.001; @@ -97,10 +101,15 @@ struct VertexOut { // by `world_half_width / world_per_pixel` (screen pixels) so the band grows // and shrinks with zoom. A normal wire (world_half_width == 0) uses the // screen-pixel half-width, honouring the LWDISPLAY toggle. + // A tapered band carries a per-endpoint world half-width on the instance: + // interpolate across the segment so the band narrows/widens smoothly. A + // constant band uses the per-wire `world_half_width`. Both clamp to a + // half-pixel so a zoomed-out band stays a hairline instead of vanishing. + let taper_hw = mix(in.world_hw_a, in.world_hw_b, which_end); var hw: f32; - if c.world_half_width > 0.0 { - // Clamp to a half-pixel so a zoomed-out band never drops below a - // hairline (its centre-line) instead of vanishing. + if taper_hw > 0.0 { + hw = max(taper_hw / u.world_per_pixel, 0.5); + } else if c.world_half_width > 0.0 { hw = max(c.world_half_width / u.world_per_pixel, 0.5); } else { hw = select(0.5, c.half_width, u.lwdisplay_enable > 0.5);