forked from 1b-agent/opencadstudio-upstream
Improve spline fit and control vertex grips
This commit is contained in:
1 parent
97a6322e2a
commit
0d37d3ff78
13 files changed
+471
-80
No files matched your search
@@ -1,7 +1,7 @@
|
||||
// Spline tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: SPLINE (SPL)
|
||||
// Click to add control points. Enter (≥2 pts) → commits EntityType::Spline.
|
||||
// Click to add fit points. Enter (≥2 pts) → commits EntityType::Spline.
|
||||
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Spline};
|
||||
@@ -11,7 +11,6 @@ use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
use cadkernel::space::NurbsCurve3;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn tool() -> ToolDef {
|
||||
@@ -36,19 +35,14 @@ impl SplineCommand {
|
||||
if self.pts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let control_points = self
|
||||
let fit_points = self
|
||||
.pts
|
||||
.iter()
|
||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
||||
.collect();
|
||||
let n = self.pts.len();
|
||||
// Uniform open knot vector for degree-3 clamped B-spline
|
||||
let degree = 3_i32.min((n - 1) as i32);
|
||||
let knots = uniform_knots(n, degree as usize);
|
||||
let mut spline = Spline {
|
||||
degree,
|
||||
control_points,
|
||||
knots,
|
||||
degree: 3,
|
||||
fit_points,
|
||||
..Default::default()
|
||||
};
|
||||
// Closed splines render with a segment bridging the last point back to
|
||||
@@ -58,48 +52,26 @@ impl SplineCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn uniform_knots(n: usize, d: usize) -> Vec<f64> {
|
||||
let m = n + d + 1;
|
||||
(0..m)
|
||||
.map(|i| {
|
||||
if i <= d {
|
||||
0.0
|
||||
} else if i >= m - d - 1 {
|
||||
1.0
|
||||
} else {
|
||||
(i - d) as f64 / (n - d) as f64
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sample the degree-3 clamped B-spline through `pts` into a dense polyline,
|
||||
/// built with the same control points + knot vector `build()` commits so the
|
||||
/// preview traces the exact final curve — not the straight control polygon.
|
||||
/// Fewer than 2 points has no curve; the points are returned as-is.
|
||||
fn sample_curve(pts: &[DVec3]) -> Vec<[f32; 3]> {
|
||||
let n = pts.len();
|
||||
if n < 2 {
|
||||
/// Preview through the same fit-point representation that `build()` commits.
|
||||
fn sample_curve(pts: &[DVec3], closed: bool) -> Vec<[f32; 3]> {
|
||||
if pts.len() < 2 {
|
||||
return pts
|
||||
.iter()
|
||||
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
||||
.collect();
|
||||
}
|
||||
let degree = 3_usize.min(n - 1);
|
||||
let ctrl: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
let Some(curve) = NurbsCurve3::new(degree, ctrl, uniform_knots(n, degree), None) else {
|
||||
return pts
|
||||
let mut spline = Spline {
|
||||
degree: 3,
|
||||
fit_points: pts
|
||||
.iter()
|
||||
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
||||
.collect();
|
||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
// Resolution scales with span count so long splines stay smooth.
|
||||
let steps = 24 * (n - 1);
|
||||
(0..=steps)
|
||||
.map(|i| {
|
||||
let p = curve.point_at(i as f64 / steps as f64);
|
||||
[p[0] as f32, p[1] as f32, p[2] as f32]
|
||||
})
|
||||
spline.flags.closed = closed;
|
||||
crate::entities::spline::measurement_polyline(&spline)
|
||||
.into_iter()
|
||||
.map(|p| [p[0] as f32, p[1] as f32, p[2] as f32])
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -110,7 +82,7 @@ impl CadCommand for SplineCommand {
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.pts.is_empty() {
|
||||
t!("SPLINE Specify first control point:").into_owned()
|
||||
t!("SPLINE Specify first point:").into_owned()
|
||||
} else {
|
||||
let n = self.pts.len();
|
||||
t!("SPLINE Specify next point [%{n} pts]:", n = n).into_owned()
|
||||
@@ -189,7 +161,7 @@ impl CadCommand for SplineCommand {
|
||||
ctrl.push(pt);
|
||||
Some(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
sample_curve(&ctrl),
|
||||
sample_curve(&ctrl, false),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
|
||||
@@ -47,6 +47,185 @@ pub fn spline_to_nurbs(spl: &Spline) -> Option<NurbsCurve> {
|
||||
spline_to_nurbs_with(spl, |p| [p.x, p.y], |v| [v.x, v.y])
|
||||
}
|
||||
|
||||
/// Return the compact control-polygon form of a spline for interactive grips.
|
||||
///
|
||||
/// The kernel's fit-point interpolator intentionally stores one Bezier span
|
||||
/// per fit-point interval. That is exact and convenient for evaluation, but
|
||||
/// its repeated internal knots expose three controls per interval to an
|
||||
/// editor. An open cubic interpolation has an equivalent simple-knot form
|
||||
/// with only `fit point count + 2` controls. Solve that form here so the grip
|
||||
/// polygon stays compact without changing the curve or its saved fit points.
|
||||
pub fn spline_control_curve(spl: &Spline) -> Option<NurbsCurve> {
|
||||
let exact = spline_to_nurbs(spl)?;
|
||||
if !spl.control_points.is_empty()
|
||||
|| spl.flags.closed
|
||||
|| spl.flags.periodic
|
||||
|| spl.fit_points.len() < 2
|
||||
{
|
||||
return Some(exact);
|
||||
}
|
||||
|
||||
let fit: Vec<[f64; 2]> = spl.fit_points.iter().map(|p| [p.x, p.y]).collect();
|
||||
let parameters = fit_parameters(&fit, spl.knot_parameterization);
|
||||
let control_count = fit.len() + 2;
|
||||
let mut knots = vec![parameters[0]; 4];
|
||||
knots.extend(parameters.iter().take(fit.len() - 1).skip(1).copied());
|
||||
knots.extend([parameters[fit.len() - 1]; 4]);
|
||||
|
||||
let mut matrix = Vec::with_capacity(control_count);
|
||||
let mut right = Vec::with_capacity(control_count);
|
||||
for (¶meter, &point) in parameters.iter().zip(&fit) {
|
||||
matrix.push(bspline_basis(&knots, 3, parameter));
|
||||
right.push(point);
|
||||
}
|
||||
|
||||
// Scaling the derivative equations by the domain width keeps the solve
|
||||
// well conditioned for drawings whose coordinates are very large/small.
|
||||
let (start, end) = exact.domain();
|
||||
let width = (end - start).abs().max(1e-9);
|
||||
matrix.push(
|
||||
bspline_basis_derivative(&knots, 3, parameters[0])
|
||||
.into_iter()
|
||||
.map(|value| value * width)
|
||||
.collect(),
|
||||
);
|
||||
let start_slope = exact.derivative_at_knot(start);
|
||||
right.push([start_slope[0] * width, start_slope[1] * width]);
|
||||
matrix.push(
|
||||
bspline_basis_derivative(&knots, 3, parameters[fit.len() - 1])
|
||||
.into_iter()
|
||||
.map(|value| value * width)
|
||||
.collect(),
|
||||
);
|
||||
let end_slope = exact.derivative_at_knot(end);
|
||||
right.push([end_slope[0] * width, end_slope[1] * width]);
|
||||
|
||||
let controls = solve_control_points(matrix, right)?;
|
||||
NurbsCurve::new(3, controls, knots, None)
|
||||
}
|
||||
|
||||
fn fit_parameters(points: &[[f64; 2]], parameterization: i32) -> Vec<f64> {
|
||||
let mut parameters = vec![0.0; points.len()];
|
||||
for i in 1..points.len() {
|
||||
let dx = points[i][0] - points[i - 1][0];
|
||||
let dy = points[i][1] - points[i - 1][1];
|
||||
let chord = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let step = match parameterization {
|
||||
2 => 1.0,
|
||||
1 => chord.sqrt(),
|
||||
_ => chord,
|
||||
};
|
||||
parameters[i] = parameters[i - 1] + step;
|
||||
}
|
||||
parameters
|
||||
}
|
||||
|
||||
fn bspline_basis(knots: &[f64], degree: usize, parameter: f64) -> Vec<f64> {
|
||||
let count = knots.len() - degree - 1;
|
||||
if (parameter - knots[knots.len() - 1]).abs() < 1e-12 {
|
||||
let mut endpoint = vec![0.0; count];
|
||||
let last_span = knots
|
||||
.windows(2)
|
||||
.rposition(|span| span[0] < span[1])
|
||||
.unwrap_or(count - 1);
|
||||
endpoint[last_span.min(count - 1)] = 1.0;
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
let mut values: Vec<f64> = knots
|
||||
.windows(2)
|
||||
.map(|span| {
|
||||
if span[0] <= parameter && parameter < span[1] {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for order in 1..=degree {
|
||||
let next_count = knots.len() - order - 1;
|
||||
let mut next = vec![0.0; next_count];
|
||||
for i in 0..next_count {
|
||||
let left_span = knots[i + order] - knots[i];
|
||||
let right_span = knots[i + order + 1] - knots[i + 1];
|
||||
let left = if left_span.abs() < 1e-15 {
|
||||
0.0
|
||||
} else {
|
||||
(parameter - knots[i]) / left_span * values[i]
|
||||
};
|
||||
let right = if right_span.abs() < 1e-15 {
|
||||
0.0
|
||||
} else {
|
||||
(knots[i + order + 1] - parameter) / right_span * values[i + 1]
|
||||
};
|
||||
next[i] = left + right;
|
||||
}
|
||||
values = next;
|
||||
}
|
||||
values
|
||||
}
|
||||
|
||||
fn bspline_basis_derivative(knots: &[f64], degree: usize, parameter: f64) -> Vec<f64> {
|
||||
let lower = bspline_basis(knots, degree - 1, parameter);
|
||||
let count = knots.len() - degree - 1;
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let left_span = knots[i + degree] - knots[i];
|
||||
let right_span = knots[i + degree + 1] - knots[i + 1];
|
||||
let left = if left_span.abs() < 1e-15 {
|
||||
0.0
|
||||
} else {
|
||||
degree as f64 / left_span * lower[i]
|
||||
};
|
||||
let right = if right_span.abs() < 1e-15 {
|
||||
0.0
|
||||
} else {
|
||||
degree as f64 / right_span * lower[i + 1]
|
||||
};
|
||||
left - right
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn solve_control_points(
|
||||
mut matrix: Vec<Vec<f64>>,
|
||||
mut right: Vec<[f64; 2]>,
|
||||
) -> Option<Vec<[f64; 2]>> {
|
||||
let count = right.len();
|
||||
for column in 0..count {
|
||||
let pivot = (column..count).max_by(|&a, &b| {
|
||||
matrix[a][column]
|
||||
.abs()
|
||||
.total_cmp(&matrix[b][column].abs())
|
||||
})?;
|
||||
if matrix[pivot][column].abs() < 1e-14 {
|
||||
return None;
|
||||
}
|
||||
matrix.swap(column, pivot);
|
||||
right.swap(column, pivot);
|
||||
|
||||
for row in column + 1..count {
|
||||
let factor = matrix[row][column] / matrix[column][column];
|
||||
for col in column..count {
|
||||
matrix[row][col] -= factor * matrix[column][col];
|
||||
}
|
||||
right[row][0] -= factor * right[column][0];
|
||||
right[row][1] -= factor * right[column][1];
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = vec![[0.0; 2]; count];
|
||||
for row in (0..count).rev() {
|
||||
for axis in 0..2 {
|
||||
let known: f64 = (row + 1..count)
|
||||
.map(|column| matrix[row][column] * result[column][axis])
|
||||
.sum();
|
||||
result[row][axis] = (right[row][axis] - known) / matrix[row][row];
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// [`spline_to_nurbs`] with the points expressed in `plane`'s coordinates.
|
||||
///
|
||||
/// The two differ only for a spline whose extrusion normal is not +Z. Where
|
||||
|
||||
Reference in new issue
Block a user