fix(measure): space points along a curve by distance, not by parameter
DIVIDE and MEASURE did nothing at all on an ELLIPSE or a SPLINE. Their length function answered `0.0` for both — the `_ => 0.0` arm — and the command reported the entity type as unsupported. On a polyline they measured the chords and ignored the bulges, so points landed short of where they belonged; on an arc or a circle they read the centre as world coordinates when the entity stores it in its OCS. All three now walk the entity's own curve, which the kernel measures properly: closed forms where they exist, and the integral of the speed where they do not. A path ARRAY had the same confusion in a different place. It sampled the path at even *parameters*, which is the same as even distances on a line and a circle and nothing like it on an ellipse or a spline — copies bunched up wherever the parameter ran slow. It also now closes properly: on a closed path the last copy no longer lands on the first. LENGTHEN measured an ellipse with a hundred and twenty-eight chords and then bisected on that, and a spline with forty rounds of thirty-two chords apiece. A chord sum reads short, so "current length" was already below the true one before any delta was applied. Both now ask the kernel, which also makes the spline case one evaluation instead of about thirteen hundred. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e6345f89f7
commit
d32f39221c
4 changed files with 118 additions and 405 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -935,7 +935,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git#9406868b1bedd57d121b11fc75658bf9eaa2c764"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git#92ef66a4e124881252d10eef066c65bd129ebddc"
|
||||
dependencies = [
|
||||
"cavalier_contours",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// DIVIDE command — place Point entities at N equal intervals along an entity.
|
||||
// MEASURE command — place Point entities at fixed-distance intervals along an entity.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use acadrust::entities::Point as PointEnt;
|
||||
use acadrust::kernel::space::PlanarCurve;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use crate::entities::curve::entity_curve;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
|
@ -164,16 +164,12 @@ pub fn divide_entity(entity: &EntityType, n: usize) -> Vec<EntityType> {
|
|||
if n < 2 {
|
||||
return vec![];
|
||||
}
|
||||
let total = entity_length(entity);
|
||||
if total < 1e-10 {
|
||||
let Some((curve, total)) = measurable(entity) else {
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
let step = total / n as f64;
|
||||
(1..n)
|
||||
.filter_map(|k| {
|
||||
let t = step * k as f64;
|
||||
point_at_distance(entity, t).map(make_point)
|
||||
})
|
||||
.map(|k| make_point(curve.point_at_distance(step * k as f64)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -182,135 +178,30 @@ pub fn measure_entity(entity: &EntityType, segment_length: f64) -> Vec<EntityTyp
|
|||
if segment_length <= 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let total = entity_length(entity);
|
||||
if total < 1e-10 {
|
||||
let Some((curve, total)) = measurable(entity) else {
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
let mut pts = Vec::new();
|
||||
let mut t = segment_length;
|
||||
while t < total - 1e-6 {
|
||||
if let Some(p) = point_at_distance(entity, t) {
|
||||
pts.push(make_point(p));
|
||||
}
|
||||
t += segment_length;
|
||||
let mut walked = segment_length;
|
||||
while walked < total - 1e-6 {
|
||||
pts.push(make_point(curve.point_at_distance(walked)));
|
||||
walked += segment_length;
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
fn make_point(pos: Vector3) -> EntityType {
|
||||
fn make_point(pos: [f64; 3]) -> EntityType {
|
||||
let mut p = PointEnt::new();
|
||||
p.location = pos;
|
||||
p.location = Vector3::new(pos[0], pos[1], pos[2]);
|
||||
EntityType::Point(p)
|
||||
}
|
||||
|
||||
fn entity_length(entity: &EntityType) -> f64 {
|
||||
match entity {
|
||||
EntityType::Line(l) => {
|
||||
let dx = l.end.x - l.start.x;
|
||||
let dy = l.end.y - l.start.y;
|
||||
let dz = l.end.z - l.start.z;
|
||||
(dx * dx + dy * dy + dz * dz).sqrt()
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let span = arc_span_rad(a.start_angle, a.end_angle);
|
||||
a.radius * span
|
||||
}
|
||||
EntityType::Circle(c) => 2.0 * PI * c.radius,
|
||||
EntityType::LwPolyline(p) => {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let segs = if p.is_closed { n } else { n - 1 };
|
||||
(0..segs)
|
||||
.map(|i| {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
(dx * dx + dy * dy).sqrt()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
||||
match entity {
|
||||
EntityType::Line(l) => {
|
||||
let total = entity_length(entity);
|
||||
if total < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let t = (d / total).clamp(0.0, 1.0);
|
||||
Some(Vector3::new(
|
||||
l.start.x + t * (l.end.x - l.start.x),
|
||||
l.start.y + t * (l.end.y - l.start.y),
|
||||
l.start.z + t * (l.end.z - l.start.z),
|
||||
))
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let span = arc_span_rad(a.start_angle, a.end_angle);
|
||||
let t = d / a.radius; // arc_length = r * theta
|
||||
if t > span {
|
||||
return None;
|
||||
}
|
||||
let angle = a.start_angle + t;
|
||||
Some(Vector3::new(
|
||||
a.center.x + a.radius * angle.cos(),
|
||||
a.center.y + a.radius * angle.sin(),
|
||||
a.center.z,
|
||||
))
|
||||
}
|
||||
EntityType::Circle(c) => {
|
||||
let circumference = 2.0 * PI * c.radius;
|
||||
if circumference < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let angle = 2.0 * PI * (d / circumference);
|
||||
Some(Vector3::new(
|
||||
c.center.x + c.radius * angle.cos(),
|
||||
c.center.y + c.radius * angle.sin(),
|
||||
c.center.z,
|
||||
))
|
||||
}
|
||||
EntityType::LwPolyline(p) => {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
let segs = if p.is_closed { n } else { n - 1 };
|
||||
let mut acc = 0.0f64;
|
||||
for i in 0..segs {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
let seg_len = (dx * dx + dy * dy).sqrt();
|
||||
if acc + seg_len >= d - 1e-10 {
|
||||
let t = (d - acc) / seg_len.max(1e-10);
|
||||
return Some(Vector3::new(
|
||||
v0.location.x + t * dx,
|
||||
v0.location.y + t * dy,
|
||||
p.elevation,
|
||||
));
|
||||
}
|
||||
acc += seg_len;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn arc_span_rad(start: f64, end: f64) -> f64 {
|
||||
let span = (end - start).rem_euclid(2.0 * PI);
|
||||
if span < 1e-6 {
|
||||
2.0 * PI
|
||||
} else {
|
||||
span
|
||||
}
|
||||
/// The entity's curve and its length, or `None` for anything that cannot be
|
||||
/// walked along — a hatch, a block, an unbounded ray.
|
||||
fn measurable(entity: &EntityType) -> Option<(PlanarCurve, f64)> {
|
||||
let curve = entity_curve(entity)?;
|
||||
let total = curve.curve.length();
|
||||
(total.is_finite() && total > 1e-10).then_some((curve, total))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -422,102 +422,13 @@ impl CadCommand for ArrayPolarCommand {
|
|||
// → Returns BatchCopy with Translate transforms derived from path samples.
|
||||
|
||||
use acadrust::EntityType;
|
||||
use crate::entities::curve::entity_curve;
|
||||
use std::f64::consts::PI as FPI;
|
||||
use std::f64::consts::TAU as FTAU;
|
||||
|
||||
// ── Path geometry helpers ──────────────────────────────────────────────────
|
||||
|
||||
/// 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<DVec3> {
|
||||
use crate::entities::common::BulgeArc;
|
||||
use crate::scene::view::transform::ocs_point_to_wcs;
|
||||
|
||||
let verts = &p.vertices;
|
||||
if verts.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let normal = (p.normal.x, p.normal.y, p.normal.z);
|
||||
let elev = p.elevation;
|
||||
let to_wcs = |x: f64, y: f64| -> DVec3 {
|
||||
let (wx, wy, wz) = ocs_point_to_wcs((x, y, elev), normal);
|
||||
DVec3::new(wx, wy, wz)
|
||||
};
|
||||
|
||||
let n = verts.len();
|
||||
let segs = if p.is_closed { n } else { n.saturating_sub(1) };
|
||||
let mut out: Vec<DVec3> = vec![];
|
||||
|
||||
for i in 0..segs {
|
||||
let v0 = &verts[i];
|
||||
let v1 = &verts[(i + 1) % n];
|
||||
let p0 = [v0.location.x, v0.location.y];
|
||||
let p1 = [v1.location.x, v1.location.y];
|
||||
|
||||
if out.is_empty() {
|
||||
out.push(to_wcs(p0[0], p0[1]));
|
||||
}
|
||||
|
||||
let bulge = v0.bulge;
|
||||
match (bulge.abs() >= 1e-9)
|
||||
.then(|| BulgeArc::from_bulge(p0, p1, bulge))
|
||||
.flatten()
|
||||
{
|
||||
Some(arc) => {
|
||||
let steps = ((arc.radius * arc.sweep.abs() / 2.0).ceil() as usize).clamp(4, 64);
|
||||
for j in 1..=steps {
|
||||
let s = arc.sample(j as f64 / steps as f64);
|
||||
out.push(to_wcs(s[0], s[1]));
|
||||
}
|
||||
}
|
||||
None => out.push(to_wcs(p1[0], p1[1])),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Walk `pts` (ordered) and return `count` points at equal arc-length spacing.
|
||||
fn subsample_equidistant(pts: &[DVec3], count: usize) -> Vec<DVec3> {
|
||||
if count == 0 {
|
||||
return vec![];
|
||||
}
|
||||
if pts.len() < 2 {
|
||||
return vec![pts.first().copied().unwrap_or(DVec3::ZERO); count];
|
||||
}
|
||||
|
||||
let mut cum = vec![0.0f64; pts.len()];
|
||||
for i in 1..pts.len() {
|
||||
cum[i] = cum[i - 1] + pts[i].distance(pts[i - 1]);
|
||||
}
|
||||
let total = *cum.last().unwrap();
|
||||
if total < 1e-9 {
|
||||
return vec![pts[0]; count];
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(count);
|
||||
let mut seg = 0usize;
|
||||
for i in 0..count {
|
||||
let target = if count > 1 {
|
||||
total * i as f64 / (count - 1) as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
while seg + 2 < pts.len() && cum[seg + 1] < target - 1e-9 {
|
||||
seg += 1;
|
||||
}
|
||||
let seg_len = cum[seg + 1] - cum[seg];
|
||||
let t = if seg_len > 1e-9 {
|
||||
(target - cum[seg]) / seg_len
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
out.push(pts[seg].lerp(pts[seg + 1], t.clamp(0.0, 1.0)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── State machine ──────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -570,66 +481,38 @@ impl ArrayPathCommand {
|
|||
}
|
||||
|
||||
/// Sample `count` evenly-spaced points along `entity`.
|
||||
/// `count` points spaced evenly **by distance** along the path.
|
||||
///
|
||||
/// By distance, not by parameter. The two agree on a line, a circle and a
|
||||
/// circular arc, which is how a per-type version got away with confusing
|
||||
/// them; on an ellipse or a spline they do not, and copies laid out at
|
||||
/// even parameters bunch up wherever the parameter runs slow. The path's
|
||||
/// own OCS is honoured too, so an arc carrying a mirrored normal is
|
||||
/// walked along the side it is drawn on.
|
||||
fn sample_path(entity: &EntityType, count: usize) -> Vec<DVec3> {
|
||||
if count == 0 {
|
||||
return vec![];
|
||||
}
|
||||
match entity {
|
||||
EntityType::Line(l) => {
|
||||
let p0 = DVec3::new(l.start.x, l.start.y, 0.0);
|
||||
let p1 = DVec3::new(l.end.x, l.end.y, 0.0);
|
||||
let d = (count - 1).max(1) as f64;
|
||||
(0..count).map(|i| p0.lerp(p1, i as f64 / d)).collect()
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
// Sample in WCS via the arc's OCS basis so flipped-normal arcs
|
||||
// (normal = -Z) are traced on the same side the renderer draws,
|
||||
// matching `entities::arc::to_truck`. Sampling the raw centre +
|
||||
// angle ignores the extrusion and lands on the mirrored arc.
|
||||
use crate::scene::view::transform::{ocs_axes, ocs_point_to_wcs};
|
||||
let normal = (a.normal.x, a.normal.y, a.normal.z);
|
||||
let (ax, ay) = ocs_axes(normal);
|
||||
let (cx, cy, cz) =
|
||||
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 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)
|
||||
.map(|i| {
|
||||
let ang = sa + span * (i as f64 / d);
|
||||
let (c, s) = (ang.cos(), ang.sin());
|
||||
DVec3::new(
|
||||
cx + r * c * ax.0 + r * s * ay.0,
|
||||
cy + r * c * ax.1 + r * s * ay.1,
|
||||
cz + r * c * ax.2 + r * s * ay.2,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::Circle(c) => {
|
||||
use crate::scene::view::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)
|
||||
.map(|i| {
|
||||
let ang = i as f64 / count as f64 * std::f64::consts::TAU;
|
||||
let (cs, sn) = (ang.cos(), ang.sin());
|
||||
DVec3::new(
|
||||
cx + r * cs * ax.0 + r * sn * ay.0,
|
||||
cy + r * cs * ax.1 + r * sn * ay.1,
|
||||
cz + r * cs * ax.2 + r * sn * ay.2,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::LwPolyline(p) => subsample_equidistant(&lw_dense_pts(p), count),
|
||||
_ => vec![DVec3::ZERO; count],
|
||||
let Some(curve) = entity_curve(entity) else {
|
||||
return vec![DVec3::ZERO; count];
|
||||
};
|
||||
let total = curve.length();
|
||||
if !total.is_finite() || total <= 0.0 {
|
||||
return vec![DVec3::ZERO; count];
|
||||
}
|
||||
// A closed path has no far end to stop short of, so the last copy
|
||||
// must not land on top of the first.
|
||||
let steps = if curve.is_closed() {
|
||||
count as f64
|
||||
} else {
|
||||
(count - 1).max(1) as f64
|
||||
};
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let p = curve.point_at_distance(total * (i as f64 / steps));
|
||||
DVec3::new(p[0], p[1], p[2])
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Continuous path-tangent angle (XY) at each sample, via central
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ use crate::modules::draw::modify::spline_ops::{spline_cut, spline_to_nurbs};
|
|||
use acadrust::entities::{
|
||||
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt,
|
||||
};
|
||||
use acadrust::kernel::geom2d::{
|
||||
Curve, Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc,
|
||||
};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::{DVec3, Vec3};
|
||||
|
|
@ -18,6 +21,8 @@ use crate::t;
|
|||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
|
||||
pub struct LengthenCommand {
|
||||
state: LenState,
|
||||
}
|
||||
|
|
@ -263,71 +268,64 @@ fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
|||
let t0 = ell.start_parameter;
|
||||
let mut t1 = ell.end_parameter;
|
||||
if t1 <= t0 {
|
||||
t1 += std::f64::consts::TAU;
|
||||
t1 += TAU;
|
||||
}
|
||||
let span = t1 - t0;
|
||||
|
||||
// Approximate arc length via 128-point Gaussian quadrature estimate.
|
||||
let arc_len_approx = |span: f64| -> f64 {
|
||||
let n = 128usize;
|
||||
let mut len = 0.0;
|
||||
for i in 0..n {
|
||||
let ti = t0 + span * (i as f64 / n as f64);
|
||||
let tip = t0 + span * ((i + 1) as f64 / n as f64);
|
||||
let xi = a * ti.cos() * nx - b * ti.sin() * ny + ell.center.x;
|
||||
let yi = a * ti.cos() * ny + b * ti.sin() * nx + ell.center.y;
|
||||
let xip = a * tip.cos() * nx - b * tip.sin() * ny + ell.center.x;
|
||||
let yip = a * tip.cos() * ny + b * tip.sin() * nx + ell.center.y;
|
||||
len += (xip - xi).hypot(yip - yi);
|
||||
}
|
||||
len
|
||||
// Measured by the kernel rather than by a hundred and twenty-eight
|
||||
// chords: the chord sum reads short, so LENGTHEN's idea of "current" was
|
||||
// already below the true length before a delta was applied to it.
|
||||
let shape = KernelEllipse {
|
||||
centre: [ell.center.x, ell.center.y],
|
||||
major_radius: a,
|
||||
minor_radius: b,
|
||||
major_axis: [nx, ny],
|
||||
};
|
||||
|
||||
let current_len = arc_len_approx(span);
|
||||
let arc = |from: f64, to: f64| {
|
||||
Curve::Ellipse(KernelEllipseArc {
|
||||
ellipse: shape,
|
||||
start_parameter: from,
|
||||
end_parameter: to,
|
||||
})
|
||||
};
|
||||
let current_len = arc(t0, t1).length();
|
||||
if current_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_len = apply_mode(current_len, mode)?;
|
||||
if new_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the new span via bisection so that arc_len_approx(new_span) ≈ new_len.
|
||||
let max_span = std::f64::consts::TAU;
|
||||
let mut lo = 0.0f64;
|
||||
let mut hi = max_span;
|
||||
for _ in 0..40 {
|
||||
let mid = (lo + hi) * 0.5;
|
||||
if arc_len_approx(mid) < new_len {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
let new_span = (lo + hi) * 0.5;
|
||||
|
||||
// Determine which end is closer to pick_pt (use DXF XY plane).
|
||||
let p_x = pick_pt.x as f64;
|
||||
let p_y = pick_pt.y as f64;
|
||||
let pt_start_x = ell.center.x + a * t0.cos() * nx - b * t0.sin() * ny;
|
||||
let pt_start_y = ell.center.y + a * t0.cos() * ny + b * t0.sin() * nx;
|
||||
let pt_end_x = ell.center.x + a * t1.cos() * nx - b * t1.sin() * ny;
|
||||
let pt_end_y = ell.center.y + a * t1.cos() * ny + b * t1.sin() * nx;
|
||||
let dist_start = (p_x - pt_start_x).hypot(p_y - pt_start_y);
|
||||
let dist_end = (p_x - pt_end_x).hypot(p_y - pt_end_y);
|
||||
// Which end is closer to the pick, in the DXF XY plane.
|
||||
let point_at = |t: f64| {
|
||||
(
|
||||
ell.center.x + a * t.cos() * nx - b * t.sin() * ny,
|
||||
ell.center.y + a * t.cos() * ny + b * t.sin() * nx,
|
||||
)
|
||||
};
|
||||
let (p_x, p_y) = (pick_pt.x as f64, pick_pt.y as f64);
|
||||
let (sx, sy) = point_at(t0);
|
||||
let (ex, ey) = point_at(t1);
|
||||
let extend_end = (p_x - ex).hypot(p_y - ey) <= (p_x - sx).hypot(p_y - sy);
|
||||
|
||||
let mut result = ell.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
|
||||
if dist_end <= dist_start {
|
||||
result.end_parameter = t0 + new_span;
|
||||
if extend_end {
|
||||
// Walk `new_len` forward from the fixed start. A whole turn is the
|
||||
// most there is to walk, and the kernel clamps to it.
|
||||
let whole = arc(t0, t0 + TAU);
|
||||
result.end_parameter = t0 + whole.parameter_at_distance(new_len) * TAU;
|
||||
} else {
|
||||
result.start_parameter = t1 - new_span;
|
||||
// The same measured backwards from the fixed end: the last `new_len`
|
||||
// of a whole turn ending at t1.
|
||||
let whole = arc(t1 - TAU, t1);
|
||||
let from_start = whole.length() - new_len;
|
||||
result.start_parameter = t1 - TAU + whole.parameter_at_distance(from_start) * TAU;
|
||||
}
|
||||
Some(EntityType::Ellipse(result))
|
||||
}
|
||||
|
||||
|
||||
fn apply_mode(current: f64, mode: &LenMode) -> Option<f64> {
|
||||
match mode {
|
||||
LenMode::Delta(d) => Some(current + d),
|
||||
|
|
@ -337,9 +335,9 @@ fn apply_mode(current: f64, mode: &LenMode) -> Option<f64> {
|
|||
}
|
||||
|
||||
fn arc_span_rad(start: f64, end: f64) -> f64 {
|
||||
let span = (end - start).rem_euclid(std::f64::consts::TAU);
|
||||
let span = (end - start).rem_euclid(TAU);
|
||||
if span < 1e-6 {
|
||||
std::f64::consts::TAU
|
||||
TAU
|
||||
} else {
|
||||
span
|
||||
}
|
||||
|
|
@ -410,103 +408,44 @@ fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
|||
}
|
||||
|
||||
fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||
let curve = spline_to_nurbs(spl)?;
|
||||
let (t0, t1) = curve.domain();
|
||||
let nurbs = spline_to_nurbs(spl)?;
|
||||
let (t0, t1) = nurbs.domain();
|
||||
if (t1 - t0).abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Approximate arc length via 64-point numerical integration.
|
||||
let arc_len = {
|
||||
let n = 64usize;
|
||||
let mut len = 0.0f64;
|
||||
for i in 0..n {
|
||||
let ta = t0 + (t1 - t0) * (i as f64 / n as f64);
|
||||
let tb = t0 + (t1 - t0) * ((i + 1) as f64 / n as f64);
|
||||
let pa = curve.point_at_knot(ta);
|
||||
let pb = curve.point_at_knot(tb);
|
||||
len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
|
||||
}
|
||||
len
|
||||
};
|
||||
let curve = Curve::Nurbs(nurbs.clone());
|
||||
let arc_len = curve.length();
|
||||
if arc_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_len = apply_mode(arc_len, mode)?;
|
||||
if new_len < 1e-10 {
|
||||
if new_len < 1e-10 || new_len >= arc_len {
|
||||
// A spline is shortened by splitting it, so there is nothing to keep
|
||||
// if the new length is the whole of it or more. Extending would mean
|
||||
// continuing the curve past its own control polygon, which is a
|
||||
// different operation from cutting one.
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine which end (start or end) is closer to pick_pt.
|
||||
let p_start = curve.point_at_knot(t0);
|
||||
let p_end = curve.point_at_knot(t1);
|
||||
let dist_start = (p_start[0] - pick_pt.x as f64).hypot(p_start[1] - pick_pt.y as f64);
|
||||
let dist_end = (p_end[0] - pick_pt.x as f64).hypot(p_end[1] - pick_pt.y as f64);
|
||||
let extend_end = dist_end <= dist_start;
|
||||
let p_start = nurbs.point_at_knot(t0);
|
||||
let p_end = nurbs.point_at_knot(t1);
|
||||
let (px, py) = (pick_pt.x as f64, pick_pt.y as f64);
|
||||
let extend_end = (p_end[0] - px).hypot(p_end[1] - py)
|
||||
<= (p_start[0] - px).hypot(p_start[1] - py);
|
||||
|
||||
// Find the parameter `t_new` such that the arc length from the fixed end to t_new = new_len.
|
||||
// Use bisection on cumulative arc length.
|
||||
let fixed_t = if extend_end { t0 } else { t1 };
|
||||
let delta_ratio = new_len / arc_len;
|
||||
|
||||
// Find t_new via bisection: cumulative_len(fixed_t..t_new) = new_len.
|
||||
let cum_len = |t_end_param: f64| -> f64 {
|
||||
let (lo, hi) = if extend_end {
|
||||
(t0, t_end_param)
|
||||
} else {
|
||||
(t_end_param, t1)
|
||||
};
|
||||
if hi <= lo {
|
||||
return 0.0;
|
||||
}
|
||||
let n = 32usize;
|
||||
let mut len = 0.0f64;
|
||||
for i in 0..n {
|
||||
let ta = lo + (hi - lo) * (i as f64 / n as f64);
|
||||
let tb = lo + (hi - lo) * ((i + 1) as f64 / n as f64);
|
||||
let pa = curve.point_at_knot(ta);
|
||||
let pb = curve.point_at_knot(tb);
|
||||
len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
|
||||
}
|
||||
len
|
||||
};
|
||||
|
||||
let (mut lo_t, mut hi_t) = if extend_end {
|
||||
(t0, t0 + delta_ratio * (t1 - t0) * 2.0)
|
||||
// Where to cut, by distance along the curve rather than by a bisection
|
||||
// over repeated chord sums. Keeping the head means cutting `new_len` from
|
||||
// the start; keeping the tail means cutting what is left over.
|
||||
let along = if extend_end {
|
||||
new_len
|
||||
} else {
|
||||
(t1 - delta_ratio * (t1 - t0) * 2.0, t1)
|
||||
arc_len - new_len
|
||||
};
|
||||
// Clamp to valid range with some buffer for extension.
|
||||
let buf = (t1 - t0) * 0.5;
|
||||
lo_t = lo_t.max(t0 - buf);
|
||||
hi_t = hi_t.min(t1 + buf);
|
||||
|
||||
for _ in 0..40 {
|
||||
let mid = (lo_t + hi_t) * 0.5;
|
||||
if cum_len(mid) < new_len {
|
||||
if extend_end {
|
||||
hi_t = mid;
|
||||
} else {
|
||||
lo_t = mid;
|
||||
}
|
||||
} else {
|
||||
if extend_end {
|
||||
lo_t = mid;
|
||||
} else {
|
||||
hi_t = mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
let t_new = (lo_t + hi_t) * 0.5;
|
||||
let _ = fixed_t;
|
||||
|
||||
// Split the spline at t_new and keep the side the pick asked for.
|
||||
let cut = t_new.clamp(t0 + 1e-10, t1 - 1e-10);
|
||||
let at = curve.parameter_at_distance(along);
|
||||
let cut = (t0 + at * (t1 - t0)).clamp(t0 + 1e-10, t1 - 1e-10);
|
||||
let (left, right) = spline_cut(spl, cut)?;
|
||||
Some(EntityType::Spline(if extend_end { left } else { right }))
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["LENGTHEN"] }); // LengthenCommand
|
||||
|
|
|
|||
Loading…
Reference in a new issue