feat(spline): correct property semantics

Merge PR #824 while keeping periodic and coplanarity geometry in cadkernel.
This commit is contained in:
Hakan Seven 2026-08-20 12:13:35 +03:00
commit dcedf945ea
11 changed files with 208 additions and 73 deletions

4
Cargo.lock generated
View file

@ -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/HakanSeven12/cadcodec.git?rev=0975677#0975677029f2b472759db00e9692421a5831ad00"
dependencies = [ dependencies = [
"ahash 0.8.12", "ahash 0.8.12",
"anyhow", "anyhow",
@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]] [[package]]
name = "cadkernel" name = "cadkernel"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ff950ce#ff950ce4146c9fb94a5478e6704ada01264dbf9f" source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=2af87ad#2af87adf3e3234fd9e8df1bfe01faf7e2b6333d1"
dependencies = [ dependencies = [
"acadrust", "acadrust",
"cavalier_contours", "cavalier_contours",

View file

@ -27,8 +27,8 @@ 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/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ff950ce", features = ["acis", "offset"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "2af87ad", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1" flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }

View file

@ -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/HakanSeven12/cadcodec.git", rev = "0975677", 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/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
serde_json = "1" serde_json = "1"

View file

@ -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/HakanSeven12/cadcodec.git", rev = "0975677", 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"

View file

@ -121,13 +121,13 @@ impl OpenCADStudio {
acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()), acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()),
acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()), acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()),
acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()), acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()),
acadrust::EntityType::Spline(spline) => Some( acadrust::EntityType::Spline(spline) => {
if crate::entities::spline::uses_fit_method(spline) { Some(if crate::entities::spline::shows_fit_points(spline) {
spline.fit_points.len() spline.fit_points.len()
} else { } else {
spline.control_points.len() crate::entities::spline::control_vertex_count(spline)
}, })
), }
_ => None, _ => None,
}); });
vertex_count.map_or(prop_vertex, |count| prop_vertex.min(count.saturating_sub(1))) vertex_count.map_or(prop_vertex, |count| prop_vertex.min(count.saturating_sub(1)))

View file

@ -1758,7 +1758,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}; };
match field { match field {
"spline_method" => { "spline_method" => {
value == if crate::entities::spline::uses_fit_method(spline) { value == if crate::entities::spline::shows_fit_points(spline) {
"Fit" "Fit"
} else { } else {
"Control Vertices" "Control Vertices"
@ -2193,7 +2193,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
( (
"current_control_point", "current_control_point",
acadrust::EntityType::Spline(spline), acadrust::EntityType::Spline(spline),
) => Some(spline.control_points.len()), ) => Some(
crate::entities::spline::control_vertex_count(spline),
),
_ => None, _ => None,
} }
}) })

View file

