Correct spline property value semantics
This commit is contained in:
parent
df71fa61bd
commit
1a6b61fb3a
8 changed files with 401 additions and 70 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.4.1"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=931c4ab#931c4ab0c590b755e280bed318a35f41c57b139f"
|
||||
source = "git+https://github.com/ramox81/cadcodec.git?rev=969940a#969940a0e616507b603f3480d2d2780a6d565961"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||
rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "931c4ab", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "969940a", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ff950ce", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
|
|
@ -56,6 +56,9 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
|
|||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] }
|
||||
|
||||
[patch."https://github.com/HakanSeven12/cadcodec.git"]
|
||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "969940a" }
|
||||
|
||||
[patch.crates-io]
|
||||
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
|||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "931c4ab", optional = true, features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "969940a", optional = true, features = ["serde"] }
|
||||
|
||||
# Runtime IPC and serialization (host feature only).
|
||||
interprocess = { version = "2", optional = true }
|
||||
|
|
@ -37,7 +37,7 @@ serde_json = "1"
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
cargo-lock = "11"
|
||||
# acadrust is scanned at build time to generate the embedded type registry.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "931c4ab", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "969940a", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ publish = false
|
|||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "931c4ab", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "969940a", features = ["serde"] }
|
||||
bincode = "1.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
|
|
|||
|
|
@ -524,6 +524,28 @@ pub fn edit_prop(label: &str, field: &'static str, value: f64) -> Property {
|
|||
}
|
||||
}
|
||||
|
||||
/// Editable dimensionless number. Unlike coordinates and distances, vector
|
||||
/// components and NURBS weights must stay plain decimal values when the
|
||||
/// drawing uses engineering, architectural, or fractional length units.
|
||||
pub fn edit_scalar_prop(label: &str, field: &'static str, value: f64) -> Property {
|
||||
let precision = unit_context().luprec.max(0) as usize;
|
||||
let formatted = format!("{:.*}", precision, value);
|
||||
let trimmed = if formatted.contains('.') {
|
||||
formatted.trim_end_matches('0').trim_end_matches('.')
|
||||
} else {
|
||||
formatted.as_str()
|
||||
};
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::EditText(if trimmed.is_empty() || trimmed == "-0" {
|
||||
"0".to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ro_prop(label: &str, field: &'static str, value: impl Into<String>) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
|
|
@ -563,8 +585,13 @@ pub fn stepper_prop(
|
|||
|
||||
pub fn parse_f64(value: &str) -> Option<f64> {
|
||||
let t = value.trim();
|
||||
// Angle rows display via AUNITS (#297) — accept those formats back.
|
||||
t.parse::<f64>().ok().or_else(|| parse_angle_deg(t))
|
||||
// Length rows display via LUNITS and angle rows via AUNITS. Accept both
|
||||
// representations back so a value shown by Properties can always be
|
||||
// committed unchanged.
|
||||
t.parse::<f64>()
|
||||
.ok()
|
||||
.or_else(|| parse_length(t))
|
||||
.or_else(|| parse_angle_deg(t))
|
||||
}
|
||||
|
||||
/// Parse an angle string the panel displayed via AUNITS back to DEGREES:
|
||||
|
|
|
|||
|
|
@ -294,20 +294,101 @@ pub fn spline_curve(spline: &SplineEnt) -> Option<PlanarCurve> {
|
|||
.copied()
|
||||
.collect();
|
||||
let first = points.first()?;
|
||||
if !spline_is_planar(spline) {
|
||||
return None;
|
||||
}
|
||||
let normal = normalized(spline.normal);
|
||||
let elevation = Vec3::from(xyz(*first)).dot(Vec3::from(xyz(normal)));
|
||||
let plane = ocs_plane(normal, elevation);
|
||||
|
||||
let tolerance = PLANARITY_TOLERANCE * scale_of(&points);
|
||||
let tolerance = spline_point_tolerance(&points);
|
||||
if !points.iter().all(|p| plane.contains(xyz(*p), tolerance)) {
|
||||
return None;
|
||||
}
|
||||
if !spline.fit_points.is_empty() {
|
||||
let plane_normal = Vec3::from(plane.normal()?);
|
||||
for tangent in [spline.begin_tangent, spline.end_tangent] {
|
||||
let tangent = Vec3::from(xyz(tangent));
|
||||
if tangent.length_squared() > 1e-18
|
||||
&& tangent.dot(plane_normal).abs()
|
||||
> PLANARITY_TOLERANCE * tangent.length().max(1.0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(PlanarCurve::new(
|
||||
plane,
|
||||
Curve::Nurbs(spline_to_nurbs_on(spline, &plane)?),
|
||||
))
|
||||
}
|
||||
|
||||
/// Whether the actual spline definition fits some plane. Fit-point tangents
|
||||
/// participate because coplanar points can still define a spatial curve when
|
||||
/// an endpoint derivative leaves their plane.
|
||||
pub fn spline_is_planar(spline: &SplineEnt) -> bool {
|
||||
let points = if spline.fit_points.is_empty() {
|
||||
&spline.control_points
|
||||
} else {
|
||||
&spline.fit_points
|
||||
};
|
||||
let Some(origin) = points.first().copied() else {
|
||||
return true;
|
||||
};
|
||||
let origin = Vec3::from(xyz(origin));
|
||||
let mut directions: Vec<Vec3> = points
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|point| Vec3::from(xyz(*point)) - origin)
|
||||
.collect();
|
||||
if !spline.fit_points.is_empty() {
|
||||
directions.extend(
|
||||
[spline.begin_tangent, spline.end_tangent]
|
||||
.into_iter()
|
||||
.map(|tangent| Vec3::from(xyz(tangent))),
|
||||
);
|
||||
}
|
||||
|
||||
let extent = directions
|
||||
.iter()
|
||||
.map(|direction| direction.length())
|
||||
.fold(1.0, f64::max);
|
||||
let tolerance = PLANARITY_TOLERANCE * extent
|
||||
+ f64::EPSILON * scale_of(points) * 64.0;
|
||||
let Some(axis) = directions
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|direction| direction.length() > tolerance)
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
let Some(normal) = directions.iter().find_map(|direction| {
|
||||
let cross = axis.cross(*direction);
|
||||
if cross.length() > tolerance * axis.length().max(1.0) {
|
||||
cross.normalize()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) else {
|
||||
return true;
|
||||
};
|
||||
directions
|
||||
.iter()
|
||||
.all(|direction| direction.dot(normal).abs() <= tolerance)
|
||||
}
|
||||
|
||||
fn spline_point_tolerance(points: &[Vector3]) -> f64 {
|
||||
let Some(origin) = points.first().copied() else {
|
||||
return PLANARITY_TOLERANCE;
|
||||
};
|
||||
let origin = Vec3::from(xyz(origin));
|
||||
let extent = points
|
||||
.iter()
|
||||
.map(|point| (Vec3::from(xyz(*point)) - origin).length())
|
||||
.fold(1.0, f64::max);
|
||||
PLANARITY_TOLERANCE * extent + f64::EPSILON * scale_of(points) * 64.0
|
||||
}
|
||||
|
||||
/// The entity's curve in world XY coordinates.
|
||||
///
|
||||
/// The editing commands — TRIM, EXTEND, FILLET, OFFSET — work in plan view,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ use cadkernel::space::NurbsCurve3;
|
|||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
dropdown_grip, edit_prop as edit, parse_f64, ro_prop as ro, round_grip, square_grip,
|
||||
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};
|
||||
|
|
@ -31,7 +32,8 @@ fn to_render(spl: &Spline) -> RenderEntity {
|
|||
// 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.closed || spl.flags.periodic => {
|
||||
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),
|
||||
|
|
@ -149,7 +151,9 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
|||
if spl.fit_points.len() < 2 {
|
||||
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
}
|
||||
return if spl.flags.closed || spl.flags.periodic {
|
||||
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)
|
||||
|
|
@ -311,6 +315,151 @@ fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
|||
out
|
||||
}
|
||||
|
||||
/// Spatial counterpart of the planar periodic interpolator. The cyclic
|
||||
/// derivative system gives the seam the same C² continuity as every interior
|
||||
/// fit point instead of only drawing a final chord back to the start.
|
||||
fn periodic_fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
||||
let mut points: Vec<[f64; 3]> = spl
|
||||
.fit_points
|
||||
.iter()
|
||||
.map(|point| [point.x, point.y, point.z])
|
||||
.collect();
|
||||
if points.len() > 1 {
|
||||
let first = points[0];
|
||||
let last = points[points.len() - 1];
|
||||
let distance2: f64 = (0..3).map(|axis| (last[axis] - first[axis]).powi(2)).sum();
|
||||
if distance2 <= 1e-18 {
|
||||
points.pop();
|
||||
}
|
||||
}
|
||||
let count = points.len();
|
||||
if count < 3 {
|
||||
return catmull_rom_polyline(&spl.fit_points, true);
|
||||
}
|
||||
|
||||
let step = |from: [f64; 3], to: [f64; 3]| {
|
||||
let chord = (0..3)
|
||||
.map(|axis| (to[axis] - from[axis]).powi(2))
|
||||
.sum::<f64>()
|
||||
.sqrt()
|
||||
.max(1e-9);
|
||||
match spl.knot_parameterization {
|
||||
2 => 1.0,
|
||||
1 => chord.sqrt(),
|
||||
_ => chord,
|
||||
}
|
||||
};
|
||||
let spans: Vec<f64> = (0..count)
|
||||
.map(|index| step(points[index], points[(index + 1) % count]))
|
||||
.collect();
|
||||
let mut matrix = vec![vec![0.0; count]; count];
|
||||
let mut right = vec![[0.0; 3]; count];
|
||||
for index in 0..count {
|
||||
let previous = (index + count - 1) % count;
|
||||
let next = (index + 1) % count;
|
||||
let before = spans[previous];
|
||||
let after = spans[index];
|
||||
matrix[index][previous] += after;
|
||||
matrix[index][index] += 2.0 * (before + after);
|
||||
matrix[index][next] += before;
|
||||
for axis in 0..3 {
|
||||
let previous_slope = (points[index][axis] - points[previous][axis]) / before;
|
||||
let next_slope = (points[next][axis] - points[index][axis]) / after;
|
||||
right[index][axis] =
|
||||
3.0 * (after * previous_slope + before * next_slope);
|
||||
}
|
||||
}
|
||||
let Some(slopes) = solve_spatial_system(matrix, right) else {
|
||||
return catmull_rom_polyline(&spl.fit_points, true);
|
||||
};
|
||||
|
||||
let mut out = Vec::new();
|
||||
for index in 0..count {
|
||||
let next = (index + 1) % count;
|
||||
let span = spans[index];
|
||||
let point_at = |u: f64| {
|
||||
let (u2, u3) = (u * u, u * u * u);
|
||||
let basis = [
|
||||
2.0 * u3 - 3.0 * u2 + 1.0,
|
||||
u3 - 2.0 * u2 + u,
|
||||
-2.0 * u3 + 3.0 * u2,
|
||||
u3 - u2,
|
||||
];
|
||||
let mut point = [0.0; 3];
|
||||
for axis in 0..3 {
|
||||
point[axis] = basis[0] * points[index][axis]
|
||||
+ basis[1] * slopes[index][axis] * span
|
||||
+ basis[2] * points[next][axis]
|
||||
+ basis[3] * slopes[next][axis] * span;
|
||||
}
|
||||
point
|
||||
};
|
||||
let tangent_at = |u: f64| {
|
||||
let u2 = u * u;
|
||||
let basis = [
|
||||
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 tangent = [0.0; 3];
|
||||
for axis in 0..3 {
|
||||
tangent[axis] = basis[0] * points[index][axis]
|
||||
+ basis[1] * slopes[index][axis] * span
|
||||
+ basis[2] * points[next][axis]
|
||||
+ basis[3] * slopes[next][axis] * span;
|
||||
}
|
||||
tangent
|
||||
};
|
||||
let sampled = cadkernel::tessellation::sample_curve3_angle(
|
||||
point_at,
|
||||
tangent_at,
|
||||
cadkernel::tessellation::DEFAULT_ANGLE,
|
||||
);
|
||||
out.extend(sampled.into_iter().skip(usize::from(index > 0)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn solve_spatial_system(
|
||||
mut matrix: Vec<Vec<f64>>,
|
||||
mut right: Vec<[f64; 3]>,
|
||||
) -> Option<Vec<[f64; 3]>> {
|
||||
let count = right.len();
|
||||
for column in 0..count {
|
||||
let pivot = (column..count).max_by(|&left, &right_index| {
|
||||
matrix[left][column]
|
||||
.abs()
|
||||
.total_cmp(&matrix[right_index][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 entry in column..count {
|
||||
matrix[row][entry] -= factor * matrix[column][entry];
|
||||
}
|
||||
for axis in 0..3 {
|
||||
right[row][axis] -= factor * right[column][axis];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = vec![[0.0; 3]; count];
|
||||
for row in (0..count).rev() {
|
||||
for axis in 0..3 {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Slopes used by the spatial fit-point interpolator. Keeping this solve
|
||||
/// shared lets Properties report the same effective end tangents that the
|
||||
/// renderer uses when the stored tangent fields mean "automatic".
|
||||
|
|
@ -535,49 +684,6 @@ fn convert_to_fit_method(spline: &mut Spline) -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
fn is_planar(spline: &Spline) -> bool {
|
||||
let points = if spline.fit_points.is_empty() {
|
||||
&spline.control_points
|
||||
} else {
|
||||
&spline.fit_points
|
||||
};
|
||||
if points.len() <= 3 {
|
||||
return true;
|
||||
}
|
||||
let origin = points[0];
|
||||
let Some(axis) = points.iter().skip(1).find_map(|point| {
|
||||
let vector = [point.x - origin.x, point.y - origin.y, point.z - origin.z];
|
||||
let length2 = vector.iter().map(|value| value * value).sum::<f64>();
|
||||
(length2 > 1e-18).then_some(vector)
|
||||
}) else {
|
||||
return true;
|
||||
};
|
||||
let Some(normal) = points.iter().skip(1).find_map(|point| {
|
||||
let vector = [point.x - origin.x, point.y - origin.y, point.z - origin.z];
|
||||
let cross = [
|
||||
axis[1] * vector[2] - axis[2] * vector[1],
|
||||
axis[2] * vector[0] - axis[0] * vector[2],
|
||||
axis[0] * vector[1] - axis[1] * vector[0],
|
||||
];
|
||||
let length2 = cross.iter().map(|value| value * value).sum::<f64>();
|
||||
(length2 > 1e-18).then_some(cross)
|
||||
}) else {
|
||||
return true;
|
||||
};
|
||||
let normal_length = normal.iter().map(|value| value * value).sum::<f64>().sqrt();
|
||||
points.iter().all(|point| {
|
||||
let offset = [point.x - origin.x, point.y - origin.y, point.z - origin.z];
|
||||
let distance = normal
|
||||
.iter()
|
||||
.zip(offset)
|
||||
.map(|(component, value)| component * value)
|
||||
.sum::<f64>()
|
||||
.abs()
|
||||
/ normal_length;
|
||||
distance <= 1e-8
|
||||
})
|
||||
}
|
||||
|
||||
fn tangent_is_set(tangent: &acadrust::types::Vector3) -> bool {
|
||||
tangent.x * tangent.x + tangent.y * tangent.y + tangent.z * tangent.z > 1e-18
|
||||
}
|
||||
|
|
@ -753,7 +859,7 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
|
|||
"ctrl_pt_z",
|
||||
point.map(|value| value.z).unwrap_or(0.0),
|
||||
),
|
||||
edit(t!("Weight").as_ref(), "weight", weight),
|
||||
edit_scalar(t!("Weight").as_ref(), "weight", weight),
|
||||
]
|
||||
};
|
||||
data_points.push(choice_prop(
|
||||
|
|
@ -785,37 +891,37 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
|
|||
ro(
|
||||
t!("Planar").as_ref(),
|
||||
"planar",
|
||||
yes_no(is_planar(spline)),
|
||||
yes_no(crate::entities::curve::spline_is_planar(spline)),
|
||||
),
|
||||
];
|
||||
if fit_method {
|
||||
misc.extend([
|
||||
edit(
|
||||
edit_scalar(
|
||||
t!("Start tangent vector X").as_ref(),
|
||||
"start_tan_x",
|
||||
effective_begin_tangent.x,
|
||||
),
|
||||
edit(
|
||||
edit_scalar(
|
||||
t!("Start tangent vector Y").as_ref(),
|
||||
"start_tan_y",
|
||||
effective_begin_tangent.y,
|
||||
),
|
||||
edit(
|
||||
edit_scalar(
|
||||
t!("Start tangent vector Z").as_ref(),
|
||||
"start_tan_z",
|
||||
effective_begin_tangent.z,
|
||||
),
|
||||
edit(
|
||||
edit_scalar(
|
||||
t!("End tangent vector X").as_ref(),
|
||||
"end_tan_x",
|
||||
effective_end_tangent.x,
|
||||
),
|
||||
edit(
|
||||
edit_scalar(
|
||||
t!("End tangent vector Y").as_ref(),
|
||||
"end_tan_y",
|
||||
effective_end_tangent.y,
|
||||
),
|
||||
edit(
|
||||
edit_scalar(
|
||||
t!("End tangent vector Z").as_ref(),
|
||||
"end_tan_z",
|
||||
effective_end_tangent.z,
|
||||
|
|
@ -855,7 +961,17 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
|||
"Chord" => 0,
|
||||
"Square Root" => 1,
|
||||
"Uniform" => 2,
|
||||
"Custom" => 15,
|
||||
"Custom" => {
|
||||
// Custom parameterization is an explicit knot/control
|
||||
// representation, not a fourth automatic spacing rule.
|
||||
// Materialize the current fit curve before marking it
|
||||
// custom so rendering and saving cannot silently fall
|
||||
// back to chord spacing.
|
||||
if !spline.fit_points.is_empty() && !convert_to_control_method(spline) {
|
||||
return;
|
||||
}
|
||||
15
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
return;
|
||||
|
|
@ -947,7 +1063,7 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
spline.flags.planar = is_planar(spline);
|
||||
spline.flags.planar = crate::entities::curve::spline_is_planar(spline);
|
||||
}
|
||||
|
||||
fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
||||
|
|
@ -985,6 +1101,21 @@ fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
@ -993,6 +1124,7 @@ fn apply_transform(spline: &mut Spline, t: &EntityTransform) {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -227,6 +227,91 @@ fn solve_control_points(
|
|||
Some(result)
|
||||
}
|
||||
|
||||
/// A closed C² cubic through every fit point. The cyclic slope solve makes
|
||||
/// the first and last derivatives and accelerations agree at the seam; merely
|
||||
/// repeating the first point in the open interpolator only closes the shape
|
||||
/// and does not make it periodic.
|
||||
fn interpolate_periodic(
|
||||
points: &[[f64; 2]],
|
||||
parameterization: Parameterization,
|
||||
) -> Option<NurbsCurve> {
|
||||
let mut points = points.to_vec();
|
||||
if points.len() > 1 {
|
||||
let first = points[0];
|
||||
let last = points[points.len() - 1];
|
||||
let dx = last[0] - first[0];
|
||||
let dy = last[1] - first[1];
|
||||
if dx * dx + dy * dy <= 1e-18 {
|
||||
points.pop();
|
||||
}
|
||||
}
|
||||
let count = points.len();
|
||||
if count < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let step = |from: [f64; 2], to: [f64; 2]| {
|
||||
let dx = to[0] - from[0];
|
||||
let dy = to[1] - from[1];
|
||||
let chord = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
match parameterization {
|
||||
Parameterization::Uniform => 1.0,
|
||||
Parameterization::Centripetal => chord.sqrt(),
|
||||
Parameterization::Chord => chord,
|
||||
}
|
||||
};
|
||||
let spans: Vec<f64> = (0..count)
|
||||
.map(|index| step(points[index], points[(index + 1) % count]))
|
||||
.collect();
|
||||
|
||||
let mut matrix = vec![vec![0.0; count]; count];
|
||||
let mut right = vec![[0.0; 2]; count];
|
||||
for index in 0..count {
|
||||
let previous = (index + count - 1) % count;
|
||||
let next = (index + 1) % count;
|
||||
let before = spans[previous];
|
||||
let after = spans[index];
|
||||
matrix[index][previous] += after;
|
||||
matrix[index][index] += 2.0 * (before + after);
|
||||
matrix[index][next] += before;
|
||||
for axis in 0..2 {
|
||||
let previous_slope = (points[index][axis] - points[previous][axis]) / before;
|
||||
let next_slope = (points[next][axis] - points[index][axis]) / after;
|
||||
right[index][axis] =
|
||||
3.0 * (after * previous_slope + before * next_slope);
|
||||
}
|
||||
}
|
||||
let slopes = solve_control_points(matrix, right)?;
|
||||
|
||||
let mut controls = Vec::with_capacity(3 * count + 1);
|
||||
let mut boundaries = Vec::with_capacity(count + 1);
|
||||
controls.push(points[0]);
|
||||
boundaries.push(0.0);
|
||||
let mut parameter = 0.0;
|
||||
for index in 0..count {
|
||||
let next = (index + 1) % count;
|
||||
let span = spans[index];
|
||||
controls.push([
|
||||
points[index][0] + slopes[index][0] * span / 3.0,
|
||||
points[index][1] + slopes[index][1] * span / 3.0,
|
||||
]);
|
||||
controls.push([
|
||||
points[next][0] - slopes[next][0] * span / 3.0,
|
||||
points[next][1] - slopes[next][1] * span / 3.0,
|
||||
]);
|
||||
controls.push(points[next]);
|
||||
parameter += span;
|
||||
boundaries.push(parameter);
|
||||
}
|
||||
|
||||
let mut knots = vec![0.0; 4];
|
||||
for boundary in boundaries.iter().take(count).skip(1) {
|
||||
knots.extend([*boundary; 3]);
|
||||
}
|
||||
knots.extend([parameter; 4]);
|
||||
NurbsCurve::new(3, controls, knots, None)
|
||||
}
|
||||
|
||||
/// [`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
|
||||
|
|
@ -259,7 +344,15 @@ fn spline_to_nurbs_with(
|
|||
|
||||
// No usable control polygon, so this is a fit-point spline.
|
||||
let mut fit: Vec<[f64; 2]> = spl.fit_points.iter().map(&point).collect();
|
||||
if spl.flags.closed || spl.flags.periodic {
|
||||
let parameterization = match spl.knot_parameterization {
|
||||
2 => Parameterization::Uniform,
|
||||
1 => Parameterization::Centripetal,
|
||||
_ => Parameterization::Chord,
|
||||
};
|
||||
if spl.flags.periodic {
|
||||
return interpolate_periodic(&fit, parameterization);
|
||||
}
|
||||
if spl.flags.closed {
|
||||
// The interpolation is a clamped solve and does not model a wrap, so
|
||||
// a closed spline came back as an open curve that never returned to
|
||||
// its start — and a TRIM against it then cut nothing along the seam.
|
||||
|
|
@ -278,11 +371,6 @@ fn spline_to_nurbs_with(
|
|||
};
|
||||
let start_tangent = tangent(&spl.begin_tangent);
|
||||
let end_tangent = tangent(&spl.end_tangent);
|
||||
let parameterization = match spl.knot_parameterization {
|
||||
2 => Parameterization::Uniform,
|
||||
1 => Parameterization::Centripetal,
|
||||
_ => Parameterization::Chord,
|
||||
};
|
||||
if !spl.flags.closed && !spl.flags.periodic && spl.fit_tolerance > 0.0 {
|
||||
fit = fit_within_tolerance(
|
||||
fit,
|
||||
|
|
|
|||
Loading…
Reference in a new issue