forked from 1b-agent/opencadstudio-upstream
fix(arraypath): correct arc paths and align items to the tangent
Path arrays along arc segments came out mirrored: the LwPolyline bulge sampler placed the arc centre on the wrong side of the chord and ignored the polyline normal. Sample bulge arcs through the canonical BulgeArc and map OCS->WCS, matching the renderer. Sample standalone Arc/Circle paths via their OCS basis too. Also align each copy to the path tangent (the first item keeps its orientation), so items follow a curve instead of staying upright. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2be8197b4
commit
cf6dd174fc
1 file changed
+150
-80
@@ -383,17 +383,31 @@ impl CadCommand for ArrayPolarCommand {
|
|||||||
// → Returns BatchCopy with Translate transforms derived from path samples.
|
// → Returns BatchCopy with Translate transforms derived from path samples.
|
||||||
|
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
|
use std::f32::consts::PI as FPI;
|
||||||
use std::f32::consts::TAU as FTAU;
|
use std::f32::consts::TAU as FTAU;
|
||||||
|
|
||||||
// ── Path geometry helpers ──────────────────────────────────────────────────
|
// ── Path geometry helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Tessellate an LwPolyline.
|
/// Tessellate an LwPolyline into dense WCS points, matching the renderer:
|
||||||
|
/// bulge arcs use the canonical `BulgeArc` (correct centre side + signed
|
||||||
|
/// sweep) and every point is mapped OCS→WCS via the polyline's normal. The
|
||||||
|
/// previous ad-hoc bulge math placed the arc centre on the wrong side of the
|
||||||
|
/// chord and ignored the extrusion, so arc-segment paths came out mirrored.
|
||||||
fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> {
|
fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> {
|
||||||
let z = p.elevation as f32;
|
use crate::entities::common::BulgeArc;
|
||||||
|
use crate::scene::transform::ocs_point_to_wcs;
|
||||||
|
|
||||||
let verts = &p.vertices;
|
let verts = &p.vertices;
|
||||||
if verts.is_empty() {
|
if verts.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
let normal = (p.normal.x, p.normal.y, p.normal.z);
|
||||||
|
let elev = p.elevation;
|
||||||
|
let to_wcs = |x: f64, y: f64| -> Vec3 {
|
||||||
|
let (wx, wy, wz) = ocs_point_to_wcs((x, y, elev), normal);
|
||||||
|
Vec3::new(wx as f32, wy as f32, wz as f32)
|
||||||
|
};
|
||||||
|
|
||||||
let n = verts.len();
|
let n = verts.len();
|
||||||
let segs = if p.is_closed { n } else { n.saturating_sub(1) };
|
let segs = if p.is_closed { n } else { n.saturating_sub(1) };
|
||||||
let mut out: Vec<Vec3> = vec![];
|
let mut out: Vec<Vec3> = vec![];
|
||||||
@@ -401,58 +415,26 @@ fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> {
|
|||||||
for i in 0..segs {
|
for i in 0..segs {
|
||||||
let v0 = &verts[i];
|
let v0 = &verts[i];
|
||||||
let v1 = &verts[(i + 1) % n];
|
let v1 = &verts[(i + 1) % n];
|
||||||
let x0 = v0.location.x as f32;
|
let p0 = [v0.location.x, v0.location.y];
|
||||||
let y0 = v0.location.y as f32;
|
let p1 = [v1.location.x, v1.location.y];
|
||||||
let x1 = v1.location.x as f32;
|
|
||||||
let y1 = v1.location.y as f32;
|
|
||||||
|
|
||||||
if out.is_empty() {
|
if out.is_empty() {
|
||||||
out.push(Vec3::new(x0, y0, z));
|
out.push(to_wcs(p0[0], p0[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let bulge = v0.bulge as f32;
|
let bulge = v0.bulge;
|
||||||
if bulge.abs() < 1e-9 {
|
match (bulge.abs() >= 1e-9)
|
||||||
out.push(Vec3::new(x1, y1, z));
|
.then(|| BulgeArc::from_bulge(p0, p1, bulge))
|
||||||
} else {
|
.flatten()
|
||||||
let dx = x1 - x0;
|
{
|
||||||
let dy = y1 - y0;
|
Some(arc) => {
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let steps = ((arc.radius * arc.sweep.abs() / 2.0).ceil() as usize).clamp(4, 64);
|
||||||
if d < 1e-9 {
|
for j in 1..=steps {
|
||||||
out.push(Vec3::new(x1, y1, z));
|
let s = arc.sample(j as f64 / steps as f64);
|
||||||
continue;
|
out.push(to_wcs(s[0], s[1]));
|
||||||
}
|
|
||||||
let angle = 4.0 * bulge.atan();
|
|
||||||
let r = (d * 0.5) / (angle * 0.5).sin().abs();
|
|
||||||
let mx = (x0 + x1) * 0.5;
|
|
||||||
let my = (y0 + y1) * 0.5;
|
|
||||||
let inv = 1.0 / d;
|
|
||||||
let (px, py) = (-dy * inv, dx * inv);
|
|
||||||
let sign = if bulge > 0.0 { 1.0f32 } else { -1.0 };
|
|
||||||
let h_val = r - (r * r - d * d * 0.25).max(0.0).sqrt();
|
|
||||||
let (cx, cy) = (mx - sign * px * (r - h_val), my - sign * py * (r - h_val));
|
|
||||||
let a0 = (y0 - cy).atan2(x0 - cx);
|
|
||||||
let a1 = (y1 - cy).atan2(x1 - cx);
|
|
||||||
// Signed span: CCW for bulge > 0, CW (negative) for bulge < 0.
|
|
||||||
let span = if bulge > 0.0 {
|
|
||||||
let s = a1 - a0;
|
|
||||||
if s <= 0.0 {
|
|
||||||
s + FTAU
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let s = a1 - a0;
|
|
||||||
if s >= 0.0 {
|
|
||||||
s - FTAU
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let steps = ((r * span.abs() / 2.0).ceil() as usize).clamp(4, 64);
|
|
||||||
for j in 1..=steps {
|
|
||||||
let ang = a0 + span * (j as f32 / steps as f32);
|
|
||||||
out.push(Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), z));
|
|
||||||
}
|
}
|
||||||
|
None => out.push(to_wcs(p1[0], p1[1])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -533,7 +515,6 @@ impl ArrayPathCommand {
|
|||||||
if count == 0 {
|
if count == 0 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let fn_norm = |x: f32| -> f32 { ((x % FTAU) + FTAU) % FTAU };
|
|
||||||
match entity {
|
match entity {
|
||||||
EntityType::Line(l) => {
|
EntityType::Line(l) => {
|
||||||
let p0 = Vec3::new(l.start.x as f32, l.start.y as f32, 0.0);
|
let p0 = Vec3::new(l.start.x as f32, l.start.y as f32, 0.0);
|
||||||
@@ -542,31 +523,48 @@ impl ArrayPathCommand {
|
|||||||
(0..count).map(|i| p0.lerp(p1, i as f32 / d)).collect()
|
(0..count).map(|i| p0.lerp(p1, i as f32 / d)).collect()
|
||||||
}
|
}
|
||||||
EntityType::Arc(a) => {
|
EntityType::Arc(a) => {
|
||||||
let (cx, cy, r) = (a.center.x as f32, a.center.y as f32, a.radius as f32);
|
// Sample in WCS via the arc's OCS basis so flipped-normal arcs
|
||||||
let a0 = a.start_angle as f32;
|
// (normal = -Z) are traced on the same side the renderer draws,
|
||||||
let a1 = a.end_angle as f32;
|
// matching `entities::arc::to_truck`. Sampling the raw centre +
|
||||||
let span = {
|
// angle ignores the extrusion and lands on the mirrored arc.
|
||||||
let s = fn_norm(a1) - fn_norm(a0);
|
use crate::scene::transform::{ocs_axes, ocs_point_to_wcs};
|
||||||
if s <= 0.0 {
|
let normal = (a.normal.x, a.normal.y, a.normal.z);
|
||||||
s + FTAU
|
let (ax, ay) = ocs_axes(normal);
|
||||||
} else {
|
let (cx, cy, cz) =
|
||||||
s
|
ocs_point_to_wcs((a.center.x, a.center.y, a.center.z), normal);
|
||||||
}
|
let r = a.radius;
|
||||||
};
|
let (sa, ea) = (a.start_angle, a.end_angle);
|
||||||
let d = (count - 1).max(1) as f32;
|
let end = if ea >= sa { ea } else { ea + std::f64::consts::TAU };
|
||||||
|
let span = end - sa;
|
||||||
|
let d = (count - 1).max(1) as f64;
|
||||||
(0..count)
|
(0..count)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let ang = fn_norm(a0) + span * (i as f32 / d);
|
let ang = sa + span * (i as f64 / d);
|
||||||
Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), 0.0)
|
let (c, s) = (ang.cos(), ang.sin());
|
||||||
|
Vec3::new(
|
||||||
|
(cx + r * c * ax.0 + r * s * ay.0) as f32,
|
||||||
|
(cy + r * c * ax.1 + r * s * ay.1) as f32,
|
||||||
|
(cz + r * c * ax.2 + r * s * ay.2) as f32,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
EntityType::Circle(c) => {
|
EntityType::Circle(c) => {
|
||||||
let (cx, cy, r) = (c.center.x as f32, c.center.y as f32, c.radius as f32);
|
use crate::scene::transform::{ocs_axes, ocs_point_to_wcs};
|
||||||
|
let normal = (c.normal.x, c.normal.y, c.normal.z);
|
||||||
|
let (ax, ay) = ocs_axes(normal);
|
||||||
|
let (cx, cy, cz) =
|
||||||
|
ocs_point_to_wcs((c.center.x, c.center.y, c.center.z), normal);
|
||||||
|
let r = c.radius;
|
||||||
(0..count)
|
(0..count)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let ang = i as f32 / count as f32 * FTAU;
|
let ang = i as f64 / count as f64 * std::f64::consts::TAU;
|
||||||
Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), 0.0)
|
let (cs, sn) = (ang.cos(), ang.sin());
|
||||||
|
Vec3::new(
|
||||||
|
(cx + r * cs * ax.0 + r * sn * ay.0) as f32,
|
||||||
|
(cy + r * cs * ax.1 + r * sn * ay.1) as f32,
|
||||||
|
(cz + r * cs * ax.2 + r * sn * ay.2) as f32,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -575,17 +573,86 @@ impl ArrayPathCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build Translate transforms: delta from pts[0] to each subsequent point.
|
/// Continuous path-tangent angle (XY) at each sample, via central
|
||||||
|
/// differences and unwrapped so the sequence stays smooth across an arc
|
||||||
|
/// that turns more than ±π in total.
|
||||||
|
fn tangents(pts: &[Vec3]) -> Vec<f32> {
|
||||||
|
let n = pts.len();
|
||||||
|
let mut a = vec![0.0f32; n];
|
||||||
|
for i in 0..n {
|
||||||
|
let prev = if i > 0 { pts[i - 1] } else { pts[i] };
|
||||||
|
let next = if i + 1 < n { pts[i + 1] } else { pts[i] };
|
||||||
|
let d = next - prev;
|
||||||
|
a[i] = if d.x.abs() < 1e-12 && d.y.abs() < 1e-12 {
|
||||||
|
if i > 0 {
|
||||||
|
a[i - 1]
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d.y.atan2(d.x)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Unwrap to remove ±2π jumps.
|
||||||
|
for i in 1..n {
|
||||||
|
while a[i] - a[i - 1] > FPI {
|
||||||
|
a[i] -= FTAU;
|
||||||
|
}
|
||||||
|
while a[i] - a[i - 1] <= -FPI {
|
||||||
|
a[i] += FTAU;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build per-copy transforms that array the selection along the path with
|
||||||
|
/// the items aligned to the path tangent (the first item keeps its drawn
|
||||||
|
/// orientation; each copy rotates by the tangent change at its sample).
|
||||||
|
/// On a straight path the tangent never changes, so every copy is a pure
|
||||||
|
/// `Translate` — identical to a non-aligned array.
|
||||||
fn build_transforms(pts: &[Vec3]) -> Vec<EntityTransform> {
|
fn build_transforms(pts: &[Vec3]) -> Vec<EntityTransform> {
|
||||||
if pts.len() < 2 {
|
if pts.len() < 2 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let origin = pts[0];
|
let p0 = pts[0];
|
||||||
pts[1..]
|
let tans = Self::tangents(pts);
|
||||||
.iter()
|
let t0 = tans[0];
|
||||||
.map(|&p| EntityTransform::Translate(p - origin))
|
pts.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(1)
|
||||||
|
.map(|(i, &p)| {
|
||||||
|
let dth = tans[i] - t0;
|
||||||
|
if dth.abs() < 1e-5 {
|
||||||
|
EntityTransform::Translate(p - p0)
|
||||||
|
} else {
|
||||||
|
// The aligned copy is the rigid motion x' = p + R(x - p0),
|
||||||
|
// which is a pure rotation by `dth` about its fixed point.
|
||||||
|
let center = Self::rigid_center(p0, p, dth);
|
||||||
|
EntityTransform::Rotate {
|
||||||
|
center,
|
||||||
|
angle_rad: dth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fixed point of the rigid motion `x' = pi + R(dth)·(x - p0)`; rotating an
|
||||||
|
/// entity by `dth` about this point reproduces that motion exactly.
|
||||||
|
fn rigid_center(p0: Vec3, pi: Vec3, dth: f32) -> Vec3 {
|
||||||
|
let (s, c) = dth.sin_cos();
|
||||||
|
// b = pi - R·p0
|
||||||
|
let bx = pi.x - (c * p0.x - s * p0.y);
|
||||||
|
let by = pi.y - (s * p0.x + c * p0.y);
|
||||||
|
// c = (I - R)^{-1} · b, with (I-R)^{-1} = 1/(2(1-c)) [[1-c, -s],[s, 1-c]]
|
||||||
|
let one_c = 1.0 - c;
|
||||||
|
let det = 2.0 * one_c;
|
||||||
|
Vec3::new(
|
||||||
|
(one_c * bx - s * by) / det,
|
||||||
|
(s * bx + one_c * by) / det,
|
||||||
|
p0.z,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for ArrayPathCommand {
|
impl CadCommand for ArrayPathCommand {
|
||||||
@@ -692,15 +759,18 @@ impl CadCommand for ArrayPathCommand {
|
|||||||
let transforms = Self::build_transforms(&pts);
|
let transforms = Self::build_transforms(&pts);
|
||||||
transforms
|
transforms
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|t| {
|
.flat_map(|t| match t {
|
||||||
if let EntityTransform::Translate(delta) = t {
|
EntityTransform::Translate(delta) => self
|
||||||
self.wire_models
|
.wire_models
|
||||||
.iter()
|
.iter()
|
||||||
.map(|w| w.translated(*delta))
|
.map(|w| w.translated(*delta))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>(),
|
||||||
} else {
|
EntityTransform::Rotate { center, angle_rad } => self
|
||||||
vec![]
|
.wire_models
|
||||||
}
|
.iter()
|
||||||
|
.map(|w| w.rotated(*center, *angle_rad))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
_ => vec![],
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user