diff --git a/Cargo.lock b/Cargo.lock index 829cec05..44709c46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 41d69113..44b38889 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index a2f1a505..320aef19 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -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" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index b33a568b..a165a22c 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -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" diff --git a/src/entities/common.rs b/src/entities/common.rs index e46b8595..dcff0aeb 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -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) -> Property { Property { label: label.into(), @@ -563,8 +585,13 @@ pub fn stepper_prop( pub fn parse_f64(value: &str) -> Option { let t = value.trim(); - // Angle rows display via AUNITS (#297) — accept those formats back. - t.parse::().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::() + .ok() + .or_else(|| parse_length(t)) + .or_else(|| parse_angle_deg(t)) } /// Parse an angle string the panel displayed via AUNITS back to DEGREES: diff --git a/src/entities/curve.rs b/src/entities/curve.rs index 23116cc4..1aaa14c7 100644 --- a/src/entities/curve.rs +++ b/src/entities/curve.rs @@ -294,20 +294,101 @@ pub fn spline_curve(spline: &SplineEnt) -> Option { .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 = 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, diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 09a18225..44635765 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -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::() + .sqrt() + .max(1e-9); + match spl.knot_parameterization { + 2 => 1.0, + 1 => chord.sqrt(), + _ => chord, + } + }; + let spans: Vec = (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>, + mut right: Vec<[f64; 3]>, +) -> Option> { + 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::(); - (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::(); - (length2 > 1e-18).then_some(cross) - }) else { - return true; - }; - let normal_length = normal.iter().map(|value| value * value).sum::().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::() - .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 { "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 { 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 { diff --git a/src/modules/draw/modify/spline_ops.rs b/src/modules/draw/modify/spline_ops.rs index 12201a8b..628c3390 100644 --- a/src/modules/draw/modify/spline_ops.rs +++ b/src/modules/draw/modify/spline_ops.rs @@ -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 { + 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 = (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,