feat: render LWPolyline and Polyline2D wide strokes as solid fills
Wide polyline segments (constant_width, per-vertex start/end widths) now appear as solid-filled quads or arc bands via HatchModel, generated inline in synced_hatch_models(). Straight segments → trapezoid, arc segments → arc-band polygon (inner/outer arc). MINSERT confirmed working via acadrust explode_from_document → array_transforms(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c7a7022293
commit
67022589df
1 changed files with 164 additions and 0 deletions
164
src/scene/mod.rs
164
src/scene/mod.rs
|
|
@ -1441,6 +1441,37 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wide LWPolyline and Polyline2D fills
|
||||||
|
for entity in self.document.entities() {
|
||||||
|
let (common, fills) = match entity {
|
||||||
|
EntityType::LwPolyline(pl) => {
|
||||||
|
(&pl.common, wide_lwpolyline_fills(pl))
|
||||||
|
}
|
||||||
|
EntityType::Polyline2D(pl) => {
|
||||||
|
(&pl.common, wide_polyline2d_fills(pl))
|
||||||
|
}
|
||||||
|
_ => 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,
|
||||||
|
pattern: hatch_model::HatchPattern::Solid,
|
||||||
|
name: "SOLID".into(),
|
||||||
|
color,
|
||||||
|
angle_offset: 0.0,
|
||||||
|
scale: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
models
|
models
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2704,3 +2735,136 @@ fn entity_aabb(e: &acadrust::EntityType) -> [f32; 4] {
|
||||||
[min_x, min_y, max_x, max_y]
|
[min_x, min_y, max_x, max_y]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate solid-fill boundary polygons for each wide segment of an LWPolyline.
|
||||||
|
/// Returns one polygon per segment that has non-zero width; empty if the polyline
|
||||||
|
/// has zero `constant_width` and all vertex widths are zero.
|
||||||
|
fn wide_lwpolyline_fills(
|
||||||
|
pl: &acadrust::entities::LwPolyline,
|
||||||
|
) -> Vec<Vec<[f32; 2]>> {
|
||||||
|
let hw_const = (pl.constant_width / 2.0) as f32;
|
||||||
|
let verts = &pl.vertices;
|
||||||
|
let n = verts.len();
|
||||||
|
if n < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let seg_count = if pl.is_closed { n } else { n - 1 };
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for i in 0..seg_count {
|
||||||
|
let v0 = &verts[i];
|
||||||
|
let v1 = &verts[(i + 1) % n];
|
||||||
|
let hw0 = if v0.start_width > 1e-9 { v0.start_width as f32 / 2.0 } else { hw_const };
|
||||||
|
let hw1 = if v0.end_width > 1e-9 { v0.end_width as f32 / 2.0 } else { hw_const };
|
||||||
|
if hw0 < 1e-6 && hw1 < 1e-6 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p0 = [v0.location.x as f32, v0.location.y as f32];
|
||||||
|
let p1 = [v1.location.x as f32, v1.location.y as f32];
|
||||||
|
if let Some(poly) = polyline_segment_fill(p0, p1, hw0, hw1, v0.bulge as f32) {
|
||||||
|
out.push(poly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate solid-fill boundary polygons for each wide segment of a Polyline2D.
|
||||||
|
fn wide_polyline2d_fills(
|
||||||
|
pl: &acadrust::entities::Polyline2D,
|
||||||
|
) -> Vec<Vec<[f32; 2]>> {
|
||||||
|
let hw_default = (pl.start_width.max(pl.end_width) / 2.0) as f32;
|
||||||
|
let verts = &pl.vertices;
|
||||||
|
let n = verts.len();
|
||||||
|
if n < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let seg_count = if pl.is_closed() { n } else { n - 1 };
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for i in 0..seg_count {
|
||||||
|
let v0 = &verts[i];
|
||||||
|
let v1 = &verts[(i + 1) % n];
|
||||||
|
let hw0 = if v0.start_width > 1e-9 { v0.start_width as f32 / 2.0 } else { hw_default };
|
||||||
|
let hw1 = if v0.end_width > 1e-9 { v0.end_width as f32 / 2.0 } else { hw_default };
|
||||||
|
if hw0 < 1e-6 && hw1 < 1e-6 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p0 = [v0.location.x as f32, v0.location.y as f32];
|
||||||
|
let p1 = [v1.location.x as f32, v1.location.y as f32];
|
||||||
|
if let Some(poly) = polyline_segment_fill(p0, p1, hw0, hw1, v0.bulge as f32) {
|
||||||
|
out.push(poly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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).
|
||||||
|
/// Returns `None` if the segment is degenerate.
|
||||||
|
fn polyline_segment_fill(
|
||||||
|
p0: [f32; 2],
|
||||||
|
p1: [f32; 2],
|
||||||
|
hw0: f32,
|
||||||
|
hw1: f32,
|
||||||
|
bulge: f32,
|
||||||
|
) -> Option<Vec<[f32; 2]>> {
|
||||||
|
if bulge.abs() < 1e-9 {
|
||||||
|
// Straight segment — rectangle or trapezoid
|
||||||
|
let dx = p1[0] - p0[0];
|
||||||
|
let dy = p1[1] - p0[1];
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len < 1e-9 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let nx = -dy / len;
|
||||||
|
let ny = dx / len;
|
||||||
|
Some(vec![
|
||||||
|
[p0[0] + hw0 * nx, p0[1] + hw0 * ny],
|
||||||
|
[p1[0] + hw1 * nx, p1[1] + hw1 * ny],
|
||||||
|
[p1[0] - hw1 * nx, p1[1] - hw1 * ny],
|
||||||
|
[p0[0] - hw0 * nx, p0[1] - hw0 * ny],
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
// Arc segment — arc band polygon
|
||||||
|
let angle = 4.0 * (bulge as f64).atan();
|
||||||
|
let dx = (p1[0] - p0[0]) as f64;
|
||||||
|
let dy = (p1[1] - p0[1]) as f64;
|
||||||
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
|
if d < 1e-9 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let r = (d / 2.0) / (angle / 2.0).sin().abs();
|
||||||
|
let mx = ((p0[0] + p1[0]) * 0.5) as f64;
|
||||||
|
let my = ((p0[1] + p1[1]) * 0.5) as f64;
|
||||||
|
let px = -dy / d;
|
||||||
|
let py = dx / d;
|
||||||
|
let sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
||||||
|
let h = r - (r * r - d * d / 4.0).max(0.0).sqrt();
|
||||||
|
let cx = (mx - sign * px * (r - h)) as f32;
|
||||||
|
let cy = (my - sign * py * (r - h)) as f32;
|
||||||
|
let a0 = ((p0[1] - cy) as f32).atan2((p0[0] - cx) as f32);
|
||||||
|
let a1 = ((p1[1] - cy) as f32).atan2((p1[0] - cx) as f32);
|
||||||
|
let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) };
|
||||||
|
if ea < sa {
|
||||||
|
ea += std::f32::consts::TAU;
|
||||||
|
}
|
||||||
|
let span = ea - sa;
|
||||||
|
let segs = ((span.abs() / std::f32::consts::TAU) * 12.0)
|
||||||
|
.ceil()
|
||||||
|
.max(4.0) as u32;
|
||||||
|
let r = r as f32;
|
||||||
|
let hw = (hw0 + hw1) * 0.5;
|
||||||
|
let r_outer = r + hw;
|
||||||
|
let r_inner = (r - hw).max(0.0);
|
||||||
|
let mut boundary = Vec::with_capacity((segs as usize + 1) * 2);
|
||||||
|
for j in 0..=segs {
|
||||||
|
let t = sa + span * (j as f32 / segs as f32);
|
||||||
|
boundary.push([cx + r_outer * t.cos(), cy + r_outer * t.sin()]);
|
||||||
|
}
|
||||||
|
for j in (0..=segs).rev() {
|
||||||
|
let t = sa + span * (j as f32 / segs as f32);
|
||||||
|
boundary.push([cx + r_inner * t.cos(), cy + r_inner * t.sin()]);
|
||||||
|
}
|
||||||
|
boundary.truncate(64);
|
||||||
|
Some(boundary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue