forked from 1b-agent/opencadstudio-upstream
feat(polyline): smooth per-segment taper for varying-width wide polylines
A wide polyline with per-vertex varying widths rendered at its widest edge (uniform-width ones were already exact). Draw the true taper instead. `WireModel.taper_widths` carries a per-point full band width. The wire instance (native slim + web fat) gains per-endpoint `world_hw_a/b`, and both wire shaders interpolate `mix(hw_a, hw_b, which_end)` so each segment's band narrows/widens linearly — depth-tested, and the linetype dash still applies. Alignment (widths must match the tessellated points) is solved by building both together: a new `TruckObject::TaperedLines(points, widths)`, filled by `common::tapered_band_points` (straight = endpoints, arc = 16 samples with the width lerped along it). to_truck routes a varying-width polyline here — LwPolyline before the PLINEGEN split, Polyline2D at its return — while a uniform-width one keeps the constant-band Contour path unchanged. `world_width` stays the widest edge as the PDF-stroke + zoom-floor fallback. Blocks fall back to the constant-widest band (`LocalWire` carries no taper_widths). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
a8aece1909
commit
b3016ca892
48 files changed
+322
-7
No files matched your search
@@ -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<f32>) {
|
||||
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<f32> = 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).
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<TangentGeom> = 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<Vec<([f64; 2], f64, f64, f64)>> {
|
||||
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<GripDef> {
|
||||
let elev = pline.elevation;
|
||||
let n = pline.vertices.len();
|
||||
|
||||
@@ -1404,6 +1404,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
// WireModels so the renderer respects per-piece coloring.
|
||||
let mut wires: Vec<WireModel> = 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(),
|
||||
|
||||
@@ -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::<Wire>()),
|
||||
};
|
||||
TruckEntity {
|
||||
pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills),
|
||||
object: TruckObject::Contour(edges.into_iter().collect::<Wire>()),
|
||||
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<Vec<([f64; 2], f64, f64, f64)>> {
|
||||
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<TruckEntity> {
|
||||
Some(tessellate_polyline2d(self))
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user