@ -423,12 +423,8 @@ impl OpenCADStudio {
.and_then(|h| { .and_then(|h| {
let indexed = match tab.scene.document.get_entity(h) { let indexed = match tab.scene.document.get_entity(h) {
Some(acadrust::EntityType::LwPolyline(_)) Some(acadrust::EntityType::LwPolyline(_))
| Some(acadrust::EntityType::Polyline2D(_)) => true, | Some(acadrust::EntityType::Polyline2D(_))
Some(acadrust::EntityType::Spline(spline)) => { | Some(acadrust::EntityType::Spline(_)) => true,
!crate::entities::spline::uses_fit_method(spline)
|| !spline.cv_frame_visible
|| crate::entities::curve::spline_curve(spline).is_none()
}
_ => false, _ => false,
}; };
indexed.then_some(tab.properties.prop_vertex) indexed.then_some(tab.properties.prop_vertex)

View file

@ -535,6 +535,16 @@ pub fn edit_prop(label: &str, field: &'static str, value: f64) -> Property {
} }
} }
/// Editable dimensionless value independent of drawing units.
pub fn edit_scalar_prop(label: &str, field: &'static str, value: f64) -> Property {
let value = if value == 0.0 { 0.0 } else { value };
Property {
label: label.into(),
field,
value: PropValue::EditText(value.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(),
@ -574,8 +584,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:

View file

@ -32,13 +32,13 @@ use acadrust::entities::{
Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, LwPolyline as LwPolylineEnt, Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, LwPolyline as LwPolylineEnt,
Polyline2D, Spline as SplineEnt, Polyline2D, Spline as SplineEnt,
}; };
use acadrust::types::Vector3;
use acadrust::EntityType;
use cadkernel::geom2d::{ use cadkernel::geom2d::{
characteristic_points, Arc, Circle, Curve, Ellipse, EllipseArc, Line, Polyline, PolylineVertex, characteristic_points, Arc, Circle, Curve, Ellipse, EllipseArc, Line, Polyline, PolylineVertex,
Ray, SnapKind, Transform, XLine, Ray, SnapKind, Transform, XLine,
}; };
use cadkernel::space::{PlanarCurve, Plane, Vec3}; use cadkernel::space::{are_coplanar, coplanarity_tolerance, PlanarCurve, Plane, Vec3};
use acadrust::types::Vector3;
use acadrust::EntityType;
use crate::modules::draw::modify::spline_ops::spline_to_nurbs_on; use crate::modules::draw::modify::spline_ops::spline_to_nurbs_on;
use crate::scene::model::wire_model::SnapHint; use crate::scene::model::wire_model::SnapHint;
@ -287,27 +287,61 @@ pub fn polyline2d_curve(polyline: &Polyline2D) -> Option<PlanarCurve> {
/// plane the normal describes. A spline that genuinely wanders in space gets /// plane the normal describes. A spline that genuinely wanders in space gets
/// `None`, which is honest: flattening it to XY would move it. /// `None`, which is honest: flattening it to XY would move it.
pub fn spline_curve(spline: &SplineEnt) -> Option<PlanarCurve> { pub fn spline_curve(spline: &SplineEnt) -> Option<PlanarCurve> {
let source = if crate::entities::spline::uses_fit_method(spline) { let fit_method = crate::entities::spline::uses_fit_method(spline);
let source = if fit_method {
&spline.fit_points &spline.fit_points
} else { } else {
&spline.control_points &spline.control_points
}; };
let points: Vec<Vector3> = source.to_vec(); let points: Vec<Vector3> = source.to_vec();
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 point_arrays: Vec<[f64; 3]> = points.iter().copied().map(xyz).collect();
let tolerance = coplanarity_tolerance(&point_arrays);
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 fit_method && !spline.flags.periodic {
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 defining points and active tangents share a plane.
pub fn spline_is_planar(spline: &SplineEnt) -> bool {
let fit_method = crate::entities::spline::uses_fit_method(spline);
let points = if fit_method {
&spline.fit_points
} else {
&spline.control_points
};
let points: Vec<[f64; 3]> = points.iter().copied().map(xyz).collect();
let directions: Vec<[f64; 3]> = if fit_method && !spline.flags.periodic {
vec![xyz(spline.begin_tangent), xyz(spline.end_tangent)]
} else {
Vec::new()
};
are_coplanar(&points, &directions)
}
/// 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,

View file

@ -1,21 +1,26 @@
use acadrust::entities::Spline; use acadrust::entities::Spline;
use crate::t;
use cadkernel::geom2d::Curve as KernelCurve; use cadkernel::geom2d::Curve as KernelCurve;
use cadkernel::space::NurbsCurve3; use cadkernel::space::{NurbsCurve3, Parameterization};
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, format_length,
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};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::t;
pub(crate) fn uses_fit_method(spline: &Spline) -> bool { pub(crate) fn uses_fit_method(spline: &Spline) -> bool {
let degree = spline.degree.max(1) as usize; let degree = spline.degree.max(1) as usize;
!spline.fit_points.is_empty() && spline.control_points.len() <= degree !spline.fit_points.is_empty() && spline.control_points.len() <= degree
} }
pub(crate) fn shows_fit_points(spline: &Spline) -> bool {
uses_fit_method(spline) && (!spline.cv_frame_visible || spline.flags.periodic)
}
fn to_render(spl: &Spline) -> RenderEntity { fn to_render(spl: &Spline) -> RenderEntity {
let n = spl.control_points.len(); let n = spl.control_points.len();
if n < 2 { if n < 2 {
@ -36,7 +41,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),
@ -155,7 +161,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.fit_points.iter().map(|p| [p.x, p.y, p.z]).collect(); return spl.fit_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)
@ -322,6 +330,22 @@ fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
out 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. /// Slopes used by spatial fit interpolation.
fn fit_spline_slopes(spl: &Spline, p: &[[f64; 3]]) -> Option<(Vec<f64>, [Vec<f64>; 3])> { fn fit_spline_slopes(spl: &Spline, p: &[[f64; 3]]) -> Option<(Vec<f64>, [Vec<f64>; 3])> {
let n = p.len(); let n = p.len();
@ -439,6 +463,10 @@ fn control_vertices(spline: &Spline) -> Vec<acadrust::types::Vector3> {
.collect() .collect()
} }
pub(crate) fn control_vertex_count(spline: &Spline) -> usize {
control_vertices(spline).len()
}
fn choice_prop( fn choice_prop(
label: &str, label: &str,
field: &'static str, field: &'static str,
@ -504,10 +532,6 @@ fn convert_to_fit_method(spline: &mut Spline) -> bool {
uses_fit_method(spline) uses_fit_method(spline)
} }
fn is_planar(spline: &Spline) -> bool {
crate::entities::curve::spline_curve(spline).is_some()
}
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
} }
@ -557,6 +581,7 @@ fn grips(spline: &Spline) -> Vec<GripDef> {
let fit_method = uses_fit_method(spline); let fit_method = uses_fit_method(spline);
let derived_control = control_vertices(spline); let derived_control = control_vertices(spline);
let show_control_vertices = (spline.cv_frame_visible || !fit_method) let show_control_vertices = (spline.cv_frame_visible || !fit_method)
&& !(fit_method && spline.flags.periodic)
&& derived_control.len() >= 2; && derived_control.len() >= 2;
let source = if show_control_vertices { let source = if show_control_vertices {
derived_control derived_control
@ -588,9 +613,11 @@ fn grips(spline: &Spline) -> Vec<GripDef> {
fn properties(spline: &Spline) -> Vec<PropSection> { fn properties(spline: &Spline) -> Vec<PropSection> {
let fit_method = uses_fit_method(spline); let fit_method = uses_fit_method(spline);
let method = if fit_method { "Fit" } else { "Control Vertices" }; 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 closed = spline.flags.closed || spline.flags.periodic;
let can_show_control = control_vertices(spline).len() >= 2; 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 yes_no = |b: bool| if b { "Yes" } else { "No" };
let knot_param = match spline.knot_parameterization { let knot_param = match spline.knot_parameterization {
0 => "Chord", 0 => "Chord",
@ -601,7 +628,7 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
}; };
let current = crate::scene::view::dispatch::prop_current_vertex(); let current = crate::scene::view::dispatch::prop_current_vertex();
let (effective_begin_tangent, effective_end_tangent) = effective_fit_tangents(spline); let (effective_begin_tangent, effective_end_tangent) = effective_fit_tangents(spline);
let mut data_points = if fit_method { let mut data_points = if show_fit {
let count = spline.fit_points.len(); let count = spline.fit_points.len();
let index = current.min(count.saturating_sub(1)); let index = current.min(count.saturating_sub(1));
let point = spline.fit_points.get(index); let point = spline.fit_points.get(index);
@ -648,9 +675,10 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
}, },
] ]
} else { } else {
let count = spline.control_points.len(); let controls = control_vertices(spline);
let count = controls.len();
let index = current.min(count.saturating_sub(1)); let index = current.min(count.saturating_sub(1));
let point = spline.control_points.get(index); let point = controls.get(index);
let weight = spline.weights.get(index).copied().unwrap_or(1.0); let weight = spline.weights.get(index).copied().unwrap_or(1.0);
vec![ vec![
ro( ro(
@ -679,7 +707,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(if can_show_control { data_points.push(if can_show_control {
@ -723,48 +751,51 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
ro( ro(
t!("Planar").as_ref(), t!("Planar").as_ref(),
"planar", "planar",
crate::i18n::translate(yes_no(is_planar(spline))).into_owned(), crate::i18n::translate(yes_no(crate::entities::curve::spline_is_planar(spline)))
.into_owned(),
), ),
]; ];
if fit_method { if show_fit && !spline.flags.periodic {
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,
), ),
edit(
t!("Fit tolerance").as_ref(),
"fit_tolerance",
spline.fit_tolerance,
),
]); ]);
} }
if show_fit {
misc.push(ro(
t!("Fit tolerance").as_ref(),
"fit_tolerance",
format_length(spline.fit_tolerance),
));
}
vec![ vec![
PropSection { PropSection {
@ -781,8 +812,14 @@ fn properties(spline: &Spline) -> Vec<PropSection> {
fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
match field { match field {
"spline_method" => { "spline_method" => {
if value == "Control Vertices" { if value == "Fit" {
convert_to_control_method(spline); if convert_to_fit_method(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; return;
} }
@ -796,11 +833,40 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
return; return;
} }
"cv_frame" => { "cv_frame" => {
spline.cv_frame_visible = value == "Show" && control_vertices(spline).len() >= 2; spline.cv_frame_visible = value == "Show"
&& !(uses_fit_method(spline) && spline.flags.periodic)
&& control_vertices(spline).len() >= 2;
return; 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 field == "fit_tolerance" {
return;
}
if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight")
&& uses_fit_method(spline)
&& !convert_to_control_method(spline)
{
return;
}
let (effective_begin_tangent, effective_end_tangent) = effective_fit_tangents(spline); let (effective_begin_tangent, effective_end_tangent) = effective_fit_tangents(spline);
let Some(v) = parse_f64(value) else { return }; let Some(v) = parse_f64(value) else { return };
let control_index = crate::scene::view::dispatch::prop_current_vertex() let control_index = crate::scene::view::dispatch::prop_current_vertex()
@ -850,7 +916,6 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
fp.z = v; fp.z = v;
} }
} }
"fit_tolerance" if v >= 0.0 => spline.fit_tolerance = v,
"start_tan_x" | "start_tan_y" | "start_tan_z" => { "start_tan_x" | "start_tan_y" | "start_tan_z" => {
if !tangent_is_set(&spline.begin_tangent) { if !tangent_is_set(&spline.begin_tangent) {
spline.begin_tangent = effective_begin_tangent; spline.begin_tangent = effective_begin_tangent;
@ -875,7 +940,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) {
@ -884,12 +949,12 @@ fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
} }
// First CV edit makes the displayed kernel controls authoritative. // First CV edit makes the displayed kernel controls authoritative.
let fit_method = uses_fit_method(spline); let fit_method = uses_fit_method(spline);
if spline.cv_frame_visible && fit_method { if spline.cv_frame_visible && fit_method && !spline.flags.periodic {
if !convert_to_control_method(spline) { if !convert_to_control_method(spline) {
return; return;
} }
} }
let target = if spline.cv_frame_visible || !fit_method { let target = if (spline.cv_frame_visible && !spline.flags.periodic) || !fit_method {
spline.control_points.get_mut(grip_id) spline.control_points.get_mut(grip_id)
} else { } else {
spline.fit_points.get_mut(grip_id) spline.fit_points.get_mut(grip_id)
@ -911,6 +976,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);
@ -919,6 +999,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 {
@ -938,8 +1019,9 @@ impl crate::entities::traits::Grippable for Spline {
use crate::scene::model::object::{GripMenuAction, GripMenuItem}; use crate::scene::model::object::{GripMenuAction, GripMenuItem};
if grip_id == SPLINE_MODE_GRIP_ID { if grip_id == SPLINE_MODE_GRIP_ID {
let fit_method = uses_fit_method(self); let fit_method = uses_fit_method(self);
let can_show_control = control_vertices(self).len() >= 2; let can_show_control = control_vertices(self).len() >= 2
return if self.cv_frame_visible || !fit_method { && !(fit_method && self.flags.periodic);
return if !shows_fit_points(self) {
vec![ vec![
(can_show_control && fit_method).then_some(GripMenuItem { (can_show_control && fit_method).then_some(GripMenuItem {
label: "Fit", label: "Fit",
@ -987,7 +1069,10 @@ impl crate::entities::traits::Grippable for Spline {
return; return;
} }
} }
A::ShowControlVertices if control_vertices(self).len() >= 2 => { A::ShowControlVertices
if !(uses_fit_method(self) && self.flags.periodic)
&& control_vertices(self).len() >= 2 =>
{
self.cv_frame_visible = true; self.cv_frame_visible = true;
return; return;
} }

View file

@ -10,10 +10,10 @@
// fine until it is measured. // fine until it is measured.
use acadrust::entities::Spline; use acadrust::entities::Spline;
use cadkernel::geom2d::{NurbsCurve, Parameterization};
use cadkernel::space::Plane;
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::Handle; use acadrust::Handle;
use cadkernel::geom2d::{NurbsCurve, Parameterization};
use cadkernel::space::Plane;
/// The drawing plane a spline sits on. /// The drawing plane a spline sits on.
/// ///
@ -79,7 +79,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 NurbsCurve::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.
@ -98,11 +106,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,
};
NurbsCurve::interpolate( NurbsCurve::interpolate(
&fit, &fit,
start_tangent, start_tangent,