forked from 1b-agent/opencadstudio-upstream
Correct spline property value semantics
This commit is contained in:
1 parent
df71fa61bd
commit
1a6b61fb3a
8 files changed
+401
-70
No files matched your search
Generated
+1
-1
@@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.1"
|
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 = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+4
-1
@@ -27,7 +27,7 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
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"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ff950ce", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
@@ -56,6 +56,9 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
|
|||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] }
|
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]
|
[patch.crates-io]
|
||||||
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||||
iced_widget = { 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
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# 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).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
@@ -37,7 +37,7 @@ serde_json = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# 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]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
+29
-2
@@ -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 {
|
pub fn ro_prop(label: &str, field: &'static str, value: impl Into<String>) -> Property {
|
||||||
Property {
|
Property {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
@@ -563,8 +585,13 @@ pub fn stepper_prop(
|
|||||||
|
|
||||||
pub fn parse_f64(value: &str) -> Option<f64> {
|
pub fn parse_f64(value: &str) -> Option<f64> {
|
||||||
let t = value.trim();
|
let t = value.trim();
|
||||||
// Angle rows display via AUNITS (#297) — accept those formats back.
|
// Length rows display via LUNITS and angle rows via AUNITS. Accept both
|
||||||
t.parse::<f64>().ok().or_else(|| parse_angle_deg(t))
|
// 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:
|
/// Parse an angle string the panel displayed via AUNITS back to DEGREES:
|
||||||
|
|||||||
+82
-1
@@ -294,20 +294,101 @@ pub fn spline_curve(spline: &SplineEnt) -> Option<PlanarCurve> {
|
|||||||
.copied()
|
.copied()
|
||||||
.collect();
|
.collect();
|
||||||
let first = points.first()?;
|
let first = points.first()?;
|
||||||
|
if !spline_is_planar(spline) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let normal = normalized(spline.normal);
|
let normal = normalized(spline.normal);
|
||||||
let elevation = Vec3::from(xyz(*first)).dot(Vec3::from(xyz(normal)));
|
let elevation = Vec3::from(xyz(*first)).dot(Vec3::from(xyz(normal)));
|
||||||
let plane = ocs_plane(normal, elevation);
|
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)) {
|
if !points.iter().all(|p| plane.contains(xyz(*p), tolerance)) {
|
||||||
return None;
|
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(
|
Some(PlanarCurve::new(
|
||||||
plane,
|
plane,
|
||||||
Curve::Nurbs(spline_to_nurbs_on(spline, &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 entity's curve in world XY coordinates.
|
||||||
///
|
///
|
||||||
/// The editing commands — TRIM, EXTEND, FILLET, OFFSET — work in plan view,
|
/// The editing commands — TRIM, EXTEND, FILLET, OFFSET — work in plan view,
|
||||||
|
|||||||
+188
-56
@@ -5,7 +5,8 @@ use cadkernel::space::NurbsCurve3;
|
|||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
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::entities::traits::RenderConvertible;
|
||||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
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,
|
// A fit spline through points in space is not a planar curve,
|
||||||
// so the kernel has nothing to say about it and the solve
|
// so the kernel has nothing to say about it and the solve
|
||||||
// here remains the only description of its shape.
|
// 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)
|
catmull_rom_polyline(&spl.fit_points, true)
|
||||||
}
|
}
|
||||||
None => fit_spline_polyline(spl),
|
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 {
|
if spl.fit_points.len() < 2 {
|
||||||
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
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)
|
catmull_rom_polyline(&spl.fit_points, true)
|
||||||
} else {
|
} else {
|
||||||
fit_spline_polyline(spl)
|
fit_spline_polyline(spl)
|
||||||
@@ -311,6 +315,151 @@ fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
|||||||
out
|
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
|
/// Slopes used by the spatial fit-point interpolator. Keeping this solve
|
||||||
/// shared lets Properties report the same effective end tangents that the
|
/// shared lets Properties report the same effective end tangents that the
|
||||||
/// renderer uses when the stored tangent fields mean "automatic".
|
/// renderer uses when the stored tangent fields mean "automatic".
|
||||||
@@ -535,49 +684,6 @@ fn convert_to_fit_method(spline: &mut Spline) -> bool {
|
|||||||
true
|
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 {
|
fn tangent_is_set(tangent: &acadrust::types::Vector3) -> bool {
|
||||||
tangent.x * tangent.x + tangent.y * tangent.y + tangent.z * tangent.z > 1e-18
|
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",
|
"ctrl_pt_z",
|
||||||
point.map(|value| value.z).unwrap_or(0.0),
|
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(
|
data_points.push(choice_prop(
|
||||||
@@ -785,37 +891,37 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
|
|||||||
ro(
|
ro(
|
||||||
t!("Planar").as_ref(),
|
t!("Planar").as_ref(),
|
||||||
"planar",
|
"planar",
|
||||||
yes_no(is_planar(spline)),
|
yes_no(crate::entities::curve::spline_is_planar(spline)),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if fit_method {
|
if fit_method {
|
||||||
misc.extend([
|
misc.extend([
|
||||||
edit(
|
edit_scalar(
|
||||||
t!("Start tangent vector X").as_ref(),
|
t!("Start tangent vector X").as_ref(),
|
||||||
"start_tan_x",
|
"start_tan_x",
|
||||||
effective_begin_tangent.x,
|
effective_begin_tangent.x,
|
||||||
),
|
),
|
||||||
edit(
|
edit_scalar(
|
||||||
t!("Start tangent vector Y").as_ref(),
|
t!("Start tangent vector Y").as_ref(),
|
||||||
"start_tan_y",
|
"start_tan_y",
|
||||||
effective_begin_tangent.y,
|
effective_begin_tangent.y,
|
||||||
),
|
),
|
||||||
edit(
|
edit_scalar(
|
||||||
t!("Start tangent vector Z").as_ref(),
|
t!("Start tangent vector Z").as_ref(),
|
||||||
"start_tan_z",
|
"start_tan_z",
|
||||||
effective_begin_tangent.z,
|
effective_begin_tangent.z,
|
||||||
),
|
),
|
||||||
edit(
|
edit_scalar(
|
||||||
t!("End tangent vector X").as_ref(),
|
t!("End tangent vector X").as_ref(),
|
||||||
"end_tan_x",
|
"end_tan_x",
|
||||||
effective_end_tangent.x,
|
effective_end_tangent.x,
|
||||||
),
|
),
|
||||||
edit(
|
edit_scalar(
|
||||||
t!("End tangent vector Y").as_ref(),
|
t!("End tangent vector Y").as_ref(),
|
||||||
"end_tan_y",
|
"end_tan_y",
|
||||||
effective_end_tangent.y,
|
effective_end_tangent.y,
|
||||||
),
|
),
|
||||||
edit(
|
edit_scalar(
|
||||||
t!("End tangent vector Z").as_ref(),
|
t!("End tangent vector Z").as_ref(),
|
||||||
"end_tan_z",
|
"end_tan_z",
|
||||||
effective_end_tangent.z,
|
effective_end_tangent.z,
|
||||||
@@ -855,7 +961,17 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
|||||||
"Chord" => 0,
|
"Chord" => 0,
|
||||||
"Square Root" => 1,
|
"Square Root" => 1,
|
||||||
"Uniform" => 2,
|
"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,
|
||||||
};
|
};
|
||||||
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) {
|
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) {
|
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| {
|
crate::scene::view::transform::apply_standard_entity_transform(spline, t, |entity, p1, p2| {
|
||||||
for cp in &mut entity.control_points {
|
for cp in &mut entity.control_points {
|
||||||
crate::scene::view::transform::reflect_xy_point(&mut cp.x, &mut cp.y, p1, p2);
|
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);
|
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 {
|
impl RenderConvertible for Spline {
|
||||||
|
|||||||
@@ -227,6 +227,91 @@ fn solve_control_points(
|
|||||||
Some(result)
|
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.
|
/// [`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
|
/// 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.
|
// No usable control polygon, so this is a fit-point spline.
|
||||||
let mut fit: Vec<[f64; 2]> = spl.fit_points.iter().map(&point).collect();
|
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
|
// 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
|
// 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.
|
// 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 start_tangent = tangent(&spl.begin_tangent);
|
||||||
let end_tangent = tangent(&spl.end_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 {
|
if !spl.flags.closed && !spl.flags.periodic && spl.fit_tolerance > 0.0 {
|
||||||
fit = fit_within_tolerance(
|
fit = fit_within_tolerance(
|
||||||
fit,
|
fit,
|
||||||
|
|||||||
Reference in new issue
Block a user