Align spline properties and editing
This commit is contained in:
parent
0d37d3ff78
commit
5595837a69
8 changed files with 552 additions and 131 deletions
|
|
@ -2353,6 +2353,7 @@ properties =
|
|||
.chord-x = Chord X
|
||||
.chord-y = Chord Y
|
||||
.chord-z = Chord Z
|
||||
.chord = Chord
|
||||
.circumference = Circumference
|
||||
.class = Class
|
||||
.class-id = Class ID
|
||||
|
|
@ -2379,6 +2380,10 @@ properties =
|
|||
.control-point-x = Control Point X
|
||||
.control-point-y = Control Point Y
|
||||
.control-point-z = Control Point Z
|
||||
.control-point-x-sentence-case = Control point X
|
||||
.control-point-y-sentence-case = Control point Y
|
||||
.control-point-z-sentence-case = Control point Z
|
||||
.control-vertices = Control Vertices
|
||||
.coordination-model = Coordination Model
|
||||
.creased-edges = Creased Edges
|
||||
.croppings = Croppings
|
||||
|
|
@ -2386,6 +2391,9 @@ properties =
|
|||
.current-color-scheme = Current Color Scheme
|
||||
.current-vertex-title-case = Current Vertex
|
||||
.current-vertex-sentence-case = Current vertex
|
||||
.current-control-point = Current Control point
|
||||
.current-fit-point = Current Fit point
|
||||
.cv-frame = CV frame
|
||||
.cutting-line = Cutting Line
|
||||
.dwg-payload = DWG Payload
|
||||
.dwg-version = DWG Version
|
||||
|
|
@ -2445,6 +2453,9 @@ properties =
|
|||
.end-tangent-x = End Tangent X
|
||||
.end-tangent-y = End Tangent Y
|
||||
.end-tangent-z = End Tangent Z
|
||||
.end-tangent-vector-x = End tangent vector X
|
||||
.end-tangent-vector-y = End tangent vector Y
|
||||
.end-tangent-vector-z = End tangent vector Z
|
||||
.end-x = End X
|
||||
.end-y = End Y
|
||||
.end-z = End Z
|
||||
|
|
@ -2482,6 +2493,10 @@ properties =
|
|||
.fit-point-y = Fit Point Y
|
||||
.fit-point-z = Fit Point Z
|
||||
.fit-tolerance = Fit Tolerance
|
||||
.fit-point-x-sentence-case = Fit point X
|
||||
.fit-point-y-sentence-case = Fit point Y
|
||||
.fit-point-z-sentence-case = Fit point Z
|
||||
.fit-tolerance-sentence-case = Fit tolerance
|
||||
.fit-smooth-title-case = Fit/Smooth
|
||||
.fit-smooth-sentence-case = Fit/smooth
|
||||
.fixed-elevation-range = Fixed Elevation Range
|
||||
|
|
@ -2527,6 +2542,7 @@ properties =
|
|||
.jog-z = Jog Z
|
||||
.justify = Justify
|
||||
.knot-parameterization = Knot Parameterization
|
||||
.knot-parameterization-sentence-case = Knot parameterization
|
||||
.lamp-preset = Lamp Preset
|
||||
.landing-gap = Landing Gap
|
||||
.layout-print-configuration = Layout Print Configuration
|
||||
|
|
@ -2619,6 +2635,8 @@ properties =
|
|||
.number-of-grips = Number of Grips
|
||||
.number-of-vertices = Number of Vertices
|
||||
.number-of-turns = Number of turns
|
||||
.number-of-control-points = Number of control points
|
||||
.number-of-fit-points = Number of fit points
|
||||
.ole-frame = OLE Frame
|
||||
.object-references = Object References
|
||||
.obliquing = Obliquing
|
||||
|
|
@ -2738,6 +2756,13 @@ properties =
|
|||
.start-tangent-x = Start Tangent X
|
||||
.start-tangent-y = Start Tangent Y
|
||||
.start-tangent-z = Start Tangent Z
|
||||
.start-tangent-vector-x = Start tangent vector X
|
||||
.start-tangent-vector-y = Start tangent vector Y
|
||||
.start-tangent-vector-z = Start tangent vector Z
|
||||
.hide = Hide
|
||||
.periodic = Periodic
|
||||
.square-root = Square Root
|
||||
.uniform = Uniform
|
||||
.start-x = Start X
|
||||
.start-y = Start Y
|
||||
.start-z = Start Z
|
||||
|
|
|
|||
|
|
@ -2293,6 +2293,7 @@ properties =
|
|||
.chord-x = Kiriş X
|
||||
.chord-y = Kiriş Y
|
||||
.chord-z = Kiriş Z
|
||||
.chord = Kiriş
|
||||
.circumference = Çevre
|
||||
.class = Sınıf
|
||||
.class-id = Sınıf Kimliği
|
||||
|
|
@ -2319,6 +2320,10 @@ properties =
|
|||
.control-point-x = Kontrol Noktası X
|
||||
.control-point-y = Kontrol Noktası Y
|
||||
.control-point-z = Kontrol Noktası Z
|
||||
.control-point-x-sentence-case = Kontrol noktası X
|
||||
.control-point-y-sentence-case = Kontrol noktası Y
|
||||
.control-point-z-sentence-case = Kontrol noktası Z
|
||||
.control-vertices = Kontrol Köşeleri
|
||||
.coordination-model = Koordinasyon Modeli
|
||||
.creased-edges = Keskin Kenarlar
|
||||
.croppings = Kırpmalar
|
||||
|
|
@ -2326,6 +2331,9 @@ properties =
|
|||
.current-color-scheme = Geçerli Renk Düzeni
|
||||
.current-vertex-title-case = Geçerli Köşe
|
||||
.current-vertex-sentence-case = Geçerli köşe
|
||||
.current-control-point = Geçerli kontrol noktası
|
||||
.current-fit-point = Geçerli uydurma noktası
|
||||
.cv-frame = Kontrol çerçevesi
|
||||
.cutting-line = Kesme Çizgisi
|
||||
.dwg-payload = DWG Verisi
|
||||
.dwg-version = DWG Sürümü
|
||||
|
|
@ -2385,6 +2393,9 @@ properties =
|
|||
.end-tangent-x = Bitiş Teğeti X
|
||||
.end-tangent-y = Bitiş Teğeti Y
|
||||
.end-tangent-z = Bitiş Teğeti Z
|
||||
.end-tangent-vector-x = Bitiş teğet vektörü X
|
||||
.end-tangent-vector-y = Bitiş teğet vektörü Y
|
||||
.end-tangent-vector-z = Bitiş teğet vektörü Z
|
||||
.end-x = Bitiş X
|
||||
.end-y = Bitiş Y
|
||||
.end-z = Bitiş Z
|
||||
|
|
@ -2422,6 +2433,10 @@ properties =
|
|||
.fit-point-y = Uydurma Noktası Y
|
||||
.fit-point-z = Uydurma Noktası Z
|
||||
.fit-tolerance = Uydurma Toleransı
|
||||
.fit-point-x-sentence-case = Uydurma noktası X
|
||||
.fit-point-y-sentence-case = Uydurma noktası Y
|
||||
.fit-point-z-sentence-case = Uydurma noktası Z
|
||||
.fit-tolerance-sentence-case = Uydurma toleransı
|
||||
.fit-smooth-title-case = Uydur/Yumuşat
|
||||
.fit-smooth-sentence-case = Uydur/yumuşat
|
||||
.fixed-elevation-range = Sabit Kot Aralığı
|
||||
|
|
@ -2467,6 +2482,7 @@ properties =
|
|||
.jog-z = Kırılma Z
|
||||
.justify = Hizala
|
||||
.knot-parameterization = Düğüm Parametrelemesi
|
||||
.knot-parameterization-sentence-case = Düğüm parametrelemesi
|
||||
.lamp-preset = Lamba Ön Ayarı
|
||||
.landing-gap = Yatay Bağlantı Boşluğu
|
||||
.layout-print-configuration = Yerleşim Yazdırma Yapılandırması
|
||||
|
|
@ -2559,6 +2575,8 @@ properties =
|
|||
.number-of-grips = Tutamaç Sayısı
|
||||
.number-of-vertices = Köşe Sayısı
|
||||
.number-of-turns = Sarım sayısı
|
||||
.number-of-control-points = Kontrol noktası sayısı
|
||||
.number-of-fit-points = Uydurma noktası sayısı
|
||||
.ole-frame = OLE Çerçevesi
|
||||
.object-references = Nesne Referansları
|
||||
.obliquing = Eğiklik
|
||||
|
|
@ -2678,6 +2696,13 @@ properties =
|
|||
.start-tangent-x = Başlangıç Teğeti X
|
||||
.start-tangent-y = Başlangıç Teğeti Y
|
||||
.start-tangent-z = Başlangıç Teğeti Z
|
||||
.start-tangent-vector-x = Başlangıç teğet vektörü X
|
||||
.start-tangent-vector-y = Başlangıç teğet vektörü Y
|
||||
.start-tangent-vector-z = Başlangıç teğet vektörü Z
|
||||
.hide = Gizle
|
||||
.periodic = Periyodik
|
||||
.square-root = Karekök
|
||||
.uniform = Eşit Aralıklı
|
||||
.start-x = Başlangıç X
|
||||
.start-y = Başlangıç Y
|
||||
.start-z = Başlangıç Z
|
||||
|
|
|
|||
|
|
@ -121,6 +121,11 @@ impl OpenCADStudio {
|
|||
acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()),
|
||||
acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()),
|
||||
acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()),
|
||||
acadrust::EntityType::Spline(spline) => Some(if spline.fit_points.is_empty() {
|
||||
spline.control_points.len()
|
||||
} else {
|
||||
spline.fit_points.len()
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
vertex_count.map_or(prop_vertex, |count| prop_vertex.min(count.saturating_sub(1)))
|
||||
|
|
|
|||
|
|
@ -2099,6 +2099,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
if field == "spline_method" {
|
||||
self.tabs[i].properties.prop_vertex = 0;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = false;
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
Task::none()
|
||||
|
|
@ -2121,6 +2125,35 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
} else {
|
||||
crate::app::expr_eval::eval_to_string(&raw_val)
|
||||
};
|
||||
if matches!(field, "current_fit_point" | "current_control_point") {
|
||||
let count = handles
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
let entity = self.tabs[i].scene.document.get_entity(*handle)?;
|
||||
match (field, entity) {
|
||||
(
|
||||
"current_fit_point",
|
||||
acadrust::EntityType::Spline(spline),
|
||||
) => Some(spline.fit_points.len()),
|
||||
(
|
||||
"current_control_point",
|
||||
acadrust::EntityType::Spline(spline),
|
||||
) => Some(spline.control_points.len()),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.min()
|
||||
.unwrap_or(0);
|
||||
if let Ok(requested) = val.trim().parse::<usize>() {
|
||||
if count > 0 {
|
||||
let next = requested.clamp(1, count) - 1;
|
||||
self.tabs[i].properties.prop_vertex = next;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = true;
|
||||
}
|
||||
}
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "CHPROP");
|
||||
if field == "block" {
|
||||
// Name row on a block reference: an existing name
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::entities::common::{
|
|||
};
|
||||
use crate::entities::traits::RenderConvertible;
|
||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
|
||||
fn to_render(spl: &Spline) -> RenderEntity {
|
||||
let n = spl.control_points.len();
|
||||
|
|
@ -410,6 +410,158 @@ fn control_vertices(spline: &Spline) -> Vec<acadrust::types::Vector3> {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn choice_prop(
|
||||
label: &str,
|
||||
field: &'static str,
|
||||
selected: &str,
|
||||
options: &[&str],
|
||||
) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::Choice {
|
||||
selected: selected.to_string(),
|
||||
options: options.iter().map(|option| option.to_string()).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn index_prop(label: &str, field: &'static str, index: usize, count: usize) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::EditText(if count == 0 {
|
||||
"0".to_string()
|
||||
} else {
|
||||
(index + 1).to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_to_control_method(spline: &mut Spline) -> bool {
|
||||
if !spline.control_points.is_empty() && spline.fit_points.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let elevation = spline.fit_points.first().map(|point| point.z).unwrap_or(0.0);
|
||||
let Some(curve) = crate::modules::draw::modify::spline_ops::spline_control_curve(spline) else {
|
||||
return false;
|
||||
};
|
||||
spline.degree = curve.degree() as i32;
|
||||
spline.knots = curve.knots().to_vec();
|
||||
spline.control_points = curve
|
||||
.control_points()
|
||||
.iter()
|
||||
.map(|point| acadrust::types::Vector3::new(point[0], point[1], elevation))
|
||||
.collect();
|
||||
spline.weights = if curve.is_rational() {
|
||||
curve.weights().to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
spline.flags.rational = curve.is_rational();
|
||||
spline.fit_points.clear();
|
||||
spline.begin_tangent = acadrust::types::Vector3::ZERO;
|
||||
spline.end_tangent = acadrust::types::Vector3::ZERO;
|
||||
true
|
||||
}
|
||||
|
||||
fn convert_to_fit_method(spline: &mut Spline) -> bool {
|
||||
if !spline.fit_points.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let degree = spline.degree.max(1) as usize;
|
||||
let controls: Vec<_> = spline
|
||||
.control_points
|
||||
.iter()
|
||||
.map(|point| [point.x, point.y, point.z])
|
||||
.collect();
|
||||
let weights = (spline.weights.len() == controls.len()).then(|| spline.weights.clone());
|
||||
let Some(curve) = NurbsCurve3::new(degree, controls, spline.knots.clone(), weights) else {
|
||||
return false;
|
||||
};
|
||||
let (start, end) = curve.domain();
|
||||
let mut parameters: Vec<f64> = curve.knots()[degree..=curve.control_points().len()]
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|parameter| *parameter >= start && *parameter <= end)
|
||||
.collect();
|
||||
parameters.dedup_by(|left, right| (*left - *right).abs() < 1e-12);
|
||||
let sample_count = if degree == 3 {
|
||||
curve.control_points().len().saturating_sub(2).max(2)
|
||||
} else {
|
||||
curve.control_points().len().max(4)
|
||||
};
|
||||
for step in 0..sample_count {
|
||||
let fraction = step as f64 / (sample_count - 1) as f64;
|
||||
parameters.push(start + (end - start) * fraction);
|
||||
}
|
||||
parameters.sort_by(f64::total_cmp);
|
||||
parameters.dedup_by(|left, right| (*left - *right).abs() < 1e-12);
|
||||
|
||||
spline.fit_points = parameters
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
let point = curve.point_at_knot(*parameter);
|
||||
acadrust::types::Vector3::new(point[0], point[1], point[2])
|
||||
})
|
||||
.collect();
|
||||
let begin = curve.tangent_at_knot(start);
|
||||
let end_tangent = curve.tangent_at_knot(end);
|
||||
spline.begin_tangent = acadrust::types::Vector3::new(begin[0], begin[1], begin[2]);
|
||||
spline.end_tangent =
|
||||
acadrust::types::Vector3::new(end_tangent[0], end_tangent[1], end_tangent[2]);
|
||||
spline.degree = 3;
|
||||
spline.knots.clear();
|
||||
spline.control_points.clear();
|
||||
spline.weights.clear();
|
||||
spline.flags.rational = false;
|
||||
spline.knot_parameterization = 0;
|
||||
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 grips(spline: &Spline) -> Vec<GripDef> {
|
||||
let (source, show_control_vertices) = if spline.cv_frame_visible {
|
||||
(control_vertices(spline), true)
|
||||
|
|
@ -430,166 +582,267 @@ fn grips(spline: &Spline) -> Vec<GripDef> {
|
|||
}
|
||||
})
|
||||
.collect();
|
||||
if !spline.fit_points.is_empty() {
|
||||
if let Some(first) = source.first() {
|
||||
grips.push(dropdown_grip(
|
||||
SPLINE_MODE_GRIP_ID,
|
||||
glam::DVec3::new(first.x, first.y, first.z),
|
||||
));
|
||||
}
|
||||
if let Some(first) = source.first() {
|
||||
grips.push(dropdown_grip(
|
||||
SPLINE_MODE_GRIP_ID,
|
||||
glam::DVec3::new(first.x, first.y, first.z),
|
||||
));
|
||||
}
|
||||
grips
|
||||
}
|
||||
|
||||
fn properties(spline: &Spline) -> Vec<PropSection> {
|
||||
let show = if spline.cv_frame_visible || spline.fit_points.is_empty() {
|
||||
"Control Vertices"
|
||||
} else {
|
||||
"Fit"
|
||||
};
|
||||
let cp0 = spline.control_points.first();
|
||||
let fp0 = spline.fit_points.first();
|
||||
let w0 = spline.weights.first().copied();
|
||||
|
||||
// Polyline-approximation length over the effective defining points.
|
||||
let pts = if spline.fit_points.is_empty() {
|
||||
&spline.control_points
|
||||
} else {
|
||||
&spline.fit_points
|
||||
};
|
||||
let mut length = 0.0;
|
||||
for w in pts.windows(2) {
|
||||
let dx = w[1].x - w[0].x;
|
||||
let dy = w[1].y - w[0].y;
|
||||
let dz = w[1].z - w[0].z;
|
||||
length += (dx * dx + dy * dy + dz * dz).sqrt();
|
||||
}
|
||||
|
||||
let fit_method = !spline.fit_points.is_empty();
|
||||
let method = if fit_method { "Fit" } else { "Control Vertices" };
|
||||
let closed = spline.flags.closed || spline.flags.periodic;
|
||||
let yes_no = |b: bool| if b { "Yes" } else { "No" };
|
||||
|
||||
// Knot parameterization method (R2013+ DWG); older splines report 0.
|
||||
let knot_param = match spline.knot_parameterization {
|
||||
0 => "Chord",
|
||||
1 => "Square Root",
|
||||
2 => "Uniform",
|
||||
15 => "Custom",
|
||||
_ => "Custom",
|
||||
};
|
||||
|
||||
// Closed splines enclose an area; approximate it with the shoelace
|
||||
// formula over the defining points projected to the XY plane, matching
|
||||
// the polyline approximation already used for Length.
|
||||
let area = if closed && pts.len() >= 3 {
|
||||
let mut acc = 0.0;
|
||||
for w in pts.windows(2) {
|
||||
acc += w[0].x * w[1].y - w[1].x * w[0].y;
|
||||
}
|
||||
if let (Some(first), Some(last)) = (pts.first(), pts.last()) {
|
||||
acc += last.x * first.y - first.x * last.y;
|
||||
}
|
||||
format!("{:.4}", acc.abs() * 0.5)
|
||||
let current = crate::scene::view::dispatch::prop_current_vertex();
|
||||
let mut data_points = if fit_method {
|
||||
let count = spline.fit_points.len();
|
||||
let index = current.min(count.saturating_sub(1));
|
||||
let point = spline.fit_points.get(index);
|
||||
vec![
|
||||
ro(
|
||||
t!("Number of fit points").as_ref(),
|
||||
"fit_pt_count",
|
||||
count.to_string(),
|
||||
),
|
||||
index_prop(
|
||||
t!("Current Fit point").as_ref(),
|
||||
"current_fit_point",
|
||||
index,
|
||||
count,
|
||||
),
|
||||
edit(
|
||||
t!("Fit point X").as_ref(),
|
||||
"fit_pt_x",
|
||||
point.map(|value| value.x).unwrap_or(0.0),
|
||||
),
|
||||
edit(
|
||||
t!("Fit point Y").as_ref(),
|
||||
"fit_pt_y",
|
||||
point.map(|value| value.y).unwrap_or(0.0),
|
||||
),
|
||||
edit(
|
||||
t!("Fit point Z").as_ref(),
|
||||
"fit_pt_z",
|
||||
point.map(|value| value.z).unwrap_or(0.0),
|
||||
),
|
||||
choice_prop(
|
||||
t!("Knot parameterization").as_ref(),
|
||||
"knot_param",
|
||||
knot_param,
|
||||
&["Chord", "Square Root", "Uniform", "Custom"],
|
||||
),
|
||||
]
|
||||
} else {
|
||||
String::new()
|
||||
let count = spline.control_points.len();
|
||||
let index = current.min(count.saturating_sub(1));
|
||||
let point = spline.control_points.get(index);
|
||||
let weight = spline.weights.get(index).copied().unwrap_or(1.0);
|
||||
vec![
|
||||
ro(
|
||||
t!("Number of control points").as_ref(),
|
||||
"ctrl_pt_count",
|
||||
count.to_string(),
|
||||
),
|
||||
index_prop(
|
||||
t!("Current Control point").as_ref(),
|
||||
"current_control_point",
|
||||
index,
|
||||
count,
|
||||
),
|
||||
edit(
|
||||
t!("Control point X").as_ref(),
|
||||
"ctrl_pt_x",
|
||||
point.map(|value| value.x).unwrap_or(0.0),
|
||||
),
|
||||
edit(
|
||||
t!("Control point Y").as_ref(),
|
||||
"ctrl_pt_y",
|
||||
point.map(|value| value.y).unwrap_or(0.0),
|
||||
),
|
||||
edit(
|
||||
t!("Control point Z").as_ref(),
|
||||
"ctrl_pt_z",
|
||||
point.map(|value| value.z).unwrap_or(0.0),
|
||||
),
|
||||
edit(t!("Weight").as_ref(), "weight", weight),
|
||||
]
|
||||
};
|
||||
data_points.push(choice_prop(
|
||||
t!("CV frame").as_ref(),
|
||||
"cv_frame",
|
||||
if spline.cv_frame_visible { "Show" } else { "Hide" },
|
||||
&["Hide", "Show"],
|
||||
));
|
||||
|
||||
let mut misc = vec![
|
||||
choice_prop(
|
||||
t!("Method").as_ref(),
|
||||
"spline_method",
|
||||
method,
|
||||
&["Fit", "Control Vertices"],
|
||||
),
|
||||
ro(t!("Degree").as_ref(), "degree", spline.degree.to_string()),
|
||||
choice_prop(
|
||||
t!("Closed").as_ref(),
|
||||
"closed",
|
||||
yes_no(closed),
|
||||
&["No", "Yes"],
|
||||
),
|
||||
ro(
|
||||
t!("Periodic").as_ref(),
|
||||
"periodic",
|
||||
yes_no(spline.flags.periodic),
|
||||
),
|
||||
ro(
|
||||
t!("Planar").as_ref(),
|
||||
"planar",
|
||||
yes_no(is_planar(spline)),
|
||||
),
|
||||
];
|
||||
if fit_method {
|
||||
misc.extend([
|
||||
edit(
|
||||
t!("Start tangent vector X").as_ref(),
|
||||
"start_tan_x",
|
||||
spline.begin_tangent.x,
|
||||
),
|
||||
edit(
|
||||
t!("Start tangent vector Y").as_ref(),
|
||||
"start_tan_y",
|
||||
spline.begin_tangent.y,
|
||||
),
|
||||
edit(
|
||||
t!("Start tangent vector Z").as_ref(),
|
||||
"start_tan_z",
|
||||
spline.begin_tangent.z,
|
||||
),
|
||||
edit(
|
||||
t!("End tangent vector X").as_ref(),
|
||||
"end_tan_x",
|
||||
spline.end_tangent.x,
|
||||
),
|
||||
edit(
|
||||
t!("End tangent vector Y").as_ref(),
|
||||
"end_tan_y",
|
||||
spline.end_tangent.y,
|
||||
),
|
||||
edit(
|
||||
t!("End tangent vector Z").as_ref(),
|
||||
"end_tan_z",
|
||||
spline.end_tangent.z,
|
||||
),
|
||||
edit(
|
||||
t!("Fit tolerance").as_ref(),
|
||||
"fit_tolerance",
|
||||
spline.fit_tolerance,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Data Points").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Show").as_ref(), "show", show),
|
||||
ro(t!("Degree").as_ref(), "degree", spline.degree.to_string()),
|
||||
ro(
|
||||
t!("Control Point Count").as_ref(),
|
||||
"ctrl_pt_count",
|
||||
spline.control_points.len().to_string(),
|
||||
),
|
||||
ro(t!("Control Point").as_ref(), "ctrl_pt_index", "0"),
|
||||
edit(
|
||||
t!("Control Point X").as_ref(),
|
||||
"ctrl_pt_x",
|
||||
cp0.map(|p| p.x).unwrap_or(0.0),
|
||||
),
|
||||
edit(
|
||||
t!("Control Point Y").as_ref(),
|
||||
"ctrl_pt_y",
|
||||
cp0.map(|p| p.y).unwrap_or(0.0),
|
||||
),
|
||||
edit(
|
||||
t!("Control Point Z").as_ref(),
|
||||
"ctrl_pt_z",
|
||||
cp0.map(|p| p.z).unwrap_or(0.0),
|
||||
),
|
||||
edit(t!("Weight").as_ref(), "weight", w0.unwrap_or(1.0)),
|
||||
ro(t!("Knot Parameterization").as_ref(), "knot_param", knot_param),
|
||||
ro(
|
||||
t!("Fit Point Count").as_ref(),
|
||||
"fit_pt_count",
|
||||
spline.fit_points.len().to_string(),
|
||||
),
|
||||
ro(t!("Fit Point").as_ref(), "fit_pt_index", "0"),
|
||||
edit(t!("Fit Point X").as_ref(), "fit_pt_x", fp0.map(|p| p.x).unwrap_or(0.0)),
|
||||
edit(t!("Fit Point Y").as_ref(), "fit_pt_y", fp0.map(|p| p.y).unwrap_or(0.0)),
|
||||
edit(t!("Fit Point Z").as_ref(), "fit_pt_z", fp0.map(|p| p.z).unwrap_or(0.0)),
|
||||
edit(t!("Fit Tolerance").as_ref(), "fit_tolerance", spline.fit_tolerance),
|
||||
edit(t!("Start Tangent X").as_ref(), "start_tan_x", spline.begin_tangent.x),
|
||||
edit(t!("Start Tangent Y").as_ref(), "start_tan_y", spline.begin_tangent.y),
|
||||
edit(t!("Start Tangent Z").as_ref(), "start_tan_z", spline.begin_tangent.z),
|
||||
edit(t!("End Tangent X").as_ref(), "end_tan_x", spline.end_tangent.x),
|
||||
edit(t!("End Tangent Y").as_ref(), "end_tan_y", spline.end_tangent.y),
|
||||
edit(t!("End Tangent Z").as_ref(), "end_tan_z", spline.end_tangent.z),
|
||||
],
|
||||
props: data_points,
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Misc").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Closed").as_ref(), "closed", yes_no(closed)),
|
||||
ro(t!("Planar").as_ref(), "planar", yes_no(spline.flags.planar)),
|
||||
ro(t!("Length").as_ref(), "length", format!("{length:.4}")),
|
||||
ro(t!("Area").as_ref(), "area", area),
|
||||
],
|
||||
props: misc,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
||||
match field {
|
||||
"spline_method" => {
|
||||
if value == "Fit" {
|
||||
convert_to_fit_method(spline);
|
||||
} else if value == "Control Vertices" {
|
||||
convert_to_control_method(spline);
|
||||
}
|
||||
return;
|
||||
}
|
||||
"knot_param" => {
|
||||
spline.knot_parameterization = match value {
|
||||
"Chord" => 0,
|
||||
"Square Root" => 1,
|
||||
"Uniform" => 2,
|
||||
"Custom" => 15,
|
||||
_ => return,
|
||||
};
|
||||
return;
|
||||
}
|
||||
"cv_frame" => {
|
||||
spline.cv_frame_visible = value == "Show";
|
||||
return;
|
||||
}
|
||||
"closed" => {
|
||||
spline.flags.closed = value == "Yes";
|
||||
if !spline.flags.closed {
|
||||
spline.flags.periodic = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some(v) = parse_f64(value) else { return };
|
||||
let control_index = crate::scene::view::dispatch::prop_current_vertex()
|
||||
.min(spline.control_points.len().saturating_sub(1));
|
||||
let fit_index = crate::scene::view::dispatch::prop_current_vertex()
|
||||
.min(spline.fit_points.len().saturating_sub(1));
|
||||
match field {
|
||||
"ctrl_pt_x" => {
|
||||
if let Some(cp) = spline.control_points.first_mut() {
|
||||
if let Some(cp) = spline.control_points.get_mut(control_index) {
|
||||
cp.x = v;
|
||||
}
|
||||
}
|
||||
"ctrl_pt_y" => {
|
||||
if let Some(cp) = spline.control_points.first_mut() {
|
||||
if let Some(cp) = spline.control_points.get_mut(control_index) {
|
||||
cp.y = v;
|
||||
}
|
||||
}
|
||||
"ctrl_pt_z" => {
|
||||
if let Some(cp) = spline.control_points.first_mut() {
|
||||
if let Some(cp) = spline.control_points.get_mut(control_index) {
|
||||
cp.z = v;
|
||||
}
|
||||
}
|
||||
"weight" => {
|
||||
if let Some(w) = spline.weights.first_mut() {
|
||||
*w = v;
|
||||
if v > 0.0 && !spline.control_points.is_empty() {
|
||||
if spline.weights.len() != spline.control_points.len() {
|
||||
spline.weights = vec![1.0; spline.control_points.len()];
|
||||
}
|
||||
spline.weights[control_index] = v;
|
||||
spline.flags.rational = spline
|
||||
.weights
|
||||
.iter()
|
||||
.any(|weight| (*weight - spline.weights[0]).abs() > 1e-12);
|
||||
}
|
||||
}
|
||||
"fit_pt_x" => {
|
||||
if let Some(fp) = spline.fit_points.first_mut() {
|
||||
if let Some(fp) = spline.fit_points.get_mut(fit_index) {
|
||||
fp.x = v;
|
||||
}
|
||||
}
|
||||
"fit_pt_y" => {
|
||||
if let Some(fp) = spline.fit_points.first_mut() {
|
||||
if let Some(fp) = spline.fit_points.get_mut(fit_index) {
|
||||
fp.y = v;
|
||||
}
|
||||
}
|
||||
"fit_pt_z" => {
|
||||
if let Some(fp) = spline.fit_points.first_mut() {
|
||||
if let Some(fp) = spline.fit_points.get_mut(fit_index) {
|
||||
fp.z = v;
|
||||
}
|
||||
}
|
||||
"fit_tolerance" => spline.fit_tolerance = v,
|
||||
"fit_tolerance" if v >= 0.0 => spline.fit_tolerance = v,
|
||||
"start_tan_x" => spline.begin_tangent.x = v,
|
||||
"start_tan_y" => spline.begin_tangent.y = v,
|
||||
"start_tan_z" => spline.begin_tangent.z = v,
|
||||
|
|
@ -598,6 +851,7 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
|||
"end_tan_z" => spline.end_tangent.z = v,
|
||||
_ => {}
|
||||
}
|
||||
spline.flags.planar = is_planar(spline);
|
||||
}
|
||||
|
||||
fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
||||
|
|
@ -609,22 +863,9 @@ fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
|||
// control-point storage so the dragged vertex remains authoritative when
|
||||
// the drawing is saved.
|
||||
if spline.cv_frame_visible && spline.control_points.is_empty() {
|
||||
let elevation = spline.fit_points.first().map(|p| p.z).unwrap_or(0.0);
|
||||
let Some(curve) = crate::modules::draw::modify::spline_ops::spline_control_curve(spline)
|
||||
else {
|
||||
if !convert_to_control_method(spline) {
|
||||
return;
|
||||
};
|
||||
spline.degree = curve.degree() as i32;
|
||||
spline.knots = curve.knots().to_vec();
|
||||
spline.control_points = curve
|
||||
.control_points()
|
||||
.iter()
|
||||
.map(|p| acadrust::types::Vector3::new(p[0], p[1], elevation))
|
||||
.collect();
|
||||
spline.weights = curve.is_rational().then(|| curve.weights().to_vec()).unwrap_or_default();
|
||||
spline.fit_points.clear();
|
||||
spline.begin_tangent = acadrust::types::Vector3::ZERO;
|
||||
spline.end_tangent = acadrust::types::Vector3::ZERO;
|
||||
}
|
||||
}
|
||||
let target = if spline.cv_frame_visible || spline.fit_points.is_empty() {
|
||||
spline.control_points.get_mut(grip_id)
|
||||
|
|
@ -674,7 +915,7 @@ impl crate::entities::traits::Grippable for Spline {
|
|||
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||
if grip_id == SPLINE_MODE_GRIP_ID {
|
||||
return if self.cv_frame_visible {
|
||||
return if self.cv_frame_visible || self.fit_points.is_empty() {
|
||||
vec![
|
||||
GripMenuItem {
|
||||
label: "Fit",
|
||||
|
|
@ -724,9 +965,11 @@ impl crate::entities::traits::Grippable for Spline {
|
|||
) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
match action {
|
||||
A::ShowFit if !self.fit_points.is_empty() => {
|
||||
self.cv_frame_visible = false;
|
||||
return;
|
||||
A::ShowFit => {
|
||||
if !self.fit_points.is_empty() || convert_to_fit_method(self) {
|
||||
self.cv_frame_visible = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
A::ShowControlVertices if control_vertices(self).len() >= 2 => {
|
||||
self.cv_frame_visible = true;
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Character Spacing" => Some(("properties", "character-spacing")),
|
||||
"Check your internet or proxy connection, then retry. Manually added repositories remain available." => Some(("plugins", "check-your-internet-or-proxy-connection-then-r")),
|
||||
"Choose New to capture the current layer settings." => Some(("layers", "choose-new-to-capture-the-current-layer-settin")),
|
||||
"Chord" => Some(("properties", "chord")),
|
||||
"Chord X" => Some(("properties", "chord-x")),
|
||||
"Chord Y" => Some(("properties", "chord-y")),
|
||||
"Chord Z" => Some(("properties", "chord-z")),
|
||||
|
|
@ -645,6 +646,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Control Point X" => Some(("properties", "control-point-x")),
|
||||
"Control Point Y" => Some(("properties", "control-point-y")),
|
||||
"Control Point Z" => Some(("properties", "control-point-z")),
|
||||
"Control Vertices" => Some(("properties", "control-vertices")),
|
||||
"Control point X" => Some(("properties", "control-point-x-sentence-case")),
|
||||
"Control point Y" => Some(("properties", "control-point-y-sentence-case")),
|
||||
"Control point Z" => Some(("properties", "control-point-z-sentence-case")),
|
||||
"Coordinates" => Some(("common", "coordinates")),
|
||||
"Coordination Model" => Some(("properties", "coordination-model")),
|
||||
"Copied path: {}" => Some(("common", "copied-path")),
|
||||
|
|
@ -666,6 +671,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Current Color Scheme" => Some(("properties", "current-color-scheme")),
|
||||
"Current MLeader style: {}" => Some(("layers", "current-mleader-style")),
|
||||
"Current Vertex" => Some(("properties", "current-vertex-title-case")),
|
||||
"Current Control point" => Some(("properties", "current-control-point")),
|
||||
"Current Fit point" => Some(("properties", "current-fit-point")),
|
||||
"CV frame" => Some(("properties", "cv-frame")),
|
||||
"Current annotation scale: {name}" => Some(("view", "current-annotation-scale-name")),
|
||||
"Current dim style set to '{}'." => Some(("common", "current-dim-style-set-to")),
|
||||
"Current layer" => Some(("layers", "current-layer")),
|
||||
|
|
@ -961,6 +969,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"End Tangent X" => Some(("properties", "end-tangent-x")),
|
||||
"End Tangent Y" => Some(("properties", "end-tangent-y")),
|
||||
"End Tangent Z" => Some(("properties", "end-tangent-z")),
|
||||
"End tangent vector X" => Some(("properties", "end-tangent-vector-x")),
|
||||
"End tangent vector Y" => Some(("properties", "end-tangent-vector-y")),
|
||||
"End tangent vector Z" => Some(("properties", "end-tangent-vector-z")),
|
||||
"End X" => Some(("properties", "end-x")),
|
||||
"End Y" => Some(("properties", "end-y")),
|
||||
"End Z" => Some(("properties", "end-z")),
|
||||
|
|
@ -1066,6 +1077,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Fit Point Y" => Some(("properties", "fit-point-y")),
|
||||
"Fit Point Z" => Some(("properties", "fit-point-z")),
|
||||
"Fit Tolerance" => Some(("properties", "fit-tolerance")),
|
||||
"Fit point X" => Some(("properties", "fit-point-x-sentence-case")),
|
||||
"Fit point Y" => Some(("properties", "fit-point-y-sentence-case")),
|
||||
"Fit point Z" => Some(("properties", "fit-point-z-sentence-case")),
|
||||
"Fit tolerance" => Some(("properties", "fit-tolerance-sentence-case")),
|
||||
"Fit to paper" => Some(("plot", "fit-to-paper")),
|
||||
"Fit/Smooth" => Some(("properties", "fit-smooth-title-case")),
|
||||
"Fit/smooth" => Some(("properties", "fit-smooth-sentence-case")),
|
||||
|
|
@ -1258,6 +1273,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Kilometers" => Some(("common", "kilometers")),
|
||||
"Kind" => Some(("model", "kind")),
|
||||
"Knot Parameterization" => Some(("properties", "knot-parameterization")),
|
||||
"Knot parameterization" => Some(("properties", "knot-parameterization-sentence-case")),
|
||||
"LANDXMLIMPORT path to the .xml file:" => Some(("view", "landxmlimport-path-to-the-xml-file")),
|
||||
"LANDXMLIMPORT: cannot read \"{path}\": {e}" => Some(("view", "landxmlimport-cannot-read-path-e")),
|
||||
"LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view." => Some(("view", "landxmlimport-imported-survey-point-s-use-zoom")),
|
||||
|
|
@ -1701,6 +1717,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Number of Grips" => Some(("properties", "number-of-grips")),
|
||||
"Number of Vertices" => Some(("properties", "number-of-vertices")),
|
||||
"Number of turns" => Some(("properties", "number-of-turns")),
|
||||
"Number of control points" => Some(("properties", "number-of-control-points")),
|
||||
"Number of fit points" => Some(("properties", "number-of-fit-points")),
|
||||
"OBJECTSCALE: added the active scale to {n} object(s)." => Some(("view", "objectscale-added-the-active-scale-to-n-object")),
|
||||
"OBJECTSCALE: select objects first." => Some(("view", "objectscale-select-objects-first")),
|
||||
"OBJECTSCALE: the active annotation scale is unavailable." => Some(("view", "objectscale-the-active-annotation-scale-is-una")),
|
||||
|
|
@ -1905,6 +1923,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Pinned" => Some(("common", "pinned")),
|
||||
"Pitch And Family" => Some(("properties", "pitch-and-family")),
|
||||
"Place text manually (DIMUPT)" => Some(("common", "place-text-manually-dimupt")),
|
||||
"Periodic" => Some(("properties", "periodic")),
|
||||
"Planar" => Some(("properties", "planar")),
|
||||
"Platform" => Some(("ui", "platform")),
|
||||
"Plot" => Some(("common", "plot")),
|
||||
|
|
@ -2324,6 +2343,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Shortcut '{key}' not found." => Some(("view", "shortcut-key-not-found")),
|
||||
"Shortcut '{key}' removed." => Some(("view", "shortcut-key-removed")),
|
||||
"Shortcut set: {key} → {cmd_str}" => Some(("view", "shortcut-set-key-cmd-str")),
|
||||
"Hide" => Some(("properties", "hide")),
|
||||
"Show" => Some(("properties", "show")),
|
||||
"Show Annotation Objects" => Some(("ui", "show-annotation-objects")),
|
||||
"Show Clipping" => Some(("properties", "show-clipping")),
|
||||
|
|
@ -2369,6 +2389,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Start Tangent X" => Some(("properties", "start-tangent-x")),
|
||||
"Start Tangent Y" => Some(("properties", "start-tangent-y")),
|
||||
"Start Tangent Z" => Some(("properties", "start-tangent-z")),
|
||||
"Start tangent vector X" => Some(("properties", "start-tangent-vector-x")),
|
||||
"Start tangent vector Y" => Some(("properties", "start-tangent-vector-y")),
|
||||
"Start tangent vector Z" => Some(("properties", "start-tangent-vector-z")),
|
||||
"Square Root" => Some(("properties", "square-root")),
|
||||
"Start X" => Some(("properties", "start-x")),
|
||||
"Start Y" => Some(("properties", "start-y")),
|
||||
"Start Z" => Some(("properties", "start-z")),
|
||||
|
|
@ -2769,6 +2793,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||
"Usage: {prefix} NEW <name>" => Some(("layers", "usage-prefix-new-name")),
|
||||
"Usage: {prefix} OBLIQUE <style> <angle_degrees>" => Some(("layers", "usage-prefix-oblique-style-angle-degrees")),
|
||||
"Usage: {prefix} WIDTH <style> <factor>" => Some(("layers", "usage-prefix-width-style-factor")),
|
||||
"Uniform" => Some(("properties", "uniform")),
|
||||
"Use Limits" => Some(("properties", "use-limits")),
|
||||
"Used for the first save of a new drawing. Existing drawings keep their file type and version." => Some(("file", "used-for-the-first-save-of-a-new-drawing-exist")),
|
||||
"User\nInterface" => Some(("manage", "user-interface")),
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ impl SplineCommand {
|
|||
// Closed splines render with a segment bridging the last point back to
|
||||
// the first (tessellation honours `flags.closed`).
|
||||
spline.flags.closed = closed;
|
||||
spline.flags.planar = true;
|
||||
Some(EntityType::Spline(spline))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ pub fn spline_control_curve(spl: &Spline) -> Option<NurbsCurve> {
|
|||
if !spl.control_points.is_empty()
|
||||
|| spl.flags.closed
|
||||
|| spl.flags.periodic
|
||||
|| spl.fit_tolerance > 0.0
|
||||
|| spl.fit_points.len() < 2
|
||||
{
|
||||
return Some(exact);
|
||||
|
|
@ -275,18 +276,81 @@ fn spline_to_nurbs_with(
|
|||
let flat = vector(v);
|
||||
(flat[0] * flat[0] + flat[1] * flat[1] > 1e-18).then_some(flat)
|
||||
};
|
||||
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,
|
||||
start_tangent,
|
||||
end_tangent,
|
||||
parameterization,
|
||||
spl.fit_tolerance,
|
||||
);
|
||||
}
|
||||
NurbsCurve::interpolate(
|
||||
&fit,
|
||||
tangent(&spl.begin_tangent),
|
||||
tangent(&spl.end_tangent),
|
||||
match spl.knot_parameterization {
|
||||
2 => Parameterization::Uniform,
|
||||
1 => Parameterization::Centripetal,
|
||||
_ => Parameterization::Chord,
|
||||
},
|
||||
start_tangent,
|
||||
end_tangent,
|
||||
parameterization,
|
||||
)
|
||||
}
|
||||
|
||||
/// Reduce an exact fit-point set to the smallest progressively-refined subset
|
||||
/// whose interpolated curve stays within the requested distance of every
|
||||
/// original point. A zero tolerance bypasses this path and still interpolates
|
||||
/// every point exactly.
|
||||
fn fit_within_tolerance(
|
||||
points: Vec<[f64; 2]>,
|
||||
start_tangent: Option<[f64; 2]>,
|
||||
end_tangent: Option<[f64; 2]>,
|
||||
parameterization: Parameterization,
|
||||
tolerance: f64,
|
||||
) -> Vec<[f64; 2]> {
|
||||
if points.len() <= 2 || tolerance <= 0.0 {
|
||||
return points;
|
||||
}
|
||||
let mut selected = vec![0, points.len() - 1];
|
||||
loop {
|
||||
let subset: Vec<_> = selected.iter().map(|index| points[*index]).collect();
|
||||
let Some(curve) = NurbsCurve::interpolate(
|
||||
&subset,
|
||||
start_tangent,
|
||||
end_tangent,
|
||||
parameterization,
|
||||
) else {
|
||||
return points;
|
||||
};
|
||||
let mut worst = None;
|
||||
let mut worst_distance = tolerance;
|
||||
for (index, point) in points.iter().enumerate() {
|
||||
if selected.contains(&index) {
|
||||
continue;
|
||||
}
|
||||
let nearest = curve.point_at(curve.parameter_at(*point));
|
||||
let dx = nearest[0] - point[0];
|
||||
let dy = nearest[1] - point[1];
|
||||
let distance = (dx * dx + dy * dy).sqrt();
|
||||
if distance > worst_distance {
|
||||
worst = Some(index);
|
||||
worst_distance = distance;
|
||||
}
|
||||
}
|
||||
let Some(index) = worst else {
|
||||
return subset;
|
||||
};
|
||||
selected.push(index);
|
||||
selected.sort_unstable();
|
||||
if selected.len() == points.len() {
|
||||
return points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild an acadrust `Spline` from a kernel curve.
|
||||
///
|
||||
/// The `template` supplies entity common data and the Z elevation; splines
|
||||
|
|
|
|||
Loading…
Reference in a new issue