forked from 1b-agent/opencadstudio-upstream
1420 lines
48 KiB
Rust
1420 lines
48 KiB
Rust
use acadrust::entities::Spline;
|
|
use cadkernel::geom2d::Curve as KernelCurve;
|
|
use cadkernel::space::{NurbsCurve3, Parameterization};
|
|
|
|
use crate::command::EntityTransform;
|
|
use crate::entities::common::{
|
|
dropdown_grip, edit_prop as edit, edit_scalar_prop as edit_scalar, parse_f64,
|
|
ro_prop as ro, round_grip, square_grip,
|
|
};
|
|
use crate::entities::traits::RenderConvertible;
|
|
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
|
use crate::t;
|
|
|
|
pub(crate) fn uses_fit_method(spline: &Spline) -> bool {
|
|
let degree = spline.degree.max(1) as usize;
|
|
!spline.fit_points.is_empty() && spline.control_points.len() <= degree
|
|
}
|
|
|
|
pub(crate) fn shows_fit_points(spline: &Spline) -> bool {
|
|
!spline.fit_points.is_empty() && (!spline.cv_frame_visible || spline.flags.periodic)
|
|
}
|
|
|
|
pub(crate) fn shows_control_vertices(spline: &Spline) -> bool {
|
|
!shows_fit_points(spline)
|
|
&& !(uses_fit_method(spline) && spline.flags.periodic)
|
|
&& control_vertices(spline).len() >= 2
|
|
}
|
|
|
|
pub(crate) fn fit_nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
|
|
if !uses_fit_method(spline) {
|
|
return None;
|
|
}
|
|
let mut points: Vec<_> = spline.fit_points.iter()
|
|
.map(|point| [point.x, point.y, point.z]).collect();
|
|
let parameterization = match spline.knot_parameterization {
|
|
1 => Parameterization::Centripetal,
|
|
2 => Parameterization::Uniform,
|
|
_ => Parameterization::Chord,
|
|
};
|
|
if spline.flags.periodic {
|
|
return NurbsCurve3::interpolate_periodic(&points, parameterization);
|
|
}
|
|
if spline.flags.closed && points.first() != points.last() {
|
|
if let Some(first) = points.first().copied() { points.push(first); }
|
|
}
|
|
let tangent = |value: acadrust::types::Vector3| {
|
|
let value = [value.x, value.y, value.z];
|
|
(value.iter().map(|component| component * component).sum::<f64>() > 1e-18)
|
|
.then_some(value)
|
|
};
|
|
NurbsCurve3::interpolate_fit(&points, tangent(spline.begin_tangent),
|
|
tangent(spline.end_tangent), parameterization)
|
|
}
|
|
|
|
pub(crate) fn nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
|
|
let curve = if uses_fit_method(spline) {
|
|
fit_nurbs3(spline)?
|
|
} else {
|
|
let degree = usize::try_from(spline.degree).ok()?;
|
|
let weights = if spline.weights.is_empty() {
|
|
vec![1.0; spline.control_points.len()]
|
|
} else {
|
|
spline.weights.clone()
|
|
};
|
|
NurbsCurve3::new_strict(
|
|
degree,
|
|
spline.control_points.iter().map(|point| [point.x, point.y, point.z]).collect(),
|
|
spline.knots.clone(),
|
|
weights,
|
|
)?
|
|
};
|
|
Some(curve.with_periodicity(spline.flags.closed || spline.flags.periodic))
|
|
}
|
|
|
|
/// NURBS knot locations on a control-point spline, evaluated on its own
|
|
/// curve: distinct interior knot values (the clamped ends already snap as
|
|
/// endpoints). Fit-method splines invent their knots at the fit nodes, which
|
|
/// already snap as endpoints, so they contribute nothing — re-offering them
|
|
/// would double every marker.
|
|
pub(crate) fn spline_knot_points(spline: &Spline) -> Vec<glam::DVec3> {
|
|
const MAX_KNOT_SNAPS: usize = 1024;
|
|
if uses_fit_method(spline) {
|
|
return Vec::new();
|
|
}
|
|
let Some(curve) = nurbs3(spline) else {
|
|
return Vec::new();
|
|
};
|
|
let (from, to) = curve.domain();
|
|
if !(from.is_finite() && to.is_finite()) || !(to > from) {
|
|
return Vec::new();
|
|
}
|
|
let span = to - from;
|
|
let mut knots: Vec<f64> = curve
|
|
.knots()
|
|
.iter()
|
|
.copied()
|
|
.filter(|k| k.is_finite() && *k > from + span * 1e-9 && *k < to - span * 1e-9)
|
|
.collect();
|
|
knots.sort_by(f64::total_cmp);
|
|
knots.dedup();
|
|
if knots.len() > MAX_KNOT_SNAPS {
|
|
let step = knots.len().div_ceil(MAX_KNOT_SNAPS);
|
|
knots = knots.into_iter().step_by(step).collect();
|
|
}
|
|
knots
|
|
.into_iter()
|
|
.map(|k| glam::DVec3::from_array(curve.point_at_knot(k)))
|
|
.filter(|p| p.is_finite())
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn replace_with_nurbs(spline: &mut Spline, curve: &NurbsCurve3) {
|
|
let mut fit_points = spline.fit_points.clone();
|
|
spline.degree = curve.degree() as i32;
|
|
spline.knots = curve.knots().to_vec();
|
|
spline.control_points = curve.control_points().iter()
|
|
.map(|point| acadrust::types::Vector3::new(point[0], point[1], point[2])).collect();
|
|
spline.weights = curve.weights().to_vec();
|
|
if !fit_points.is_empty() {
|
|
if let (Some(first), Some(point)) = (fit_points.first_mut(), spline.control_points.first()) {
|
|
*first = *point;
|
|
}
|
|
if let (Some(last), Some(point)) = (fit_points.last_mut(), spline.control_points.last()) {
|
|
*last = *point;
|
|
}
|
|
}
|
|
spline.fit_points = fit_points;
|
|
spline.begin_tangent = acadrust::types::Vector3::ZERO;
|
|
spline.end_tangent = acadrust::types::Vector3::ZERO;
|
|
spline.flags.rational = spline.weights.windows(2)
|
|
.any(|pair| (pair[0] - pair[1]).abs() > 1e-12);
|
|
spline.flags.closed = false;
|
|
spline.flags.periodic = false;
|
|
spline.dwg_flags1 &= !1;
|
|
spline.dxf_flags &= !(32 | 1024);
|
|
}
|
|
|
|
/// Conservative world bounds of a fit-method spline. Fit points are interpolation
|
|
/// constraints, not control-hull vertices, so their box can exclude the curve.
|
|
/// The shared kernel reconstructs the same spatial interpolation used by the
|
|
/// curve consumers, then its control hull bounds every point of that curve.
|
|
pub(crate) fn fit_geometry_bounds(spline: &Spline) -> Option<cadkernel::brep::Aabb> {
|
|
let curve = fit_nurbs3(spline)?;
|
|
cadkernel::brep::Aabb::around(curve.control_points().iter().copied())
|
|
}
|
|
fn to_render(spl: &Spline) -> RenderEntity {
|
|
let n = spl.control_points.len();
|
|
if n < 2 {
|
|
// A fit-point spline (DWG scenario 2 / R2013+) stores only the points
|
|
// the curve passes through — no control points or knots. Interpolate a
|
|
// smooth Catmull-Rom curve through them and hand it back as a dense
|
|
// polyline so it still draws.
|
|
if spl.fit_points.len() >= 2 {
|
|
// Drawn from the same interpolation TRIM and snap use, so that
|
|
// what is on screen is the curve those commands cut. The two had
|
|
// parted: this drew a C² cubic solved here (or a Catmull-Rom for a
|
|
// closed spline), while the kernel interpolated its own — near
|
|
// enough to look right and far enough apart that a trim landed
|
|
// beside the line it was aimed at.
|
|
let planar = crate::entities::curve::spline_curve(spl);
|
|
let pts = match &planar {
|
|
Some(curve) => crate::entities::curve::curve_points(curve),
|
|
// A fit spline through points in space is not a planar curve,
|
|
// so the kernel has nothing to say about it and the solve
|
|
// here remains the only description of its shape.
|
|
None if spl.flags.periodic => periodic_fit_spline_polyline(spl),
|
|
None if spl.flags.closed => {
|
|
catmull_rom_polyline(&spl.fit_points, true)
|
|
}
|
|
None => fit_spline_polyline(spl),
|
|
};
|
|
let snap = planar
|
|
.as_ref()
|
|
.map(crate::entities::curve::snap_from)
|
|
.unwrap_or_default();
|
|
// Fit points are on the curve, so they stay on offer as ends in
|
|
// their own right — but never through `key_vertices`, whose
|
|
// consecutive entries the snap engine joins and offers the middle
|
|
// of. The chord between two fit points of a curvy spline does not
|
|
// run along it.
|
|
let mut snap_pts = snap.snap_pts;
|
|
snap_pts.extend(spl.fit_points.iter().map(|p| {
|
|
(
|
|
glam::DVec3::new(p.x, p.y, p.z),
|
|
crate::scene::model::wire_model::SnapHint::Endpoint,
|
|
)
|
|
}));
|
|
snap_pts.extend(
|
|
spline_knot_points(spl)
|
|
.into_iter()
|
|
.map(|p| (p, crate::scene::model::wire_model::SnapHint::Knot)),
|
|
);
|
|
let key_vertices = if planar.is_some() {
|
|
Vec::new()
|
|
} else {
|
|
spl.fit_points.iter().map(|p| [p.x, p.y, p.z]).collect()
|
|
};
|
|
return RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object: RenderObject::Lines(pts),
|
|
snap_pts,
|
|
tangent_geoms: vec![],
|
|
key_vertices,
|
|
fill_tris: vec![],
|
|
};
|
|
}
|
|
return RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object: RenderObject::Lines(Vec::new()),
|
|
snap_pts: vec![],
|
|
tangent_geoms: vec![],
|
|
key_vertices: vec![],
|
|
fill_tris: vec![],
|
|
};
|
|
}
|
|
|
|
// Snap sources from the spline's own curve. A spline is not a chain of
|
|
// straight segments, so nothing goes in `key_vertices`: consecutive
|
|
// entries there are joined and their midpoints offered, and the chord
|
|
// between two fit points of a curvy spline does not run along it.
|
|
//
|
|
// Fit points are on the curve and stay on offer, as ends in their own
|
|
// right. Control points are not on the curve and are dropped — snapping
|
|
// to one put the cursor in empty space beside the geometry.
|
|
let curve_snap = crate::entities::curve::spline_curve(spl)
|
|
.map(|curve| crate::entities::curve::snap_from(&curve));
|
|
let (snap_pts, key_vertices) = match curve_snap {
|
|
Some(snap) => {
|
|
let mut points = snap.snap_pts;
|
|
if uses_fit_method(spl) {
|
|
points.extend(spl.fit_points.iter().map(|p| {
|
|
(
|
|
glam::DVec3::new(p.x, p.y, p.z),
|
|
crate::scene::model::wire_model::SnapHint::Endpoint,
|
|
)
|
|
}));
|
|
}
|
|
points.extend(
|
|
spline_knot_points(spl)
|
|
.into_iter()
|
|
.map(|p| (p, crate::scene::model::wire_model::SnapHint::Knot)),
|
|
);
|
|
(points, Vec::new())
|
|
}
|
|
// A spline through points in space is not a planar curve, so the
|
|
// kernel has nothing to say about it. Its stored points remain the
|
|
// only thing to offer.
|
|
None => {
|
|
let source = if uses_fit_method(spl) {
|
|
&spl.fit_points
|
|
} else {
|
|
&spl.control_points
|
|
};
|
|
(
|
|
Vec::new(),
|
|
source.iter().map(|p| [p.x, p.y, p.z]).collect::<Vec<_>>(),
|
|
)
|
|
}
|
|
};
|
|
|
|
// Sampled through the spline's own curve — the same definition EXTRUDE
|
|
// and REVOLVE read — and closed back onto its start when the flags say
|
|
// it is periodic and the two ends do not already meet.
|
|
let is_closed = spl.flags.closed || spl.flags.periodic;
|
|
let mut points = crate::entities::curve::spline_curve(spl)
|
|
.map(|planar| crate::entities::curve::curve_points(&planar))
|
|
.unwrap_or_else(|| measurement_polyline(spl));
|
|
if is_closed {
|
|
if let (Some(first), Some(last)) = (points.first().copied(), points.last().copied()) {
|
|
let gap = ((last[0] - first[0]).powi(2)
|
|
+ (last[1] - first[1]).powi(2)
|
|
+ (last[2] - first[2]).powi(2))
|
|
.sqrt();
|
|
if gap > 1e-6 {
|
|
points.push(first);
|
|
}
|
|
}
|
|
}
|
|
let object = RenderObject::Lines(points);
|
|
|
|
RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object,
|
|
snap_pts,
|
|
tangent_geoms: vec![],
|
|
key_vertices,
|
|
fill_tris: vec![],
|
|
}
|
|
}
|
|
|
|
pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
|
if uses_fit_method(spl) {
|
|
if spl.fit_points.len() < 2 {
|
|
return spl.fit_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
|
}
|
|
return if spl.flags.periodic {
|
|
periodic_fit_spline_polyline(spl)
|
|
} else if spl.flags.closed {
|
|
catmull_rom_polyline(&spl.fit_points, true)
|
|
} else {
|
|
fit_spline_polyline(spl)
|
|
};
|
|
}
|
|
|
|
let count = spl.control_points.len();
|
|
if count < 2 {
|
|
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
|
}
|
|
|
|
let degree = spl.degree.max(0) as usize;
|
|
if degree == 0 || degree >= count {
|
|
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
|
}
|
|
// The kernel's space curve holds rational and polynomial curves alike.
|
|
let controls: Vec<[f64; 3]> = spl
|
|
.control_points
|
|
.iter()
|
|
.map(|point| [point.x, point.y, point.z])
|
|
.collect();
|
|
let weights = (spl.weights.len() == count).then(|| {
|
|
spl.weights
|
|
.iter()
|
|
.map(|weight| if weight.abs() < 1e-12 { 1.0 } else { *weight })
|
|
.collect()
|
|
});
|
|
match NurbsCurve3::new(degree, controls, spl.knots.clone(), weights) {
|
|
Some(curve) => {
|
|
curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
|
}
|
|
None => spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect(),
|
|
}
|
|
}
|
|
|
|
/// Sample a Catmull-Rom spline through `pts` into a dense polyline. The curve
|
|
/// passes through every input point; open ends use reflected phantom points so
|
|
/// they don't kink, closed curves wrap around.
|
|
fn catmull_rom_polyline(pts: &[acadrust::types::Vector3], closed: bool) -> Vec<[f64; 3]> {
|
|
let n = pts.len();
|
|
if n < 2 {
|
|
return pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
|
}
|
|
let get = |i: isize| -> [f64; 3] {
|
|
let j = if closed {
|
|
let m = n as isize;
|
|
(((i % m) + m) % m) as usize
|
|
} else {
|
|
i.clamp(0, n as isize - 1) as usize
|
|
};
|
|
[pts[j].x, pts[j].y, pts[j].z]
|
|
};
|
|
let seg_count = if closed { n } else { n - 1 };
|
|
let mut out = Vec::new();
|
|
for seg in 0..seg_count {
|
|
let p1 = get(seg as isize);
|
|
let p2 = get(seg as isize + 1);
|
|
// Reflect at open ends so the tangent isn't pulled toward a clamped dup.
|
|
let p0 = if !closed && seg == 0 {
|
|
[
|
|
2.0 * p1[0] - p2[0],
|
|
2.0 * p1[1] - p2[1],
|
|
2.0 * p1[2] - p2[2],
|
|
]
|
|
} else {
|
|
get(seg as isize - 1)
|
|
};
|
|
let p3 = if !closed && seg == seg_count - 1 {
|
|
[
|
|
2.0 * p2[0] - p1[0],
|
|
2.0 * p2[1] - p1[1],
|
|
2.0 * p2[2] - p1[2],
|
|
]
|
|
} else {
|
|
get(seg as isize + 2)
|
|
};
|
|
let point_at = |t: f64| {
|
|
let (t2, t3) = (t * t, t * t * t);
|
|
let mut q = [0.0f64; 3];
|
|
for k in 0..3 {
|
|
q[k] = 0.5
|
|
* (2.0 * p1[k]
|
|
+ (-p0[k] + p2[k]) * t
|
|
+ (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2
|
|
+ (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3);
|
|
}
|
|
q
|
|
};
|
|
let tangent_at = |t: f64| {
|
|
let mut q = [0.0; 3];
|
|
for k in 0..3 {
|
|
let a = -p0[k] + p2[k];
|
|
let b = 2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k];
|
|
let c = -p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k];
|
|
q[k] = 0.5 * (a + 2.0 * b * t + 3.0 * c * t * t);
|
|
}
|
|
q
|
|
};
|
|
let sampled = cadkernel::tessellation::sample_curve3_angle(
|
|
point_at,
|
|
tangent_at,
|
|
cadkernel::tessellation::DEFAULT_ANGLE,
|
|
);
|
|
out.extend(sampled.into_iter().skip(usize::from(seg > 0)));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Interpolate an open fit-point spline into a dense polyline: the C² cubic that
|
|
/// passes through every fit point, clamped to the stored start/end tangents when
|
|
/// present (natural end otherwise), so its ends follow the specified tangents
|
|
/// instead of the local slopes Catmull-Rom would use.
|
|
fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
|
let p: Vec<[f64; 3]> = spl.fit_points.iter().map(|q| [q.x, q.y, q.z]).collect();
|
|
let n = p.len();
|
|
if n < 2 {
|
|
return p;
|
|
}
|
|
|
|
let Some((h, slopes)) = fit_spline_slopes(spl, &p) else {
|
|
return p;
|
|
};
|
|
|
|
// Evaluate each segment as a cubic Hermite (dP/du = slope · h_i).
|
|
let mut out = Vec::new();
|
|
for i in 0..n - 1 {
|
|
let point_at = |u: f64| {
|
|
let (u2, u3) = (u * u, u * u * u);
|
|
let h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
|
|
let h10 = u3 - 2.0 * u2 + u;
|
|
let h01 = -2.0 * u3 + 3.0 * u2;
|
|
let h11 = u3 - u2;
|
|
let mut q = [0.0f64; 3];
|
|
for k in 0..3 {
|
|
let m0 = slopes[k][i] * h[i];
|
|
let m1 = slopes[k][i + 1] * h[i];
|
|
q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1;
|
|
}
|
|
q
|
|
};
|
|
let tangent_at = |u: f64| {
|
|
let u2 = u * u;
|
|
let (h00, h10, h01, h11) = (
|
|
6.0 * u2 - 6.0 * u,
|
|
3.0 * u2 - 4.0 * u + 1.0,
|
|
-6.0 * u2 + 6.0 * u,
|
|
3.0 * u2 - 2.0 * u,
|
|
);
|
|
let mut q = [0.0; 3];
|
|
for k in 0..3 {
|
|
let m0 = slopes[k][i] * h[i];
|
|
let m1 = slopes[k][i + 1] * h[i];
|
|
q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1;
|
|
}
|
|
q
|
|
};
|
|
let sampled = cadkernel::tessellation::sample_curve3_angle(
|
|
point_at,
|
|
tangent_at,
|
|
cadkernel::tessellation::DEFAULT_ANGLE,
|
|
);
|
|
out.extend(sampled.into_iter().skip(usize::from(i > 0)));
|
|
}
|
|
out
|
|
}
|
|
|
|
fn periodic_fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
|
let points: Vec<[f64; 3]> = spl
|
|
.fit_points
|
|
.iter()
|
|
.map(|point| [point.x, point.y, point.z])
|
|
.collect();
|
|
let parameterization = match spl.knot_parameterization {
|
|
2 => Parameterization::Uniform,
|
|
1 => Parameterization::Centripetal,
|
|
_ => Parameterization::Chord,
|
|
};
|
|
NurbsCurve3::interpolate_periodic(&points, parameterization)
|
|
.map(|curve| curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE))
|
|
.unwrap_or_else(|| catmull_rom_polyline(&spl.fit_points, true))
|
|
}
|
|
|
|
/// Slopes used by spatial fit interpolation.
|
|
fn fit_spline_slopes(spl: &Spline, p: &[[f64; 3]]) -> Option<(Vec<f64>, [Vec<f64>; 3])> {
|
|
let n = p.len();
|
|
if n < 2 {
|
|
return None;
|
|
}
|
|
|
|
// Parameterise the fit points (unnormalised, so a unit end tangent — how the
|
|
// tangents are stored — is a consistent dP/dt). Match the spline's knot
|
|
// parameterisation: 2 = uniform, 1 = centripetal (√chord), else chord.
|
|
let dist = |a: [f64; 3], b: [f64; 3]| {
|
|
let (dx, dy, dz) = (b[0] - a[0], b[1] - a[1], b[2] - a[2]);
|
|
(dx * dx + dy * dy + dz * dz).sqrt()
|
|
};
|
|
let mut t = vec![0.0f64; n];
|
|
for i in 1..n {
|
|
let d = dist(p[i - 1], p[i]).max(1e-9);
|
|
let step = match spl.knot_parameterization {
|
|
2 => 1.0,
|
|
1 => d.sqrt(),
|
|
_ => d,
|
|
};
|
|
t[i] = t[i - 1] + step;
|
|
}
|
|
let h: Vec<f64> = (0..n - 1).map(|i| (t[i + 1] - t[i]).max(1e-9)).collect();
|
|
|
|
let nonzero = |v: &acadrust::types::Vector3| v.x * v.x + v.y * v.y + v.z * v.z > 1e-18;
|
|
let begin = nonzero(&spl.begin_tangent).then(|| {
|
|
[
|
|
spl.begin_tangent.x,
|
|
spl.begin_tangent.y,
|
|
spl.begin_tangent.z,
|
|
]
|
|
});
|
|
let end = nonzero(&spl.end_tangent)
|
|
.then(|| [spl.end_tangent.x, spl.end_tangent.y, spl.end_tangent.z]);
|
|
|
|
// Solve for the knot slopes m_i = dP/dt per coordinate. The tridiagonal is
|
|
// the C² continuity system; the end rows are the clamped tangent (m fixed)
|
|
// or the natural condition (S'' = 0) when no tangent is stored.
|
|
let mut slopes = [vec![0.0f64; n], vec![0.0f64; n], vec![0.0f64; n]];
|
|
for k in 0..3 {
|
|
let seg_slope = |i: usize| (p[i + 1][k] - p[i][k]) / h[i];
|
|
let (mut a, mut b, mut c, mut d) = (vec![0.0; n], vec![0.0; n], vec![0.0; n], vec![0.0; n]);
|
|
match begin {
|
|
Some(bt) => {
|
|
b[0] = 1.0;
|
|
d[0] = bt[k];
|
|
}
|
|
None => {
|
|
b[0] = 2.0;
|
|
c[0] = 1.0;
|
|
d[0] = 3.0 * seg_slope(0);
|
|
}
|
|
}
|
|
for i in 1..n - 1 {
|
|
a[i] = h[i];
|
|
b[i] = 2.0 * (h[i - 1] + h[i]);
|
|
c[i] = h[i - 1];
|
|
d[i] = 3.0 * (h[i] * seg_slope(i - 1) + h[i - 1] * seg_slope(i));
|
|
}
|
|
match end {
|
|
Some(et) => {
|
|
b[n - 1] = 1.0;
|
|
d[n - 1] = et[k];
|
|
}
|
|
None => {
|
|
a[n - 1] = 1.0;
|
|
b[n - 1] = 2.0;
|
|
d[n - 1] = 3.0 * seg_slope(n - 2);
|
|
}
|
|
}
|
|
thomas_solve(&a, &b, &c, &mut d);
|
|
slopes[k] = d;
|
|
}
|
|
Some((h, slopes))
|
|
}
|
|
|
|
/// In-place Thomas solve for a tridiagonal system (`a` sub-, `b` main-, `c`
|
|
/// super-diagonal; `d` right-hand side, overwritten with the solution).
|
|
fn thomas_solve(a: &[f64], b: &[f64], c: &[f64], d: &mut [f64]) {
|
|
let n = d.len();
|
|
let mut cp = vec![0.0f64; n];
|
|
cp[0] = c[0] / b[0];
|
|
d[0] /= b[0];
|
|
for i in 1..n {
|
|
let m = b[i] - a[i] * cp[i - 1];
|
|
cp[i] = c[i] / m;
|
|
d[i] = (d[i] - a[i] * d[i - 1]) / m;
|
|
}
|
|
for i in (0..n - 1).rev() {
|
|
d[i] -= cp[i] * d[i + 1];
|
|
}
|
|
}
|
|
|
|
pub(crate) const SPLINE_MODE_GRIP_ID: usize = usize::MAX - 1;
|
|
|
|
fn control_vertices(spline: &Spline) -> Vec<acadrust::types::Vector3> {
|
|
if !uses_fit_method(spline) {
|
|
return spline.control_points.clone();
|
|
}
|
|
let Some(planar) = crate::entities::curve::spline_curve(spline) else {
|
|
return Vec::new();
|
|
};
|
|
let KernelCurve::Nurbs(curve) = &planar.curve else {
|
|
return Vec::new();
|
|
};
|
|
curve
|
|
.control_points()
|
|
.iter()
|
|
.map(|point| {
|
|
let point = planar.plane.point_at(*point);
|
|
acadrust::types::Vector3::new(point[0], point[1], point[2])
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn control_vertex_count(spline: &Spline) -> usize {
|
|
control_vertices(spline).len()
|
|
}
|
|
|
|
fn choice_prop(
|
|
label: &str,
|
|
field: &'static str,
|
|
selected: &str,
|
|
options: &[&str],
|
|
) -> Property {
|
|
Property {
|
|
label: label.into(),
|
|
field,
|
|
value: PropValue::Choice {
|
|
selected: selected.to_string(),
|
|
options: options.iter().map(|option| option.to_string()).collect(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn index_prop(label: &str, field: &'static str, index: usize, count: usize) -> Property {
|
|
Property {
|
|
label: label.into(),
|
|
field,
|
|
value: PropValue::EditText(if count == 0 {
|
|
"0".to_string()
|
|
} else {
|
|
(index + 1).to_string()
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn convert_to_control_method(spline: &mut Spline) -> bool {
|
|
if !uses_fit_method(spline) {
|
|
let valid = spline.control_points.len() >= 2;
|
|
if valid {
|
|
spline.fit_points.clear();
|
|
spline.begin_tangent = acadrust::types::Vector3::ZERO;
|
|
spline.end_tangent = acadrust::types::Vector3::ZERO;
|
|
spline.dwg_flags1 &= !1;
|
|
spline.dxf_flags &= !(32 | 1024);
|
|
}
|
|
return valid;
|
|
}
|
|
let Some(planar) = crate::entities::curve::spline_curve(spline) else {
|
|
return false;
|
|
};
|
|
let KernelCurve::Nurbs(curve) = &planar.curve else {
|
|
return false;
|
|
};
|
|
spline.degree = curve.degree() as i32;
|
|
spline.knots = curve.knots().to_vec();
|
|
spline.control_points = curve
|
|
.control_points()
|
|
.iter()
|
|
.map(|point| {
|
|
let point = planar.plane.point_at(*point);
|
|
acadrust::types::Vector3::new(point[0], point[1], point[2])
|
|
})
|
|
.collect();
|
|
spline.weights = if curve.is_rational() {
|
|
curve.weights().to_vec()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
spline.flags.rational = curve.is_rational();
|
|
spline.flags.planar = true;
|
|
spline.fit_points.clear();
|
|
spline.begin_tangent = acadrust::types::Vector3::ZERO;
|
|
spline.end_tangent = acadrust::types::Vector3::ZERO;
|
|
true
|
|
}
|
|
|
|
fn convert_to_fit_method(spline: &mut Spline) -> bool {
|
|
if uses_fit_method(spline) {
|
|
return true;
|
|
}
|
|
if spline.fit_points.len() < 2 {
|
|
return false;
|
|
}
|
|
spline.control_points.clear();
|
|
spline.knots.clear();
|
|
spline.weights.clear();
|
|
spline.flags.rational = false;
|
|
spline.dwg_flags1 |= 1;
|
|
spline.dxf_flags |= 32 | 1024;
|
|
true
|
|
}
|
|
|
|
fn derived_fit_points(spline: &Spline) -> Vec<acadrust::types::Vector3> {
|
|
if spline.flags.closed || spline.flags.periodic {
|
|
return Vec::new();
|
|
}
|
|
let Some(curve) = nurbs3(spline) else {
|
|
return Vec::new();
|
|
};
|
|
let (start, end) = curve.domain();
|
|
if !(start.is_finite() && end.is_finite() && end > start) {
|
|
return Vec::new();
|
|
}
|
|
let tolerance = (end - start).abs().max(1.0) * 1.0e-12;
|
|
let mut parameters = vec![start];
|
|
for knot in curve.knots().iter().copied() {
|
|
if knot > start + tolerance
|
|
&& knot < end - tolerance
|
|
&& parameters
|
|
.last()
|
|
.is_none_or(|previous| (knot - *previous).abs() > tolerance)
|
|
{
|
|
parameters.push(knot);
|
|
}
|
|
}
|
|
parameters.push(end);
|
|
parameters
|
|
.into_iter()
|
|
.map(|parameter| {
|
|
let point = curve.point_at_knot(parameter);
|
|
acadrust::types::Vector3::new(point[0], point[1], point[2])
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn can_show_fit_points(spline: &Spline) -> bool {
|
|
spline.fit_points.len() >= 2 || derived_fit_points(spline).len() >= 2
|
|
}
|
|
|
|
fn prepare_fit_point_view(spline: &mut Spline) -> bool {
|
|
if spline.fit_points.len() < 2 {
|
|
spline.fit_points = derived_fit_points(spline);
|
|
}
|
|
spline.fit_points.len() >= 2
|
|
}
|
|
|
|
fn grips(spline: &Spline) -> Vec<GripDef> {
|
|
let derived_control = control_vertices(spline);
|
|
let show_fit = shows_fit_points(spline);
|
|
let show_control_vertices = !show_fit
|
|
&& !(uses_fit_method(spline) && spline.flags.periodic)
|
|
&& derived_control.len() >= 2;
|
|
let source = if show_control_vertices {
|
|
derived_control
|
|
} else if show_fit {
|
|
spline.fit_points.clone()
|
|
} else {
|
|
spline.control_points.clone()
|
|
};
|
|
let mut grips: Vec<_> = source
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, p)| {
|
|
let world = glam::DVec3::new(p.x, p.y, p.z);
|
|
if show_control_vertices {
|
|
round_grip(i, world)
|
|
} else {
|
|
square_grip(i, world)
|
|
}
|
|
})
|
|
.collect();
|
|
if let Some(first) = source.first() {
|
|
grips.push(dropdown_grip(
|
|
SPLINE_MODE_GRIP_ID,
|
|
glam::DVec3::new(first.x, first.y, first.z),
|
|
));
|
|
}
|
|
grips
|
|
}
|
|
|
|
fn properties(spline: &Spline) -> Vec<PropSection> {
|
|
let fit_method = uses_fit_method(spline);
|
|
let show_fit = shows_fit_points(spline);
|
|
let method = if show_fit { "Fit" } else { "Control Vertices" };
|
|
let closed = spline.flags.closed || spline.flags.periodic;
|
|
let can_show_control = control_vertices(spline).len() >= 2
|
|
&& !(fit_method && spline.flags.periodic);
|
|
let yes_no = |b: bool| if b { "Yes" } else { "No" };
|
|
let knot_param = match spline.knot_parameterization {
|
|
0 => "Chord",
|
|
1 => "Square Root",
|
|
2 => "Uniform",
|
|
15 => "Custom",
|
|
_ => "Custom",
|
|
};
|
|
let current = crate::scene::view::dispatch::prop_current_vertex();
|
|
let mut data_points = if show_fit {
|
|
let count = spline.fit_points.len();
|
|
let index = current.min(count.saturating_sub(1));
|
|
let point = spline.fit_points.get(index);
|
|
vec![
|
|
ro(
|
|
t!("Number of fit points").as_ref(),
|
|
"fit_pt_count",
|
|
count.to_string(),
|
|
),
|
|
index_prop(
|
|
t!("Current fit point").as_ref(),
|
|
"current_fit_point",
|
|
index,
|
|
count,
|
|
),
|
|
edit(
|
|
t!("Fit point X").as_ref(),
|
|
"fit_pt_x",
|
|
point.map(|value| value.x).unwrap_or(0.0),
|
|
),
|
|
edit(
|
|
t!("Fit point Y").as_ref(),
|
|
"fit_pt_y",
|
|
point.map(|value| value.y).unwrap_or(0.0),
|
|
),
|
|
edit(
|
|
t!("Fit point Z").as_ref(),
|
|
"fit_pt_z",
|
|
point.map(|value| value.z).unwrap_or(0.0),
|
|
),
|
|
if matches!(spline.knot_parameterization, 0..=2) {
|
|
choice_prop(
|
|
t!("Knot parameterization").as_ref(),
|
|
"knot_param",
|
|
knot_param,
|
|
&["Chord", "Square Root", "Uniform"],
|
|
)
|
|
} else {
|
|
ro(
|
|
t!("Knot parameterization").as_ref(),
|
|
"knot_param",
|
|
crate::i18n::translate(knot_param).into_owned(),
|
|
)
|
|
},
|
|
]
|
|
} else {
|
|
let controls = control_vertices(spline);
|
|
let count = controls.len();
|
|
let index = current.min(count.saturating_sub(1));
|
|
let point = controls.get(index);
|
|
let weight = spline.weights.get(index).copied().unwrap_or(1.0);
|
|
vec![
|
|
ro(
|
|
t!("Number of control points").as_ref(),
|
|
"ctrl_pt_count",
|
|
count.to_string(),
|
|
),
|
|
index_prop(
|
|
t!("Current Control point").as_ref(),
|
|
"current_control_point",
|
|
index,
|
|
count,
|
|
),
|
|
edit(
|
|
t!("Control point X").as_ref(),
|
|
"ctrl_pt_x",
|
|
point.map(|value| value.x).unwrap_or(0.0),
|
|
),
|
|
edit(
|
|
t!("Control point Y").as_ref(),
|
|
"ctrl_pt_y",
|
|
point.map(|value| value.y).unwrap_or(0.0),
|
|
),
|
|
edit(
|
|
t!("Control point Z").as_ref(),
|
|
"ctrl_pt_z",
|
|
point.map(|value| value.z).unwrap_or(0.0),
|
|
),
|
|
edit_scalar(t!("Weight").as_ref(), "weight", weight),
|
|
]
|
|
};
|
|
data_points.push(if can_show_control {
|
|
choice_prop(
|
|
t!("CV frame").as_ref(),
|
|
"cv_frame",
|
|
if spline.cv_frame_visible { "Show" } else { "Hide" },
|
|
&["Hide", "Show"],
|
|
)
|
|
} else {
|
|
ro(t!("CV frame").as_ref(), "cv_frame", t!("Hide").into_owned())
|
|
});
|
|
|
|
let method_prop = if fit_method && can_show_control {
|
|
choice_prop(
|
|
t!("Method").as_ref(),
|
|
"spline_method",
|
|
method,
|
|
&["Fit", "Control Vertices"],
|
|
)
|
|
} else {
|
|
ro(
|
|
t!("Method").as_ref(),
|
|
"spline_method",
|
|
crate::i18n::translate(method).into_owned(),
|
|
)
|
|
};
|
|
let mut misc = vec![
|
|
method_prop,
|
|
ro(t!("Degree").as_ref(), "degree", spline.degree.to_string()),
|
|
ro(
|
|
t!("Closed").as_ref(),
|
|
"closed",
|
|
crate::i18n::translate(yes_no(closed)).into_owned(),
|
|
),
|
|
ro(
|
|
t!("Periodic").as_ref(),
|
|
"periodic",
|
|
crate::i18n::translate(yes_no(spline.flags.periodic)).into_owned(),
|
|
),
|
|
ro(
|
|
t!("Planar").as_ref(),
|
|
"planar",
|
|
crate::i18n::translate(yes_no(crate::entities::curve::spline_is_planar(spline)))
|
|
.into_owned(),
|
|
),
|
|
];
|
|
if show_fit && !spline.flags.periodic {
|
|
misc.extend([
|
|
edit_scalar(
|
|
t!("Start tangent vector X").as_ref(),
|
|
"start_tan_x",
|
|
spline.begin_tangent.x,
|
|
),
|
|
edit_scalar(
|
|
t!("Start tangent vector Y").as_ref(),
|
|
"start_tan_y",
|
|
spline.begin_tangent.y,
|
|
),
|
|
edit_scalar(
|
|
t!("Start tangent vector Z").as_ref(),
|
|
"start_tan_z",
|
|
spline.begin_tangent.z,
|
|
),
|
|
edit_scalar(
|
|
t!("End tangent vector X").as_ref(),
|
|
"end_tan_x",
|
|
spline.end_tangent.x,
|
|
),
|
|
edit_scalar(
|
|
t!("End tangent vector Y").as_ref(),
|
|
"end_tan_y",
|
|
spline.end_tangent.y,
|
|
),
|
|
edit_scalar(
|
|
t!("End tangent vector Z").as_ref(),
|
|
"end_tan_z",
|
|
spline.end_tangent.z,
|
|
),
|
|
]);
|
|
}
|
|
if show_fit {
|
|
misc.push(edit(
|
|
t!("Fit tolerance").as_ref(),
|
|
"fit_tolerance",
|
|
spline.fit_tolerance,
|
|
));
|
|
}
|
|
|
|
vec![
|
|
PropSection {
|
|
title: t!("Data Points").into_owned(),
|
|
props: data_points,
|
|
},
|
|
PropSection {
|
|
title: t!("Misc").into_owned(),
|
|
props: misc,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
|
match field {
|
|
"spline_method" => {
|
|
if value == "Fit" {
|
|
if prepare_fit_point_view(spline) {
|
|
spline.cv_frame_visible = false;
|
|
}
|
|
} else if value == "Control Vertices" {
|
|
if !spline.flags.periodic && control_vertices(spline).len() >= 2 {
|
|
spline.cv_frame_visible = true;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
"knot_param" => {
|
|
if shows_fit_points(spline)
|
|
&& !uses_fit_method(spline)
|
|
&& !convert_to_fit_method(spline)
|
|
{
|
|
return;
|
|
}
|
|
spline.knot_parameterization = match value {
|
|
"Chord" => 0,
|
|
"Square Root" => 1,
|
|
"Uniform" => 2,
|
|
_ => return,
|
|
};
|
|
return;
|
|
}
|
|
"cv_frame" => {
|
|
spline.cv_frame_visible = value == "Show"
|
|
&& !(uses_fit_method(spline) && spline.flags.periodic)
|
|
&& control_vertices(spline).len() >= 2;
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
if uses_fit_method(spline)
|
|
&& spline.flags.periodic
|
|
&& matches!(
|
|
field,
|
|
"ctrl_pt_x"
|
|
| "ctrl_pt_y"
|
|
| "ctrl_pt_z"
|
|
| "weight"
|
|
| "start_tan_x"
|
|
| "start_tan_y"
|
|
| "start_tan_z"
|
|
| "end_tan_x"
|
|
| "end_tan_y"
|
|
| "end_tan_z"
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight") {
|
|
if !shows_fit_points(spline) && !spline.fit_points.is_empty() {
|
|
if !convert_to_control_method(spline) {
|
|
return;
|
|
}
|
|
} else if uses_fit_method(spline) && !convert_to_control_method(spline) {
|
|
return;
|
|
}
|
|
}
|
|
if shows_fit_points(spline)
|
|
&& !uses_fit_method(spline)
|
|
&& matches!(
|
|
field,
|
|
"fit_pt_x"
|
|
| "fit_pt_y"
|
|
| "fit_pt_z"
|
|
| "start_tan_x"
|
|
| "start_tan_y"
|
|
| "start_tan_z"
|
|
| "end_tan_x"
|
|
| "end_tan_y"
|
|
| "end_tan_z"
|
|
)
|
|
&& !convert_to_fit_method(spline)
|
|
{
|
|
return;
|
|
}
|
|
let Some(v) = parse_f64(value) else { return };
|
|
let control_index = crate::scene::view::dispatch::prop_current_vertex()
|
|
.min(spline.control_points.len().saturating_sub(1));
|
|
let fit_index = crate::scene::view::dispatch::prop_current_vertex()
|
|
.min(spline.fit_points.len().saturating_sub(1));
|
|
match field {
|
|
"ctrl_pt_x" => {
|
|
if let Some(cp) = spline.control_points.get_mut(control_index) {
|
|
cp.x = v;
|
|
}
|
|
}
|
|
"ctrl_pt_y" => {
|
|
if let Some(cp) = spline.control_points.get_mut(control_index) {
|
|
cp.y = v;
|
|
}
|
|
}
|
|
"ctrl_pt_z" => {
|
|
if let Some(cp) = spline.control_points.get_mut(control_index) {
|
|
cp.z = v;
|
|
}
|
|
}
|
|
"weight" => {
|
|
if v > 0.0 && !spline.control_points.is_empty() {
|
|
if spline.weights.len() != spline.control_points.len() {
|
|
spline.weights = vec![1.0; spline.control_points.len()];
|
|
}
|
|
spline.weights[control_index] = v;
|
|
spline.flags.rational = spline
|
|
.weights
|
|
.iter()
|
|
.any(|weight| (*weight - spline.weights[0]).abs() > 1e-12);
|
|
}
|
|
}
|
|
"fit_pt_x" => {
|
|
if let Some(fp) = spline.fit_points.get_mut(fit_index) {
|
|
fp.x = v;
|
|
}
|
|
}
|
|
"fit_pt_y" => {
|
|
if let Some(fp) = spline.fit_points.get_mut(fit_index) {
|
|
fp.y = v;
|
|
}
|
|
}
|
|
"fit_pt_z" => {
|
|
if let Some(fp) = spline.fit_points.get_mut(fit_index) {
|
|
fp.z = v;
|
|
}
|
|
}
|
|
"fit_tolerance" => {
|
|
if v.is_finite() && v >= 0.0 {
|
|
spline.fit_tolerance = v;
|
|
}
|
|
}
|
|
"start_tan_x" | "start_tan_y" | "start_tan_z" => {
|
|
match field {
|
|
"start_tan_x" => spline.begin_tangent.x = v,
|
|
"start_tan_y" => spline.begin_tangent.y = v,
|
|
"start_tan_z" => spline.begin_tangent.z = v,
|
|
_ => {}
|
|
}
|
|
}
|
|
"end_tan_x" | "end_tan_y" | "end_tan_z" => {
|
|
match field {
|
|
"end_tan_x" => spline.end_tangent.x = v,
|
|
"end_tan_y" => spline.end_tangent.y = v,
|
|
"end_tan_z" => spline.end_tangent.z = v,
|
|
_ => {}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
spline.flags.planar = crate::entities::curve::spline_is_planar(spline);
|
|
}
|
|
|
|
fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
|
if grip_id == SPLINE_MODE_GRIP_ID {
|
|
return;
|
|
}
|
|
// First CV edit makes the displayed kernel controls authoritative.
|
|
let editing_fit = shows_fit_points(spline);
|
|
if editing_fit && !uses_fit_method(spline) {
|
|
if !convert_to_fit_method(spline) {
|
|
return;
|
|
}
|
|
} else if !editing_fit
|
|
&& (uses_fit_method(spline) || !spline.fit_points.is_empty())
|
|
&& !spline.flags.periodic
|
|
{
|
|
if !convert_to_control_method(spline) {
|
|
return;
|
|
}
|
|
}
|
|
let target = if editing_fit {
|
|
spline.fit_points.get_mut(grip_id)
|
|
} else {
|
|
spline.control_points.get_mut(grip_id)
|
|
};
|
|
if let Some(cp) = target {
|
|
match apply {
|
|
GripApply::Absolute(p) => {
|
|
cp.x = p.x as f64;
|
|
cp.y = p.y as f64;
|
|
cp.z = p.z as f64;
|
|
}
|
|
GripApply::Translate(d) => {
|
|
cp.x += d.x as f64;
|
|
cp.y += d.y as f64;
|
|
cp.z += d.z as f64;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_transform(spline: &mut Spline, t: &EntityTransform) {
|
|
if let EntityTransform::Mirror {
|
|
p1,
|
|
p2,
|
|
working_normal,
|
|
} = t
|
|
{
|
|
let transform = crate::scene::view::transform::reflection_about_working_line(
|
|
*p1,
|
|
*p2,
|
|
*working_normal,
|
|
);
|
|
acadrust::Entity::apply_transform(spline, &transform);
|
|
spline.flags.planar = crate::entities::curve::spline_is_planar(spline);
|
|
return;
|
|
}
|
|
crate::scene::view::transform::apply_standard_entity_transform(spline, t, |entity, p1, p2| {
|
|
for cp in &mut entity.control_points {
|
|
crate::scene::view::transform::reflect_xy_point(&mut cp.x, &mut cp.y, p1, p2);
|
|
}
|
|
for fp in &mut entity.fit_points {
|
|
crate::scene::view::transform::reflect_xy_point(&mut fp.x, &mut fp.y, p1, p2);
|
|
}
|
|
});
|
|
spline.flags.planar = crate::entities::curve::spline_is_planar(spline);
|
|
}
|
|
|
|
impl RenderConvertible for Spline {
|
|
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
|
|
Some(to_render(self))
|
|
}
|
|
}
|
|
|
|
impl crate::entities::traits::Grippable for Spline {
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
grips(self)
|
|
}
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
apply_grip(self, grip_id, apply);
|
|
}
|
|
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
|
if grip_id == SPLINE_MODE_GRIP_ID {
|
|
let fit_method = uses_fit_method(self);
|
|
let can_show_control = control_vertices(self).len() >= 2
|
|
&& !(fit_method && self.flags.periodic);
|
|
let can_show_fit = can_show_fit_points(self);
|
|
return if !shows_fit_points(self) {
|
|
vec![
|
|
can_show_fit.then_some(GripMenuItem {
|
|
label: "Fit",
|
|
action: GripMenuAction::ShowFit,
|
|
}),
|
|
can_show_control.then_some(GripMenuItem {
|
|
label: "✓ Control Vertices",
|
|
action: GripMenuAction::ShowControlVertices,
|
|
}),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.collect()
|
|
} else {
|
|
vec![
|
|
Some(GripMenuItem {
|
|
label: "✓ Fit",
|
|
action: GripMenuAction::ShowFit,
|
|
}),
|
|
can_show_control.then_some(GripMenuItem {
|
|
label: "Control Vertices",
|
|
action: GripMenuAction::ShowControlVertices,
|
|
}),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.collect()
|
|
};
|
|
}
|
|
vec![GripMenuItem {
|
|
label: "Stretch",
|
|
action: GripMenuAction::Stretch,
|
|
}]
|
|
}
|
|
fn apply_grip_menu(
|
|
&mut self,
|
|
_grip_id: usize,
|
|
action: crate::scene::model::object::GripMenuAction,
|
|
) {
|
|
use crate::scene::model::object::GripMenuAction as A;
|
|
match action {
|
|
A::ShowFit => {
|
|
if prepare_fit_point_view(self) {
|
|
self.cv_frame_visible = false;
|
|
return;
|
|
}
|
|
}
|
|
A::ShowControlVertices
|
|
if !(uses_fit_method(self) && self.flags.periodic)
|
|
&& control_vertices(self).len() >= 2 =>
|
|
{
|
|
self.cv_frame_visible = true;
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl crate::entities::traits::PropertyEditable for Spline {
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
|
properties(self)
|
|
}
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
apply_geom_prop(self, field, value);
|
|
}
|
|
}
|
|
|
|
impl crate::entities::traits::Transformable for Spline {
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
apply_transform(self, t);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn knot_points_evaluate_distinct_interior_knots() {
|
|
let mut spline = Spline::default();
|
|
spline.degree = 3;
|
|
spline.control_points = vec![
|
|
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
|
acadrust::types::Vector3::new(1.0, 2.0, 0.0),
|
|
acadrust::types::Vector3::new(3.0, 3.0, 1.0),
|
|
acadrust::types::Vector3::new(5.0, 2.0, 0.0),
|
|
acadrust::types::Vector3::new(6.0, 0.0, 1.0),
|
|
];
|
|
spline.knots = vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0];
|
|
let pts = spline_knot_points(&spline);
|
|
assert_eq!(pts.len(), 1, "one interior knot value: {pts:?}");
|
|
let curve = nurbs3(&spline).expect("control-point spline builds");
|
|
let expected = curve.point_at_knot(0.5);
|
|
let got = [pts[0].x, pts[0].y, pts[0].z];
|
|
let err = ((got[0] - expected[0]).powi(2)
|
|
+ (got[1] - expected[1]).powi(2)
|
|
+ (got[2] - expected[2]).powi(2))
|
|
.sqrt();
|
|
assert!(err < 1e-9, "knot point off the curve: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn fit_method_splines_emit_no_knot_snaps() {
|
|
// Interpolated curves invent their own knots at the fit nodes, which
|
|
// already snap as endpoints — re-offering them as knots would double
|
|
// every marker.
|
|
let mut spline = Spline::default();
|
|
spline.degree = 3;
|
|
spline.fit_points = vec![
|
|
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
|
acadrust::types::Vector3::new(4.0, 0.0, 1.0),
|
|
acadrust::types::Vector3::new(4.0, 3.0, 2.0),
|
|
];
|
|
assert!(spline_knot_points(&spline).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn fit_bounds_accept_closed_curves_with_unset_tangents() {
|
|
let mut spline = Spline::default();
|
|
spline.degree = 3;
|
|
spline.flags.closed = true;
|
|
spline.control_points.push(acadrust::types::Vector3::ZERO);
|
|
spline.fit_points = vec![
|
|
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
|
acadrust::types::Vector3::new(4.0, 0.0, 1.0),
|
|
acadrust::types::Vector3::new(4.0, 3.0, 2.0),
|
|
acadrust::types::Vector3::new(0.0, 3.0, 1.0),
|
|
];
|
|
|
|
let bounds = fit_geometry_bounds(&spline).unwrap();
|
|
for point in &spline.fit_points {
|
|
for (value, min, max) in [
|
|
(point.x, bounds.min[0], bounds.max[0]),
|
|
(point.y, bounds.min[1], bounds.max[1]),
|
|
(point.z, bounds.min[2], bounds.max[2]),
|
|
] {
|
|
assert!(value >= min - 1e-9 && value <= max + 1e-9);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn fit_tolerance_is_editable_and_rejects_invalid_values() {
|
|
let mut spline = Spline::default();
|
|
spline.fit_points = vec![
|
|
acadrust::types::Vector3::ZERO,
|
|
acadrust::types::Vector3::new(1.0, 0.0, 0.0),
|
|
];
|
|
spline.fit_tolerance = 0.25;
|
|
|
|
let tolerance = properties(&spline)
|
|
.into_iter()
|
|
.flat_map(|section| section.props)
|
|
.find(|property| property.field == "fit_tolerance")
|
|
.unwrap();
|
|
assert!(matches!(tolerance.value, PropValue::EditText(_)));
|
|
|
|
apply_geom_prop(&mut spline, "fit_tolerance", "1.5");
|
|
assert_eq!(spline.fit_tolerance, 1.5);
|
|
for invalid in ["-1", "NaN", "inf"] {
|
|
apply_geom_prop(&mut spline, "fit_tolerance", invalid);
|
|
assert_eq!(spline.fit_tolerance, 1.5);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tangent_properties_edit_only_the_stored_component() {
|
|
let mut spline = Spline::default();
|
|
spline.fit_points = vec![
|
|
acadrust::types::Vector3::ZERO,
|
|
acadrust::types::Vector3::new(1.0, 1.0, 0.0),
|
|
];
|
|
|
|
let start_y = properties(&spline)
|
|
.into_iter()
|
|
.flat_map(|section| section.props)
|
|
.find(|property| property.field == "start_tan_y")
|
|
.unwrap();
|
|
assert_eq!(start_y.value, PropValue::EditText("0".into()));
|
|
|
|
apply_geom_prop(&mut spline, "start_tan_y", "2.5");
|
|
assert_eq!(
|
|
spline.begin_tangent,
|
|
acadrust::types::Vector3::new(0.0, 2.5, 0.0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn showing_derived_fit_points_does_not_change_the_curve() {
|
|
let mut spline = Spline::default();
|
|
spline.degree = 3;
|
|
spline.control_points = vec![
|
|
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
|
acadrust::types::Vector3::new(1.0, 3.0, 0.0),
|
|
acadrust::types::Vector3::new(4.0, 3.0, 0.0),
|
|
acadrust::types::Vector3::new(5.0, 0.0, 0.0),
|
|
];
|
|
spline.knots = cadkernel::space::clamped_uniform_knots(3, 4);
|
|
let before = nurbs3(&spline).unwrap().point_at(0.37);
|
|
|
|
assert!(prepare_fit_point_view(&mut spline));
|
|
spline.cv_frame_visible = false;
|
|
|
|
assert!(shows_fit_points(&spline));
|
|
assert!(!uses_fit_method(&spline));
|
|
let after = nurbs3(&spline).unwrap().point_at(0.37);
|
|
assert!(
|
|
cadkernel::space::Vec3::from(before)
|
|
.distance(cadkernel::space::Vec3::from(after))
|
|
< 1.0e-12
|
|
);
|
|
}
|
|
}
|