feat(spline): improve fit and CV editing
Keep planar conversions kernel-backed and leave unsupported 3D, CV-to-fit, and closed rebuilds unavailable instead of changing curve data.\n\nRefs #815
This commit is contained in:
commit
1c408e3a73
16 changed files with 924 additions and 350 deletions
|
|
@ -1357,6 +1357,7 @@ draw =
|
||||||
.solid-specify-second-point = SOLID Specify second point:
|
.solid-specify-second-point = SOLID Specify second point:
|
||||||
.solid-specify-third-point = SOLID Specify third point:
|
.solid-specify-third-point = SOLID Specify third point:
|
||||||
.spline-specify-first-control-point = SPLINE Specify first control point:
|
.spline-specify-first-control-point = SPLINE Specify first control point:
|
||||||
|
.spline-specify-first-point = SPLINE Specify first point:
|
||||||
.spline-specify-next-point-n-pts = SPLINE Specify next point [__ocs_arg_n__ pts]:
|
.spline-specify-next-point-n-pts = SPLINE Specify next point [__ocs_arg_n__ pts]:
|
||||||
.sketch = Sketch
|
.sketch = Sketch
|
||||||
.start-center-angle = Start, Center, Angle
|
.start-center-angle = Start, Center, Angle
|
||||||
|
|
@ -2352,6 +2353,7 @@ properties =
|
||||||
.chord-x = Chord X
|
.chord-x = Chord X
|
||||||
.chord-y = Chord Y
|
.chord-y = Chord Y
|
||||||
.chord-z = Chord Z
|
.chord-z = Chord Z
|
||||||
|
.chord = Chord
|
||||||
.circumference = Circumference
|
.circumference = Circumference
|
||||||
.class = Class
|
.class = Class
|
||||||
.class-id = Class ID
|
.class-id = Class ID
|
||||||
|
|
@ -2378,6 +2380,10 @@ properties =
|
||||||
.control-point-x = Control Point X
|
.control-point-x = Control Point X
|
||||||
.control-point-y = Control Point Y
|
.control-point-y = Control Point Y
|
||||||
.control-point-z = Control Point Z
|
.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
|
.coordination-model = Coordination Model
|
||||||
.creased-edges = Creased Edges
|
.creased-edges = Creased Edges
|
||||||
.croppings = Croppings
|
.croppings = Croppings
|
||||||
|
|
@ -2385,6 +2391,9 @@ properties =
|
||||||
.current-color-scheme = Current Color Scheme
|
.current-color-scheme = Current Color Scheme
|
||||||
.current-vertex-title-case = Current Vertex
|
.current-vertex-title-case = Current Vertex
|
||||||
.current-vertex-sentence-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
|
.cutting-line = Cutting Line
|
||||||
.dwg-payload = DWG Payload
|
.dwg-payload = DWG Payload
|
||||||
.dwg-version = DWG Version
|
.dwg-version = DWG Version
|
||||||
|
|
@ -2444,6 +2453,9 @@ properties =
|
||||||
.end-tangent-x = End Tangent X
|
.end-tangent-x = End Tangent X
|
||||||
.end-tangent-y = End Tangent Y
|
.end-tangent-y = End Tangent Y
|
||||||
.end-tangent-z = End Tangent Z
|
.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-x = End X
|
||||||
.end-y = End Y
|
.end-y = End Y
|
||||||
.end-z = End Z
|
.end-z = End Z
|
||||||
|
|
@ -2481,6 +2493,10 @@ properties =
|
||||||
.fit-point-y = Fit Point Y
|
.fit-point-y = Fit Point Y
|
||||||
.fit-point-z = Fit Point Z
|
.fit-point-z = Fit Point Z
|
||||||
.fit-tolerance = Fit Tolerance
|
.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-title-case = Fit/Smooth
|
||||||
.fit-smooth-sentence-case = Fit/smooth
|
.fit-smooth-sentence-case = Fit/smooth
|
||||||
.fixed-elevation-range = Fixed Elevation Range
|
.fixed-elevation-range = Fixed Elevation Range
|
||||||
|
|
@ -2526,6 +2542,7 @@ properties =
|
||||||
.jog-z = Jog Z
|
.jog-z = Jog Z
|
||||||
.justify = Justify
|
.justify = Justify
|
||||||
.knot-parameterization = Knot Parameterization
|
.knot-parameterization = Knot Parameterization
|
||||||
|
.knot-parameterization-sentence-case = Knot parameterization
|
||||||
.lamp-preset = Lamp Preset
|
.lamp-preset = Lamp Preset
|
||||||
.landing-gap = Landing Gap
|
.landing-gap = Landing Gap
|
||||||
.layout-print-configuration = Layout Print Configuration
|
.layout-print-configuration = Layout Print Configuration
|
||||||
|
|
@ -2618,6 +2635,8 @@ properties =
|
||||||
.number-of-grips = Number of Grips
|
.number-of-grips = Number of Grips
|
||||||
.number-of-vertices = Number of Vertices
|
.number-of-vertices = Number of Vertices
|
||||||
.number-of-turns = Number of turns
|
.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
|
.ole-frame = OLE Frame
|
||||||
.object-references = Object References
|
.object-references = Object References
|
||||||
.obliquing = Obliquing
|
.obliquing = Obliquing
|
||||||
|
|
@ -2737,6 +2756,13 @@ properties =
|
||||||
.start-tangent-x = Start Tangent X
|
.start-tangent-x = Start Tangent X
|
||||||
.start-tangent-y = Start Tangent Y
|
.start-tangent-y = Start Tangent Y
|
||||||
.start-tangent-z = Start Tangent Z
|
.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-x = Start X
|
||||||
.start-y = Start Y
|
.start-y = Start Y
|
||||||
.start-z = Start Z
|
.start-z = Start Z
|
||||||
|
|
|
||||||
|
|
@ -1339,6 +1339,7 @@ draw =
|
||||||
.solid-specify-second-point = SOLID İkinci noktayı belirtin:
|
.solid-specify-second-point = SOLID İkinci noktayı belirtin:
|
||||||
.solid-specify-third-point = SOLID Üçüncü noktayı belirtin:
|
.solid-specify-third-point = SOLID Üçüncü noktayı belirtin:
|
||||||
.spline-specify-first-control-point = SPLINE İlk kontrol noktasını belirtin:
|
.spline-specify-first-control-point = SPLINE İlk kontrol noktasını belirtin:
|
||||||
|
.spline-specify-first-point = SPLINE İlk noktayı belirtin:
|
||||||
.spline-specify-next-point-n-pts = SPLINE Sonraki noktayı belirtin [__ocs_arg_n__ nokta]:
|
.spline-specify-next-point-n-pts = SPLINE Sonraki noktayı belirtin [__ocs_arg_n__ nokta]:
|
||||||
.sketch = Serbest Çizim
|
.sketch = Serbest Çizim
|
||||||
.start-center-angle = Başlangıç, Merkez, Açı
|
.start-center-angle = Başlangıç, Merkez, Açı
|
||||||
|
|
@ -2292,6 +2293,7 @@ properties =
|
||||||
.chord-x = Kiriş X
|
.chord-x = Kiriş X
|
||||||
.chord-y = Kiriş Y
|
.chord-y = Kiriş Y
|
||||||
.chord-z = Kiriş Z
|
.chord-z = Kiriş Z
|
||||||
|
.chord = Kiriş
|
||||||
.circumference = Çevre
|
.circumference = Çevre
|
||||||
.class = Sınıf
|
.class = Sınıf
|
||||||
.class-id = Sınıf Kimliği
|
.class-id = Sınıf Kimliği
|
||||||
|
|
@ -2318,6 +2320,10 @@ properties =
|
||||||
.control-point-x = Kontrol Noktası X
|
.control-point-x = Kontrol Noktası X
|
||||||
.control-point-y = Kontrol Noktası Y
|
.control-point-y = Kontrol Noktası Y
|
||||||
.control-point-z = Kontrol Noktası Z
|
.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
|
.coordination-model = Koordinasyon Modeli
|
||||||
.creased-edges = Keskin Kenarlar
|
.creased-edges = Keskin Kenarlar
|
||||||
.croppings = Kırpmalar
|
.croppings = Kırpmalar
|
||||||
|
|
@ -2325,6 +2331,9 @@ properties =
|
||||||
.current-color-scheme = Geçerli Renk Düzeni
|
.current-color-scheme = Geçerli Renk Düzeni
|
||||||
.current-vertex-title-case = Geçerli Köşe
|
.current-vertex-title-case = Geçerli Köşe
|
||||||
.current-vertex-sentence-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
|
.cutting-line = Kesme Çizgisi
|
||||||
.dwg-payload = DWG Verisi
|
.dwg-payload = DWG Verisi
|
||||||
.dwg-version = DWG Sürümü
|
.dwg-version = DWG Sürümü
|
||||||
|
|
@ -2384,6 +2393,9 @@ properties =
|
||||||
.end-tangent-x = Bitiş Teğeti X
|
.end-tangent-x = Bitiş Teğeti X
|
||||||
.end-tangent-y = Bitiş Teğeti Y
|
.end-tangent-y = Bitiş Teğeti Y
|
||||||
.end-tangent-z = Bitiş Teğeti Z
|
.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-x = Bitiş X
|
||||||
.end-y = Bitiş Y
|
.end-y = Bitiş Y
|
||||||
.end-z = Bitiş Z
|
.end-z = Bitiş Z
|
||||||
|
|
@ -2421,6 +2433,10 @@ properties =
|
||||||
.fit-point-y = Uydurma Noktası Y
|
.fit-point-y = Uydurma Noktası Y
|
||||||
.fit-point-z = Uydurma Noktası Z
|
.fit-point-z = Uydurma Noktası Z
|
||||||
.fit-tolerance = Uydurma Toleransı
|
.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-title-case = Uydur/Yumuşat
|
||||||
.fit-smooth-sentence-case = Uydur/yumuşat
|
.fit-smooth-sentence-case = Uydur/yumuşat
|
||||||
.fixed-elevation-range = Sabit Kot Aralığı
|
.fixed-elevation-range = Sabit Kot Aralığı
|
||||||
|
|
@ -2466,6 +2482,7 @@ properties =
|
||||||
.jog-z = Kırılma Z
|
.jog-z = Kırılma Z
|
||||||
.justify = Hizala
|
.justify = Hizala
|
||||||
.knot-parameterization = Düğüm Parametrelemesi
|
.knot-parameterization = Düğüm Parametrelemesi
|
||||||
|
.knot-parameterization-sentence-case = Düğüm parametrelemesi
|
||||||
.lamp-preset = Lamba Ön Ayarı
|
.lamp-preset = Lamba Ön Ayarı
|
||||||
.landing-gap = Yatay Bağlantı Boşluğu
|
.landing-gap = Yatay Bağlantı Boşluğu
|
||||||
.layout-print-configuration = Yerleşim Yazdırma Yapılandırması
|
.layout-print-configuration = Yerleşim Yazdırma Yapılandırması
|
||||||
|
|
@ -2558,6 +2575,8 @@ properties =
|
||||||
.number-of-grips = Tutamaç Sayısı
|
.number-of-grips = Tutamaç Sayısı
|
||||||
.number-of-vertices = Köşe Sayısı
|
.number-of-vertices = Köşe Sayısı
|
||||||
.number-of-turns = Sarım 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
|
.ole-frame = OLE Çerçevesi
|
||||||
.object-references = Nesne Referansları
|
.object-references = Nesne Referansları
|
||||||
.obliquing = Eğiklik
|
.obliquing = Eğiklik
|
||||||
|
|
@ -2677,6 +2696,13 @@ properties =
|
||||||
.start-tangent-x = Başlangıç Teğeti X
|
.start-tangent-x = Başlangıç Teğeti X
|
||||||
.start-tangent-y = Başlangıç Teğeti Y
|
.start-tangent-y = Başlangıç Teğeti Y
|
||||||
.start-tangent-z = Başlangıç Teğeti Z
|
.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-x = Başlangıç X
|
||||||
.start-y = Başlangıç Y
|
.start-y = Başlangıç Y
|
||||||
.start-z = Başlangıç Z
|
.start-z = Başlangıç Z
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ pub struct GripPopup {
|
||||||
pub anchor: iced::Point,
|
pub anchor: iced::Point,
|
||||||
pub items: Vec<crate::scene::model::object::GripMenuItem>,
|
pub items: Vec<crate::scene::model::object::GripMenuItem>,
|
||||||
pub selected: usize,
|
pub selected: usize,
|
||||||
|
/// Whether a click-opened menu stays visible away from its grip.
|
||||||
|
pub pinned: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pending follow-up value for grip-menu actions that need a number
|
/// Pending follow-up value for grip-menu actions that need a number
|
||||||
|
|
|
||||||
|
|
@ -121,6 +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(
|
||||||
|
if crate::entities::spline::uses_fit_method(spline) {
|
||||||
|
spline.fit_points.len()
|
||||||
|
} else {
|
||||||
|
spline.control_points.len()
|
||||||
|
},
|
||||||
|
),
|
||||||
_ => 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)))
|
||||||
|
|
|
||||||
|
|
@ -1322,6 +1322,27 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// One-shot action — apply immediately.
|
// One-shot action — apply immediately.
|
||||||
|
let unchanged = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(popup.handle)
|
||||||
|
.is_some_and(|entity| match (item.action, entity) {
|
||||||
|
(GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => {
|
||||||
|
!spline.cv_frame_visible
|
||||||
|
&& crate::entities::spline::uses_fit_method(spline)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
GripMenuAction::ShowControlVertices,
|
||||||
|
acadrust::EntityType::Spline(spline),
|
||||||
|
) => {
|
||||||
|
spline.cv_frame_visible
|
||||||
|
|| !crate::entities::spline::uses_fit_method(spline)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
if unchanged {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.push_undo_snapshot(i, item.label);
|
self.push_undo_snapshot(i, item.label);
|
||||||
// For Add Leader, the new arrow becomes the last grip; remember
|
// For Add Leader, the new arrow becomes the last grip; remember
|
||||||
// its id so we can grab it for placement right after.
|
// its id so we can grab it for placement right after.
|
||||||
|
|
@ -1728,6 +1749,40 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
let handles = self.property_target_handles(i);
|
let handles = self.property_target_handles(i);
|
||||||
|
|
||||||
if !handles.is_empty() {
|
if !handles.is_empty() {
|
||||||
|
if matches!(field, "spline_method" | "knot_param" | "cv_frame") {
|
||||||
|
let unchanged = handles.iter().all(|handle| {
|
||||||
|
let Some(acadrust::EntityType::Spline(spline)) =
|
||||||
|
self.tabs[i].scene.document.get_entity(*handle)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match field {
|
||||||
|
"spline_method" => {
|
||||||
|
value == if crate::entities::spline::uses_fit_method(spline) {
|
||||||
|
"Fit"
|
||||||
|
} else {
|
||||||
|
"Control Vertices"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"knot_param" => {
|
||||||
|
let current = match spline.knot_parameterization {
|
||||||
|
0 => "Chord",
|
||||||
|
1 => "Square Root",
|
||||||
|
2 => "Uniform",
|
||||||
|
_ => "Custom",
|
||||||
|
};
|
||||||
|
value == current
|
||||||
|
}
|
||||||
|
"cv_frame" => {
|
||||||
|
value == if spline.cv_frame_visible { "Show" } else { "Hide" }
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if unchanged {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
self.push_undo_snapshot(i, "CHPROP");
|
self.push_undo_snapshot(i, "CHPROP");
|
||||||
|
|
||||||
if field == "vscale_std" {
|
if field == "vscale_std" {
|
||||||
|
|
@ -2099,6 +2154,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
}
|
}
|
||||||
self.invalidate_property_targets(i, &handles);
|
self.invalidate_property_targets(i, &handles);
|
||||||
self.tabs[i].dirty = true;
|
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();
|
self.refresh_properties();
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -2121,6 +2180,35 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
} else {
|
} else {
|
||||||
crate::app::expr_eval::eval_to_string(&raw_val)
|
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");
|
self.push_undo_snapshot(i, "CHPROP");
|
||||||
if field == "block" {
|
if field == "block" {
|
||||||
// Name row on a block reference: an existing name
|
// Name row on a block reference: an existing name
|
||||||
|
|
|
||||||
|
|
@ -653,7 +653,9 @@ impl OpenCADStudio {
|
||||||
screen: p,
|
screen: p,
|
||||||
started: iced::time::Instant::now(),
|
started: iced::time::Instant::now(),
|
||||||
});
|
});
|
||||||
self.grip_popup = None;
|
if !self.grip_popup.as_ref().is_some_and(|popup| popup.pinned) {
|
||||||
|
self.grip_popup = None;
|
||||||
|
}
|
||||||
} else if let Some(h) = self.grip_hover.as_mut() {
|
} else if let Some(h) = self.grip_hover.as_mut() {
|
||||||
h.screen = p;
|
h.screen = p;
|
||||||
}
|
}
|
||||||
|
|
@ -672,12 +674,17 @@ impl OpenCADStudio {
|
||||||
use crate::entities::traits::EntityTypeOps;
|
use crate::entities::traits::EntityTypeOps;
|
||||||
let items = e.grip_menu(grip_id);
|
let items = e.grip_menu(grip_id);
|
||||||
if !items.is_empty() {
|
if !items.is_empty() {
|
||||||
|
let selected = items
|
||||||
|
.iter()
|
||||||
|
.position(|item| item.label.starts_with('✓'))
|
||||||
|
.unwrap_or(0);
|
||||||
self.grip_popup = Some(crate::app::GripPopup {
|
self.grip_popup = Some(crate::app::GripPopup {
|
||||||
handle,
|
handle,
|
||||||
grip_id,
|
grip_id,
|
||||||
anchor: p,
|
anchor: p,
|
||||||
items,
|
items,
|
||||||
selected: 0,
|
selected,
|
||||||
|
pinned: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -685,7 +692,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.grip_hover = None;
|
self.grip_hover = None;
|
||||||
if let Some(popup) = &self.grip_popup {
|
if let Some(popup) = self.grip_popup.as_ref().filter(|popup| !popup.pinned) {
|
||||||
let dx = p.x - popup.anchor.x;
|
let dx = p.x - popup.anchor.x;
|
||||||
let dy = p.y - popup.anchor.y;
|
let dy = p.y - popup.anchor.y;
|
||||||
if (dx * dx + dy * dy).sqrt() > POPUP_DISMISS_PX {
|
if (dx * dx + dy * dy).sqrt() > POPUP_DISMISS_PX {
|
||||||
|
|
@ -2736,6 +2743,32 @@ impl OpenCADStudio {
|
||||||
let Some(&handle) = self.tabs[i].selected_grip_handles.get(grip_index) else {
|
let Some(&handle) = self.tabs[i].selected_grip_handles.get(grip_index) else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
|
let grip_shape = self.tabs[i].selected_grips[grip_index].shape;
|
||||||
|
if grip_shape == crate::scene::model::object::GripShape::Dropdown {
|
||||||
|
use crate::entities::traits::EntityTypeOps;
|
||||||
|
let items = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.map(|entity| entity.grip_menu(grip_id))
|
||||||
|
.unwrap_or_default();
|
||||||
|
if !items.is_empty() {
|
||||||
|
let selected = items
|
||||||
|
.iter()
|
||||||
|
.position(|item| item.label.starts_with('✓'))
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.grip_popup = Some(crate::app::GripPopup {
|
||||||
|
handle,
|
||||||
|
grip_id,
|
||||||
|
anchor: p_full,
|
||||||
|
items,
|
||||||
|
selected,
|
||||||
|
pinned: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.grip_hover = None;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
// The visibility (lookup) grip opens a state
|
// The visibility (lookup) grip opens a state
|
||||||
// dropdown instead of starting a stretch drag.
|
// dropdown instead of starting a stretch drag.
|
||||||
if grip_id == crate::app::visibility::VIS_GRIP_ID {
|
if grip_id == crate::app::visibility::VIS_GRIP_ID {
|
||||||
|
|
|
||||||
|
|
@ -414,22 +414,24 @@ impl OpenCADStudio {
|
||||||
None => tab.scene.active_model_tile_bounds(vw, vh),
|
None => tab.scene.active_model_tile_bounds(vw, vh),
|
||||||
};
|
};
|
||||||
let sel_h = tab.selected_handle;
|
let sel_h = tab.selected_handle;
|
||||||
// The Current Vertex the Properties panel is focused on:
|
// Mark the Properties panel's current point grip hot.
|
||||||
// mark that grip hot so the navigated vertex is visible in
|
|
||||||
// the drawing. Only for a single selected polyline, whose
|
|
||||||
// vertex grips are ids 0..n. (Properties vertex stepper)
|
|
||||||
let current_vertex_grip: Option<usize> = tab
|
let current_vertex_grip: Option<usize> = tab
|
||||||
.properties
|
.properties
|
||||||
.prop_vertex_indicator_active
|
.prop_vertex_indicator_active
|
||||||
.then(|| sel_h)
|
.then(|| sel_h)
|
||||||
.flatten()
|
.flatten()
|
||||||
.and_then(|h| {
|
.and_then(|h| {
|
||||||
matches!(
|
let indexed = match tab.scene.document.get_entity(h) {
|
||||||
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)) => {
|
||||||
)
|
!crate::entities::spline::uses_fit_method(spline)
|
||||||
.then_some(tab.properties.prop_vertex)
|
|| !spline.cv_frame_visible
|
||||||
|
|| crate::entities::curve::spline_curve(spline).is_none()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
indexed.then_some(tab.properties.prop_vertex)
|
||||||
});
|
});
|
||||||
// In-viewport grips are model-space; project them with the
|
// In-viewport grips are model-space; project them with the
|
||||||
// viewport camera so they sit on the wire the GPU draws.
|
// viewport camera so they sit on the wire the GPU draws.
|
||||||
|
|
@ -495,6 +497,30 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
let control_polygon = tab.selected_handle.and_then(|handle| {
|
||||||
|
let spline = match tab.scene.document.get_entity(handle) {
|
||||||
|
Some(acadrust::EntityType::Spline(spline)) if spline.cv_frame_visible => spline,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
if tab
|
||||||
|
.selected_grip_handles
|
||||||
|
.iter()
|
||||||
|
.any(|owner| *owner != handle)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let points: Vec<_> = grips
|
||||||
|
.iter()
|
||||||
|
.filter(|grip| {
|
||||||
|
grip.shape == crate::scene::model::object::GripShape::Circle
|
||||||
|
})
|
||||||
|
.map(|grip| grip.pos)
|
||||||
|
.collect();
|
||||||
|
(points.len() >= 2).then_some((
|
||||||
|
points,
|
||||||
|
spline.flags.closed || spline.flags.periodic,
|
||||||
|
))
|
||||||
|
});
|
||||||
let grip_clip = if grips.is_empty() {
|
let grip_clip = if grips.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -688,6 +714,7 @@ impl OpenCADStudio {
|
||||||
snap_ext_base,
|
snap_ext_base,
|
||||||
snap_ext_base2,
|
snap_ext_base2,
|
||||||
grips,
|
grips,
|
||||||
|
control_polygon,
|
||||||
grip_clip,
|
grip_clip,
|
||||||
ucs_icons,
|
ucs_icons,
|
||||||
ost_points,
|
ost_points,
|
||||||
|
|
@ -1092,26 +1119,28 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-functional grip popup (Phase 2). One bordered container
|
|
||||||
// wraps a column of borderless item buttons so the popup reads
|
|
||||||
// as a single widget instead of stacked tiles.
|
|
||||||
if let Some(popup) = self.grip_popup.as_ref() {
|
if let Some(popup) = self.grip_popup.as_ref() {
|
||||||
if !tab.is_start {
|
if !tab.is_start {
|
||||||
// Size the row to the widest label so the selection
|
let labels: Vec<String> = popup
|
||||||
// highlight fills the whole row instead of just the
|
|
||||||
// text glyphs. ~7 px per character at size 12 + the
|
|
||||||
// horizontal padding (10 + 10).
|
|
||||||
let max_len = popup
|
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| i.label.chars().count())
|
.map(|item| {
|
||||||
|
let (mark, raw) = item
|
||||||
|
.label
|
||||||
|
.strip_prefix("✓ ")
|
||||||
|
.map_or(("", item.label), |raw| ("✓ ", raw));
|
||||||
|
format!("{mark}{}", crate::i18n::translate(raw))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let max_len = labels
|
||||||
|
.iter()
|
||||||
|
.map(|label| label.chars().count())
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(8) as f32;
|
.unwrap_or(8) as f32;
|
||||||
let row_w = max_len * 7.0 + 24.0;
|
let row_w = max_len * 7.0 + 24.0;
|
||||||
let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w));
|
let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w));
|
||||||
for (idx, item) in popup.items.iter().enumerate() {
|
for (idx, label) in labels.into_iter().enumerate() {
|
||||||
let is_sel = idx == popup.selected;
|
let is_sel = idx == popup.selected;
|
||||||
let label = item.label;
|
|
||||||
let btn = button(text(label).size(12))
|
let btn = button(text(label).size(12))
|
||||||
.on_press(Message::GripMenuPick(idx))
|
.on_press(Message::GripMenuPick(idx))
|
||||||
.padding([3, 10])
|
.padding([3, 10])
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,30 @@ pub fn circle_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Round grip that edits one vertex.
|
||||||
|
pub fn round_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||||
|
GripDef {
|
||||||
|
id,
|
||||||
|
world,
|
||||||
|
is_midpoint: false,
|
||||||
|
shape: GripShape::Circle,
|
||||||
|
dir: None,
|
||||||
|
axis: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Screen-offset down-arrow that opens a choice menu for an entity.
|
||||||
|
pub fn dropdown_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||||
|
GripDef {
|
||||||
|
id,
|
||||||
|
world,
|
||||||
|
is_midpoint: false,
|
||||||
|
shape: GripShape::Dropdown,
|
||||||
|
dir: None,
|
||||||
|
axis: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mid-segment stretch grip oriented along `dir` (the segment's in-plane
|
/// Mid-segment stretch grip oriented along `dir` (the segment's in-plane
|
||||||
/// world-XY direction). Drawn as a small rectangle elongated along the
|
/// world-XY direction). Drawn as a small rectangle elongated along the
|
||||||
/// segment so the affordance reads as "stretch perpendicular".
|
/// segment so the affordance reads as "stretch perpendicular".
|
||||||
|
|
|
||||||
|
|
@ -287,12 +287,12 @@ 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 points: Vec<Vector3> = spline
|
let source = if crate::entities::spline::uses_fit_method(spline) {
|
||||||
.control_points
|
&spline.fit_points
|
||||||
.iter()
|
} else {
|
||||||
.chain(spline.fit_points.iter())
|
&spline.control_points
|
||||||
.copied()
|
};
|
||||||
.collect();
|
let points: Vec<Vector3> = source.to_vec();
|
||||||
let first = points.first()?;
|
let first = points.first()?;
|
||||||
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)));
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
use acadrust::entities::Spline;
|
use acadrust::entities::Spline;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
use cadkernel::geom2d::Curve as KernelCurve;
|
||||||
use cadkernel::space::NurbsCurve3;
|
use cadkernel::space::NurbsCurve3;
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
|
use crate::entities::common::{
|
||||||
|
dropdown_grip, edit_prop as edit, 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};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
|
|
||||||
|
pub(crate) fn uses_fit_method(spline: &Spline) -> bool {
|
||||||
|
let degree = spline.degree.max(1) as usize;
|
||||||
|
!spline.fit_points.is_empty() && spline.control_points.len() <= degree
|
||||||
|
}
|
||||||
|
|
||||||
fn to_render(spl: &Spline) -> RenderEntity {
|
fn to_render(spl: &Spline) -> RenderEntity {
|
||||||
let n = spl.control_points.len();
|
let n = spl.control_points.len();
|
||||||
|
|
@ -86,22 +94,24 @@ fn to_render(spl: &Spline) -> RenderEntity {
|
||||||
let (snap_pts, key_vertices) = match curve_snap {
|
let (snap_pts, key_vertices) = match curve_snap {
|
||||||
Some(snap) => {
|
Some(snap) => {
|
||||||
let mut points = snap.snap_pts;
|
let mut points = snap.snap_pts;
|
||||||
points.extend(spl.fit_points.iter().map(|p| {
|
if uses_fit_method(spl) {
|
||||||
(
|
points.extend(spl.fit_points.iter().map(|p| {
|
||||||
glam::DVec3::new(p.x, p.y, p.z),
|
(
|
||||||
crate::scene::model::wire_model::SnapHint::Endpoint,
|
glam::DVec3::new(p.x, p.y, p.z),
|
||||||
)
|
crate::scene::model::wire_model::SnapHint::Endpoint,
|
||||||
}));
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
(points, Vec::new())
|
(points, Vec::new())
|
||||||
}
|
}
|
||||||
// A spline through points in space is not a planar curve, so the
|
// A spline through points in space is not a planar curve, so the
|
||||||
// kernel has nothing to say about it. Its stored points remain the
|
// kernel has nothing to say about it. Its stored points remain the
|
||||||
// only thing to offer.
|
// only thing to offer.
|
||||||
None => {
|
None => {
|
||||||
let source = if spl.fit_points.is_empty() {
|
let source = if uses_fit_method(spl) {
|
||||||
&spl.control_points
|
|
||||||
} else {
|
|
||||||
&spl.fit_points
|
&spl.fit_points
|
||||||
|
} else {
|
||||||
|
&spl.control_points
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|
|
@ -141,10 +151,9 @@ fn to_render(spl: &Spline) -> RenderEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
||||||
let count = spl.control_points.len();
|
if uses_fit_method(spl) {
|
||||||
if count < 2 {
|
|
||||||
if spl.fit_points.len() < 2 {
|
if spl.fit_points.len() < 2 {
|
||||||
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
return spl.fit_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||||
}
|
}
|
||||||
return if spl.flags.closed || spl.flags.periodic {
|
return if spl.flags.closed || spl.flags.periodic {
|
||||||
catmull_rom_polyline(&spl.fit_points, true)
|
catmull_rom_polyline(&spl.fit_points, true)
|
||||||
|
|
@ -153,6 +162,11 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let count = spl.control_points.len();
|
||||||
|
if count < 2 {
|
||||||
|
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||||
|
}
|
||||||
|
|
||||||
let degree = spl.degree.max(0) as usize;
|
let degree = spl.degree.max(0) as usize;
|
||||||
if degree == 0 || degree >= count {
|
if degree == 0 || degree >= count {
|
||||||
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||||
|
|
@ -261,6 +275,60 @@ fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some((h, slopes)) = fit_spline_slopes(spl, &p) else {
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Evaluate each segment as a cubic Hermite (dP/du = slope · h_i).
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for i in 0..n - 1 {
|
||||||
|
let point_at = |u: f64| {
|
||||||
|
let (u2, u3) = (u * u, u * u * u);
|
||||||
|
let h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
|
||||||
|
let h10 = u3 - 2.0 * u2 + u;
|
||||||
|
let h01 = -2.0 * u3 + 3.0 * u2;
|
||||||
|
let h11 = u3 - u2;
|
||||||
|
let mut q = [0.0f64; 3];
|
||||||
|
for k in 0..3 {
|
||||||
|
let m0 = slopes[k][i] * h[i];
|
||||||
|
let m1 = slopes[k][i + 1] * h[i];
|
||||||
|
q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1;
|
||||||
|
}
|
||||||
|
q
|
||||||
|
};
|
||||||
|
let tangent_at = |u: f64| {
|
||||||
|
let u2 = u * u;
|
||||||
|
let (h00, h10, h01, h11) = (
|
||||||
|
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 q = [0.0; 3];
|
||||||
|
for k in 0..3 {
|
||||||
|
let m0 = slopes[k][i] * h[i];
|
||||||
|
let m1 = slopes[k][i + 1] * h[i];
|
||||||
|
q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1;
|
||||||
|
}
|
||||||
|
q
|
||||||
|
};
|
||||||
|
let sampled = cadkernel::tessellation::sample_curve3_angle(
|
||||||
|
point_at,
|
||||||
|
tangent_at,
|
||||||
|
cadkernel::tessellation::DEFAULT_ANGLE,
|
||||||
|
);
|
||||||
|
out.extend(sampled.into_iter().skip(usize::from(i > 0)));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slopes used by spatial fit interpolation.
|
||||||
|
fn fit_spline_slopes(spl: &Spline, p: &[[f64; 3]]) -> Option<(Vec<f64>, [Vec<f64>; 3])> {
|
||||||
|
let n = p.len();
|
||||||
|
if n < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// Parameterise the fit points (unnormalised, so a unit end tangent — how the
|
// Parameterise the fit points (unnormalised, so a unit end tangent — how the
|
||||||
// tangents are stored — is a consistent dP/dt). Match the spline's knot
|
// tangents are stored — is a consistent dP/dt). Match the spline's knot
|
||||||
// parameterisation: 2 = uniform, 1 = centripetal (√chord), else chord.
|
// parameterisation: 2 = uniform, 1 = centripetal (√chord), else chord.
|
||||||
|
|
@ -329,48 +397,7 @@ fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
||||||
thomas_solve(&a, &b, &c, &mut d);
|
thomas_solve(&a, &b, &c, &mut d);
|
||||||
slopes[k] = d;
|
slopes[k] = d;
|
||||||
}
|
}
|
||||||
|
Some((h, slopes))
|
||||||
// Evaluate each segment as a cubic Hermite (dP/du = slope · h_i).
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for i in 0..n - 1 {
|
|
||||||
let point_at = |u: f64| {
|
|
||||||
let (u2, u3) = (u * u, u * u * u);
|
|
||||||
let h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
|
|
||||||
let h10 = u3 - 2.0 * u2 + u;
|
|
||||||
let h01 = -2.0 * u3 + 3.0 * u2;
|
|
||||||
let h11 = u3 - u2;
|
|
||||||
let mut q = [0.0f64; 3];
|
|
||||||
for k in 0..3 {
|
|
||||||
let m0 = slopes[k][i] * h[i];
|
|
||||||
let m1 = slopes[k][i + 1] * h[i];
|
|
||||||
q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1;
|
|
||||||
}
|
|
||||||
q
|
|
||||||
};
|
|
||||||
let tangent_at = |u: f64| {
|
|
||||||
let u2 = u * u;
|
|
||||||
let (h00, h10, h01, h11) = (
|
|
||||||
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 q = [0.0; 3];
|
|
||||||
for k in 0..3 {
|
|
||||||
let m0 = slopes[k][i] * h[i];
|
|
||||||
let m1 = slopes[k][i + 1] * h[i];
|
|
||||||
q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1;
|
|
||||||
}
|
|
||||||
q
|
|
||||||
};
|
|
||||||
let sampled = cadkernel::tessellation::sample_curve3_angle(
|
|
||||||
point_at,
|
|
||||||
tangent_at,
|
|
||||||
cadkernel::tessellation::DEFAULT_ANGLE,
|
|
||||||
);
|
|
||||||
out.extend(sampled.into_iter().skip(usize::from(i > 0)));
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// In-place Thomas solve for a tridiagonal system (`a` sub-, `b` main-, `c`
|
/// In-place Thomas solve for a tridiagonal system (`a` sub-, `b` main-, `c`
|
||||||
|
|
@ -390,189 +417,482 @@ fn thomas_solve(a: &[f64], b: &[f64], c: &[f64], d: &mut [f64]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grips(spline: &Spline) -> Vec<GripDef> {
|
pub(crate) const SPLINE_MODE_GRIP_ID: usize = usize::MAX - 1;
|
||||||
// A fit-point spline carries no control points, so its editable points — and
|
|
||||||
// therefore its grips — are the fit points it interpolates. A CV spline
|
fn control_vertices(spline: &Spline) -> Vec<acadrust::types::Vector3> {
|
||||||
// grips its control vertices.
|
if !uses_fit_method(spline) {
|
||||||
let source = if spline.control_points.is_empty() {
|
return spline.control_points.clone();
|
||||||
&spline.fit_points
|
}
|
||||||
} else {
|
let Some(planar) = crate::entities::curve::spline_curve(spline) else {
|
||||||
&spline.control_points
|
return Vec::new();
|
||||||
};
|
};
|
||||||
source
|
let KernelCurve::Nurbs(curve) = &planar.curve else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
curve
|
||||||
|
.control_points()
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.map(|point| {
|
||||||
.map(|(i, p)| square_grip(i, glam::DVec3::new(p.x, p.y, p.z)))
|
let point = planar.plane.point_at(*point);
|
||||||
|
acadrust::types::Vector3::new(point[0], point[1], point[2])
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties(spline: &Spline) -> Vec<PropSection> {
|
fn choice_prop(
|
||||||
let show = if spline.fit_points.is_empty() {
|
label: &str,
|
||||||
"Control Vertices"
|
field: &'static str,
|
||||||
} else {
|
selected: &str,
|
||||||
"Fit Points"
|
options: &[&str],
|
||||||
};
|
) -> Property {
|
||||||
let cp0 = spline.control_points.first();
|
Property {
|
||||||
let fp0 = spline.fit_points.first();
|
label: label.into(),
|
||||||
let w0 = spline.weights.first().copied();
|
field,
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: selected.to_string(),
|
||||||
|
options: options.iter().map(|option| option.to_string()).collect(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Polyline-approximation length over the effective defining points.
|
fn index_prop(label: &str, field: &'static str, index: usize, count: usize) -> Property {
|
||||||
let pts = if spline.fit_points.is_empty() {
|
Property {
|
||||||
&spline.control_points
|
label: label.into(),
|
||||||
} else {
|
field,
|
||||||
&spline.fit_points
|
value: PropValue::EditText(if count == 0 {
|
||||||
|
"0".to_string()
|
||||||
|
} else {
|
||||||
|
(index + 1).to_string()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_to_control_method(spline: &mut Spline) -> bool {
|
||||||
|
if !uses_fit_method(spline) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(planar) = crate::entities::curve::spline_curve(spline) else {
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
let mut length = 0.0;
|
let KernelCurve::Nurbs(curve) = &planar.curve else {
|
||||||
for w in pts.windows(2) {
|
return false;
|
||||||
let dx = w[1].x - w[0].x;
|
};
|
||||||
let dy = w[1].y - w[0].y;
|
spline.degree = curve.degree() as i32;
|
||||||
let dz = w[1].z - w[0].z;
|
spline.knots = curve.knots().to_vec();
|
||||||
length += (dx * dx + dy * dy + dz * dz).sqrt();
|
spline.control_points = curve
|
||||||
|
.control_points()
|
||||||
|
.iter()
|
||||||
|
.map(|point| {
|
||||||
|
let point = planar.plane.point_at(*point);
|
||||||
|
acadrust::types::Vector3::new(point[0], point[1], point[2])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
spline.weights = if curve.is_rational() {
|
||||||
|
curve.weights().to_vec()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
spline.flags.rational = curve.is_rational();
|
||||||
|
spline.flags.planar = true;
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
tangent.x * tangent.x + tangent.y * tangent.y + tangent.z * tangent.z > 1e-18
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stored or kernel-derived fit-curve end tangents.
|
||||||
|
fn effective_fit_tangents(
|
||||||
|
spline: &Spline,
|
||||||
|
) -> (acadrust::types::Vector3, acadrust::types::Vector3) {
|
||||||
|
let begin_set = tangent_is_set(&spline.begin_tangent);
|
||||||
|
let end_set = tangent_is_set(&spline.end_tangent);
|
||||||
|
if begin_set && end_set {
|
||||||
|
return (spline.begin_tangent, spline.end_tangent);
|
||||||
}
|
}
|
||||||
|
|
||||||
let closed = spline.flags.closed || spline.flags.periodic;
|
let derived = crate::entities::curve::spline_curve(spline).and_then(|planar| {
|
||||||
let yes_no = |b: bool| if b { "Yes" } else { "No" };
|
let KernelCurve::Nurbs(curve) = &planar.curve else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let (start, end) = curve.domain();
|
||||||
|
Some((
|
||||||
|
planar.plane.vector_at(curve.derivative_at_knot(start)),
|
||||||
|
planar.plane.vector_at(curve.derivative_at_knot(end)),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
// Knot parameterization method (R2013+ DWG); older splines report 0.
|
let Some((derived_begin, derived_end)) = derived else {
|
||||||
|
return (spline.begin_tangent, spline.end_tangent);
|
||||||
|
};
|
||||||
|
let vector = |value: [f64; 3]| {
|
||||||
|
acadrust::types::Vector3::new(value[0], value[1], value[2])
|
||||||
|
};
|
||||||
|
(
|
||||||
|
if begin_set {
|
||||||
|
spline.begin_tangent
|
||||||
|
} else {
|
||||||
|
vector(derived_begin)
|
||||||
|
},
|
||||||
|
if end_set {
|
||||||
|
spline.end_tangent
|
||||||
|
} else {
|
||||||
|
vector(derived_end)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grips(spline: &Spline) -> Vec<GripDef> {
|
||||||
|
let fit_method = uses_fit_method(spline);
|
||||||
|
let derived_control = control_vertices(spline);
|
||||||
|
let show_control_vertices = (spline.cv_frame_visible || !fit_method)
|
||||||
|
&& derived_control.len() >= 2;
|
||||||
|
let source = if show_control_vertices {
|
||||||
|
derived_control
|
||||||
|
} else if fit_method {
|
||||||
|
spline.fit_points.clone()
|
||||||
|
} else {
|
||||||
|
spline.control_points.clone()
|
||||||
|
};
|
||||||
|
let mut grips: Vec<_> = source
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, p)| {
|
||||||
|
let world = glam::DVec3::new(p.x, p.y, p.z);
|
||||||
|
if show_control_vertices {
|
||||||
|
round_grip(i, world)
|
||||||
|
} else {
|
||||||
|
square_grip(i, world)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
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 fit_method = uses_fit_method(spline);
|
||||||
|
let method = if fit_method { "Fit" } else { "Control Vertices" };
|
||||||
|
let closed = spline.flags.closed || spline.flags.periodic;
|
||||||
|
let can_show_control = control_vertices(spline).len() >= 2;
|
||||||
|
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",
|
||||||
1 => "Square Root",
|
1 => "Square Root",
|
||||||
2 => "Uniform",
|
2 => "Uniform",
|
||||||
|
15 => "Custom",
|
||||||
_ => "Custom",
|
_ => "Custom",
|
||||||
};
|
};
|
||||||
|
let current = crate::scene::view::dispatch::prop_current_vertex();
|
||||||
// Closed splines enclose an area; approximate it with the shoelace
|
let (effective_begin_tangent, effective_end_tangent) = effective_fit_tangents(spline);
|
||||||
// formula over the defining points projected to the XY plane, matching
|
let mut data_points = if fit_method {
|
||||||
// the polyline approximation already used for Length.
|
let count = spline.fit_points.len();
|
||||||
let area = if closed && pts.len() >= 3 {
|
let index = current.min(count.saturating_sub(1));
|
||||||
let mut acc = 0.0;
|
let point = spline.fit_points.get(index);
|
||||||
for w in pts.windows(2) {
|
vec![
|
||||||
acc += w[0].x * w[1].y - w[1].x * w[0].y;
|
ro(
|
||||||
}
|
t!("Number of fit points").as_ref(),
|
||||||
if let (Some(first), Some(last)) = (pts.first(), pts.last()) {
|
"fit_pt_count",
|
||||||
acc += last.x * first.y - first.x * last.y;
|
count.to_string(),
|
||||||
}
|
),
|
||||||
format!("{:.4}", acc.abs() * 0.5)
|
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),
|
||||||
|
),
|
||||||
|
if matches!(spline.knot_parameterization, 0..=2) {
|
||||||
|
choice_prop(
|
||||||
|
t!("Knot parameterization").as_ref(),
|
||||||
|
"knot_param",
|
||||||
|
knot_param,
|
||||||
|
&["Chord", "Square Root", "Uniform"],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ro(
|
||||||
|
t!("Knot parameterization").as_ref(),
|
||||||
|
"knot_param",
|
||||||
|
crate::i18n::translate(knot_param).into_owned(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
]
|
||||||
} else {
|
} 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(if can_show_control {
|
||||||
|
choice_prop(
|
||||||
|
t!("CV frame").as_ref(),
|
||||||
|
"cv_frame",
|
||||||
|
if spline.cv_frame_visible { "Show" } else { "Hide" },
|
||||||
|
&["Hide", "Show"],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ro(t!("CV frame").as_ref(), "cv_frame", t!("Hide").into_owned())
|
||||||
|
});
|
||||||
|
|
||||||
|
let method_prop = if fit_method && can_show_control {
|
||||||
|
choice_prop(
|
||||||
|
t!("Method").as_ref(),
|
||||||
|
"spline_method",
|
||||||
|
method,
|
||||||
|
&["Fit", "Control Vertices"],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ro(
|
||||||
|
t!("Method").as_ref(),
|
||||||
|
"spline_method",
|
||||||
|
crate::i18n::translate(method).into_owned(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut misc = vec![
|
||||||
|
method_prop,
|
||||||
|
ro(t!("Degree").as_ref(), "degree", spline.degree.to_string()),
|
||||||
|
ro(
|
||||||
|
t!("Closed").as_ref(),
|
||||||
|
"closed",
|
||||||
|
crate::i18n::translate(yes_no(closed)).into_owned(),
|
||||||
|
),
|
||||||
|
ro(
|
||||||
|
t!("Periodic").as_ref(),
|
||||||
|
"periodic",
|
||||||
|
crate::i18n::translate(yes_no(spline.flags.periodic)).into_owned(),
|
||||||
|
),
|
||||||
|
ro(
|
||||||
|
t!("Planar").as_ref(),
|
||||||
|
"planar",
|
||||||
|
crate::i18n::translate(yes_no(is_planar(spline))).into_owned(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if fit_method {
|
||||||
|
misc.extend([
|
||||||
|
edit(
|
||||||
|
t!("Start tangent vector X").as_ref(),
|
||||||
|
"start_tan_x",
|
||||||
|
effective_begin_tangent.x,
|
||||||
|
),
|
||||||
|
edit(
|
||||||
|
t!("Start tangent vector Y").as_ref(),
|
||||||
|
"start_tan_y",
|
||||||
|
effective_begin_tangent.y,
|
||||||
|
),
|
||||||
|
edit(
|
||||||
|
t!("Start tangent vector Z").as_ref(),
|
||||||
|
"start_tan_z",
|
||||||
|
effective_begin_tangent.z,
|
||||||
|
),
|
||||||
|
edit(
|
||||||
|
t!("End tangent vector X").as_ref(),
|
||||||
|
"end_tan_x",
|
||||||
|
effective_end_tangent.x,
|
||||||
|
),
|
||||||
|
edit(
|
||||||
|
t!("End tangent vector Y").as_ref(),
|
||||||
|
"end_tan_y",
|
||||||
|
effective_end_tangent.y,
|
||||||
|
),
|
||||||
|
edit(
|
||||||
|
t!("End tangent vector Z").as_ref(),
|
||||||
|
"end_tan_z",
|
||||||
|
effective_end_tangent.z,
|
||||||
|
),
|
||||||
|
edit(
|
||||||
|
t!("Fit tolerance").as_ref(),
|
||||||
|
"fit_tolerance",
|
||||||
|
spline.fit_tolerance,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Data Points").into_owned(),
|
title: t!("Data Points").into_owned(),
|
||||||
props: vec![
|
props: data_points,
|
||||||
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),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Misc").into_owned(),
|
title: t!("Misc").into_owned(),
|
||||||
props: vec![
|
props: misc,
|
||||||
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),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
|
||||||
|
match field {
|
||||||
|
"spline_method" => {
|
||||||
|
if value == "Control Vertices" {
|
||||||
|
convert_to_control_method(spline);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"knot_param" => {
|
||||||
|
spline.knot_parameterization = match value {
|
||||||
|
"Chord" => 0,
|
||||||
|
"Square Root" => 1,
|
||||||
|
"Uniform" => 2,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"cv_frame" => {
|
||||||
|
spline.cv_frame_visible = value == "Show" && control_vertices(spline).len() >= 2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
.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 {
|
match field {
|
||||||
"ctrl_pt_x" => {
|
"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;
|
cp.x = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"ctrl_pt_y" => {
|
"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;
|
cp.y = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"ctrl_pt_z" => {
|
"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;
|
cp.z = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"weight" => {
|
"weight" => {
|
||||||
if let Some(w) = spline.weights.first_mut() {
|
if v > 0.0 && !spline.control_points.is_empty() {
|
||||||
*w = v;
|
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" => {
|
"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;
|
fp.x = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"fit_pt_y" => {
|
"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;
|
fp.y = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"fit_pt_z" => {
|
"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;
|
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_x" | "start_tan_y" | "start_tan_z" => {
|
||||||
"start_tan_y" => spline.begin_tangent.y = v,
|
if !tangent_is_set(&spline.begin_tangent) {
|
||||||
"start_tan_z" => spline.begin_tangent.z = v,
|
spline.begin_tangent = effective_begin_tangent;
|
||||||
"end_tan_x" => spline.end_tangent.x = v,
|
}
|
||||||
"end_tan_y" => spline.end_tangent.y = v,
|
match field {
|
||||||
"end_tan_z" => spline.end_tangent.z = 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,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"end_tan_x" | "end_tan_y" | "end_tan_z" => {
|
||||||
|
if !tangent_is_set(&spline.end_tangent) {
|
||||||
|
spline.end_tangent = effective_end_tangent;
|
||||||
|
}
|
||||||
|
match field {
|
||||||
|
"end_tan_x" => spline.end_tangent.x = v,
|
||||||
|
"end_tan_y" => spline.end_tangent.y = v,
|
||||||
|
"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) {
|
fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
||||||
// Match the grip source: dragging a fit-point spline's grip moves the fit
|
if grip_id == SPLINE_MODE_GRIP_ID {
|
||||||
// point (the curve re-fits through it on the next tessellation, keeping the
|
return;
|
||||||
// stored end tangents); a CV spline moves the control vertex.
|
}
|
||||||
let target = if spline.control_points.is_empty() {
|
// First CV edit makes the displayed kernel controls authoritative.
|
||||||
spline.fit_points.get_mut(grip_id)
|
let fit_method = uses_fit_method(spline);
|
||||||
} else {
|
if spline.cv_frame_visible && fit_method {
|
||||||
|
if !convert_to_control_method(spline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let target = if spline.cv_frame_visible || !fit_method {
|
||||||
spline.control_points.get_mut(grip_id)
|
spline.control_points.get_mut(grip_id)
|
||||||
|
} else {
|
||||||
|
spline.fit_points.get_mut(grip_id)
|
||||||
};
|
};
|
||||||
if let Some(cp) = target {
|
if let Some(cp) = target {
|
||||||
match apply {
|
match apply {
|
||||||
|
|
@ -614,96 +934,62 @@ impl crate::entities::traits::Grippable for Spline {
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
apply_grip(self, grip_id, apply);
|
apply_grip(self, grip_id, apply);
|
||||||
}
|
}
|
||||||
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||||
vec![
|
if grip_id == SPLINE_MODE_GRIP_ID {
|
||||||
GripMenuItem {
|
let fit_method = uses_fit_method(self);
|
||||||
label: "Stretch",
|
let can_show_control = control_vertices(self).len() >= 2;
|
||||||
action: GripMenuAction::Stretch,
|
return if self.cv_frame_visible || !fit_method {
|
||||||
},
|
vec![
|
||||||
GripMenuItem {
|
(can_show_control && fit_method).then_some(GripMenuItem {
|
||||||
label: "Add Vertex",
|
label: "Fit",
|
||||||
action: GripMenuAction::AddVertex,
|
action: GripMenuAction::ShowFit,
|
||||||
},
|
}),
|
||||||
GripMenuItem {
|
can_show_control.then_some(GripMenuItem {
|
||||||
label: "Remove Vertex",
|
label: "✓ Control Vertices",
|
||||||
action: GripMenuAction::RemoveVertex,
|
action: GripMenuAction::ShowControlVertices,
|
||||||
},
|
}),
|
||||||
GripMenuItem {
|
]
|
||||||
label: "Refine Vertices",
|
.into_iter()
|
||||||
action: GripMenuAction::RefineVertices,
|
.flatten()
|
||||||
},
|
.collect()
|
||||||
]
|
} else {
|
||||||
|
vec![
|
||||||
|
Some(GripMenuItem {
|
||||||
|
label: "✓ Fit",
|
||||||
|
action: GripMenuAction::ShowFit,
|
||||||
|
}),
|
||||||
|
can_show_control.then_some(GripMenuItem {
|
||||||
|
label: "Control Vertices",
|
||||||
|
action: GripMenuAction::ShowControlVertices,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
vec![GripMenuItem {
|
||||||
|
label: "Stretch",
|
||||||
|
action: GripMenuAction::Stretch,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
fn apply_grip_menu(
|
fn apply_grip_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
grip_id: usize,
|
_grip_id: usize,
|
||||||
action: crate::scene::model::object::GripMenuAction,
|
action: crate::scene::model::object::GripMenuAction,
|
||||||
) {
|
) {
|
||||||
use crate::scene::model::object::GripMenuAction as A;
|
use crate::scene::model::object::GripMenuAction as A;
|
||||||
let n = self.control_points.len();
|
|
||||||
let min_cv = (self.degree as usize).saturating_add(1).max(2);
|
|
||||||
match action {
|
match action {
|
||||||
A::AddVertex if grip_id < n => {
|
A::ShowFit => {
|
||||||
let i1 = (grip_id + 1).min(n - 1);
|
if convert_to_fit_method(self) {
|
||||||
if i1 == grip_id {
|
self.cv_frame_visible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let p0 = &self.control_points[grip_id];
|
|
||||||
let p1 = &self.control_points[i1];
|
|
||||||
let mid = acadrust::types::Vector3::new(
|
|
||||||
(p0.x + p1.x) * 0.5,
|
|
||||||
(p0.y + p1.y) * 0.5,
|
|
||||||
(p0.z + p1.z) * 0.5,
|
|
||||||
);
|
|
||||||
self.control_points.insert(i1, mid);
|
|
||||||
if !self.weights.is_empty() && self.weights.len() == n {
|
|
||||||
let w = (self.weights[grip_id] + self.weights[i1.min(self.weights.len() - 1)])
|
|
||||||
* 0.5;
|
|
||||||
self.weights.insert(i1, w);
|
|
||||||
}
|
|
||||||
// Clear knots so to_render rebuilds a uniform knot vector
|
|
||||||
// for the new CV count.
|
|
||||||
self.knots.clear();
|
|
||||||
}
|
}
|
||||||
A::RemoveVertex if grip_id < n && n > min_cv => {
|
A::ShowControlVertices if control_vertices(self).len() >= 2 => {
|
||||||
self.control_points.remove(grip_id);
|
self.cv_frame_visible = true;
|
||||||
if grip_id < self.weights.len() {
|
return;
|
||||||
self.weights.remove(grip_id);
|
|
||||||
}
|
|
||||||
self.knots.clear();
|
|
||||||
}
|
|
||||||
A::RefineVertices => {
|
|
||||||
// Insert a CV between every adjacent pair (chord midpoints)
|
|
||||||
// and rebuild a uniform knot vector.
|
|
||||||
if n >= 2 {
|
|
||||||
let mut refined = Vec::with_capacity(n * 2 - 1);
|
|
||||||
let mut refined_w = Vec::with_capacity(n * 2 - 1);
|
|
||||||
let has_w = !self.weights.is_empty() && self.weights.len() == n;
|
|
||||||
for i in 0..n {
|
|
||||||
refined.push(self.control_points[i].clone());
|
|
||||||
if has_w {
|
|
||||||
refined_w.push(self.weights[i]);
|
|
||||||
}
|
|
||||||
if i + 1 < n {
|
|
||||||
let a = &self.control_points[i];
|
|
||||||
let b = &self.control_points[i + 1];
|
|
||||||
refined.push(acadrust::types::Vector3::new(
|
|
||||||
(a.x + b.x) * 0.5,
|
|
||||||
(a.y + b.y) * 0.5,
|
|
||||||
(a.z + b.z) * 0.5,
|
|
||||||
));
|
|
||||||
if has_w {
|
|
||||||
refined_w.push((self.weights[i] + self.weights[i + 1]) * 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.control_points = refined;
|
|
||||||
if has_w {
|
|
||||||
self.weights = refined_w;
|
|
||||||
}
|
|
||||||
self.knots.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||||
"Character Spacing" => Some(("properties", "character-spacing")),
|
"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")),
|
"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")),
|
"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 X" => Some(("properties", "chord-x")),
|
||||||
"Chord Y" => Some(("properties", "chord-y")),
|
"Chord Y" => Some(("properties", "chord-y")),
|
||||||
"Chord Z" => Some(("properties", "chord-z")),
|
"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 X" => Some(("properties", "control-point-x")),
|
||||||
"Control Point Y" => Some(("properties", "control-point-y")),
|
"Control Point Y" => Some(("properties", "control-point-y")),
|
||||||
"Control Point Z" => Some(("properties", "control-point-z")),
|
"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")),
|
"Coordinates" => Some(("common", "coordinates")),
|
||||||
"Coordination Model" => Some(("properties", "coordination-model")),
|
"Coordination Model" => Some(("properties", "coordination-model")),
|
||||||
"Copied path: {}" => Some(("common", "copied-path")),
|
"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 Color Scheme" => Some(("properties", "current-color-scheme")),
|
||||||
"Current MLeader style: {}" => Some(("layers", "current-mleader-style")),
|
"Current MLeader style: {}" => Some(("layers", "current-mleader-style")),
|
||||||
"Current Vertex" => Some(("properties", "current-vertex-title-case")),
|
"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 annotation scale: {name}" => Some(("view", "current-annotation-scale-name")),
|
||||||
"Current dim style set to '{}'." => Some(("common", "current-dim-style-set-to")),
|
"Current dim style set to '{}'." => Some(("common", "current-dim-style-set-to")),
|
||||||
"Current layer" => Some(("layers", "current-layer")),
|
"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 X" => Some(("properties", "end-tangent-x")),
|
||||||
"End Tangent Y" => Some(("properties", "end-tangent-y")),
|
"End Tangent Y" => Some(("properties", "end-tangent-y")),
|
||||||
"End Tangent Z" => Some(("properties", "end-tangent-z")),
|
"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 X" => Some(("properties", "end-x")),
|
||||||
"End Y" => Some(("properties", "end-y")),
|
"End Y" => Some(("properties", "end-y")),
|
||||||
"End Z" => Some(("properties", "end-z")),
|
"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 Y" => Some(("properties", "fit-point-y")),
|
||||||
"Fit Point Z" => Some(("properties", "fit-point-z")),
|
"Fit Point Z" => Some(("properties", "fit-point-z")),
|
||||||
"Fit Tolerance" => Some(("properties", "fit-tolerance")),
|
"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 to paper" => Some(("plot", "fit-to-paper")),
|
||||||
"Fit/Smooth" => Some(("properties", "fit-smooth-title-case")),
|
"Fit/Smooth" => Some(("properties", "fit-smooth-title-case")),
|
||||||
"Fit/smooth" => Some(("properties", "fit-smooth-sentence-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")),
|
"Kilometers" => Some(("common", "kilometers")),
|
||||||
"Kind" => Some(("model", "kind")),
|
"Kind" => Some(("model", "kind")),
|
||||||
"Knot Parameterization" => Some(("properties", "knot-parameterization")),
|
"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 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: 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")),
|
"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 Grips" => Some(("properties", "number-of-grips")),
|
||||||
"Number of Vertices" => Some(("properties", "number-of-vertices")),
|
"Number of Vertices" => Some(("properties", "number-of-vertices")),
|
||||||
"Number of turns" => Some(("properties", "number-of-turns")),
|
"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: 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: 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")),
|
"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")),
|
"Pinned" => Some(("common", "pinned")),
|
||||||
"Pitch And Family" => Some(("properties", "pitch-and-family")),
|
"Pitch And Family" => Some(("properties", "pitch-and-family")),
|
||||||
"Place text manually (DIMUPT)" => Some(("common", "place-text-manually-dimupt")),
|
"Place text manually (DIMUPT)" => Some(("common", "place-text-manually-dimupt")),
|
||||||
|
"Periodic" => Some(("properties", "periodic")),
|
||||||
"Planar" => Some(("properties", "planar")),
|
"Planar" => Some(("properties", "planar")),
|
||||||
"Platform" => Some(("ui", "platform")),
|
"Platform" => Some(("ui", "platform")),
|
||||||
"Plot" => Some(("common", "plot")),
|
"Plot" => Some(("common", "plot")),
|
||||||
|
|
@ -2192,6 +2211,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||||
"SOLID Specify second point:" => Some(("draw", "solid-specify-second-point")),
|
"SOLID Specify second point:" => Some(("draw", "solid-specify-second-point")),
|
||||||
"SOLID Specify third point:" => Some(("draw", "solid-specify-third-point")),
|
"SOLID Specify third point:" => Some(("draw", "solid-specify-third-point")),
|
||||||
"SPLINE Specify first control point:" => Some(("draw", "spline-specify-first-control-point")),
|
"SPLINE Specify first control point:" => Some(("draw", "spline-specify-first-control-point")),
|
||||||
|
"SPLINE Specify first point:" => Some(("draw", "spline-specify-first-point")),
|
||||||
"SPLINE Specify next point [%{n} pts]:" => Some(("draw", "spline-specify-next-point-n-pts")),
|
"SPLINE Specify next point [%{n} pts]:" => Some(("draw", "spline-specify-next-point-n-pts")),
|
||||||
"SPLINEDIT Select spline:" => Some(("modify", "splinedit-select-spline")),
|
"SPLINEDIT Select spline:" => Some(("modify", "splinedit-select-spline")),
|
||||||
"SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]:" => Some(("modify", "splinedit-close-open-reverse-exit")),
|
"SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]:" => Some(("modify", "splinedit-close-open-reverse-exit")),
|
||||||
|
|
@ -2323,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}' not found." => Some(("view", "shortcut-key-not-found")),
|
||||||
"Shortcut '{key}' removed." => Some(("view", "shortcut-key-removed")),
|
"Shortcut '{key}' removed." => Some(("view", "shortcut-key-removed")),
|
||||||
"Shortcut set: {key} → {cmd_str}" => Some(("view", "shortcut-set-key-cmd-str")),
|
"Shortcut set: {key} → {cmd_str}" => Some(("view", "shortcut-set-key-cmd-str")),
|
||||||
|
"Hide" => Some(("properties", "hide")),
|
||||||
"Show" => Some(("properties", "show")),
|
"Show" => Some(("properties", "show")),
|
||||||
"Show Annotation Objects" => Some(("ui", "show-annotation-objects")),
|
"Show Annotation Objects" => Some(("ui", "show-annotation-objects")),
|
||||||
"Show Clipping" => Some(("properties", "show-clipping")),
|
"Show Clipping" => Some(("properties", "show-clipping")),
|
||||||
|
|
@ -2368,6 +2389,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||||
"Start Tangent X" => Some(("properties", "start-tangent-x")),
|
"Start Tangent X" => Some(("properties", "start-tangent-x")),
|
||||||
"Start Tangent Y" => Some(("properties", "start-tangent-y")),
|
"Start Tangent Y" => Some(("properties", "start-tangent-y")),
|
||||||
"Start Tangent Z" => Some(("properties", "start-tangent-z")),
|
"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 X" => Some(("properties", "start-x")),
|
||||||
"Start Y" => Some(("properties", "start-y")),
|
"Start Y" => Some(("properties", "start-y")),
|
||||||
"Start Z" => Some(("properties", "start-z")),
|
"Start Z" => Some(("properties", "start-z")),
|
||||||
|
|
@ -2768,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} NEW <name>" => Some(("layers", "usage-prefix-new-name")),
|
||||||
"Usage: {prefix} OBLIQUE <style> <angle_degrees>" => Some(("layers", "usage-prefix-oblique-style-angle-degrees")),
|
"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")),
|
"Usage: {prefix} WIDTH <style> <factor>" => Some(("layers", "usage-prefix-width-style-factor")),
|
||||||
|
"Uniform" => Some(("properties", "uniform")),
|
||||||
"Use Limits" => Some(("properties", "use-limits")),
|
"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")),
|
"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")),
|
"User\nInterface" => Some(("manage", "user-interface")),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Spline tool — ribbon definition + interactive command.
|
// Spline tool — ribbon definition + interactive command.
|
||||||
//
|
//
|
||||||
// Command: SPLINE (SPL)
|
// Command: SPLINE (SPL)
|
||||||
// Click to add control points. Enter (≥2 pts) → commits EntityType::Spline.
|
// Click to add fit points. Enter (≥2 pts) → commits EntityType::Spline.
|
||||||
|
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::{EntityType, Spline};
|
use acadrust::{EntityType, Spline};
|
||||||
|
|
@ -11,7 +11,6 @@ use crate::command::{CadCommand, CmdResult};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use cadkernel::space::NurbsCurve3;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn tool() -> ToolDef {
|
pub fn tool() -> ToolDef {
|
||||||
|
|
@ -36,70 +35,43 @@ impl SplineCommand {
|
||||||
if self.pts.len() < 2 {
|
if self.pts.len() < 2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let control_points = self
|
let fit_points = self
|
||||||
.pts
|
.pts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
.map(|p| Vector3::new(p.x, p.y, p.z))
|
||||||
.collect();
|
.collect();
|
||||||
let n = self.pts.len();
|
|
||||||
// Uniform open knot vector for degree-3 clamped B-spline
|
|
||||||
let degree = 3_i32.min((n - 1) as i32);
|
|
||||||
let knots = uniform_knots(n, degree as usize);
|
|
||||||
let mut spline = Spline {
|
let mut spline = Spline {
|
||||||
degree,
|
degree: 3,
|
||||||
control_points,
|
fit_points,
|
||||||
knots,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
// Closed splines render with a segment bridging the last point back to
|
|
||||||
// the first (tessellation honours `flags.closed`).
|
|
||||||
spline.flags.closed = closed;
|
spline.flags.closed = closed;
|
||||||
Some(EntityType::Spline(spline))
|
Some(EntityType::Spline(spline))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uniform_knots(n: usize, d: usize) -> Vec<f64> {
|
/// Preview through the same fit-point representation that `build()` commits.
|
||||||
let m = n + d + 1;
|
fn sample_curve(pts: &[DVec3], closed: bool) -> Vec<[f32; 3]> {
|
||||||
(0..m)
|
if pts.len() < 2 {
|
||||||
.map(|i| {
|
|
||||||
if i <= d {
|
|
||||||
0.0
|
|
||||||
} else if i >= m - d - 1 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
(i - d) as f64 / (n - d) as f64
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sample the degree-3 clamped B-spline through `pts` into a dense polyline,
|
|
||||||
/// built with the same control points + knot vector `build()` commits so the
|
|
||||||
/// preview traces the exact final curve — not the straight control polygon.
|
|
||||||
/// Fewer than 2 points has no curve; the points are returned as-is.
|
|
||||||
fn sample_curve(pts: &[DVec3]) -> Vec<[f32; 3]> {
|
|
||||||
let n = pts.len();
|
|
||||||
if n < 2 {
|
|
||||||
return pts
|
return pts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
let degree = 3_usize.min(n - 1);
|
let mut spline = Spline {
|
||||||
let ctrl: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
degree: 3,
|
||||||
let Some(curve) = NurbsCurve3::new(degree, ctrl, uniform_knots(n, degree), None) else {
|
fit_points: pts
|
||||||
return pts
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
.map(|p| Vector3::new(p.x, p.y, p.z))
|
||||||
.collect();
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
// Resolution scales with span count so long splines stay smooth.
|
spline.flags.closed = closed;
|
||||||
let steps = 24 * (n - 1);
|
crate::entities::curve::spline_curve(&spline)
|
||||||
(0..=steps)
|
.map(|curve| crate::entities::curve::curve_points(&curve))
|
||||||
.map(|i| {
|
.unwrap_or_else(|| crate::entities::spline::measurement_polyline(&spline))
|
||||||
let p = curve.point_at(i as f64 / steps as f64);
|
.into_iter()
|
||||||
[p[0] as f32, p[1] as f32, p[2] as f32]
|
.map(|p| [p[0] as f32, p[1] as f32, p[2] as f32])
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,7 +82,7 @@ impl CadCommand for SplineCommand {
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if self.pts.is_empty() {
|
if self.pts.is_empty() {
|
||||||
t!("SPLINE Specify first control point:").into_owned()
|
t!("SPLINE Specify first point:").into_owned()
|
||||||
} else {
|
} else {
|
||||||
let n = self.pts.len();
|
let n = self.pts.len();
|
||||||
t!("SPLINE Specify next point [%{n} pts]:", n = n).into_owned()
|
t!("SPLINE Specify next point [%{n} pts]:", n = n).into_owned()
|
||||||
|
|
@ -135,9 +107,6 @@ impl CadCommand for SplineCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
// A spline gathers many points into ONE entity, so finishing must end
|
|
||||||
// the command (CommitAndExit) — CommitEntity keeps the command armed
|
|
||||||
// with the accumulated points and traps the cursor in a redraw loop.
|
|
||||||
match self.build(false) {
|
match self.build(false) {
|
||||||
Some(e) => CmdResult::CommitAndExit(e),
|
Some(e) => CmdResult::CommitAndExit(e),
|
||||||
None => CmdResult::Cancel,
|
None => CmdResult::Cancel,
|
||||||
|
|
@ -156,7 +125,6 @@ impl CadCommand for SplineCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
// Accept Close / Undo once a control point exists.
|
|
||||||
!self.pts.is_empty()
|
!self.pts.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,14 +150,12 @@ impl CadCommand for SplineCommand {
|
||||||
if self.pts.is_empty() {
|
if self.pts.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Preview the actual B-spline curve, treating the cursor as the next
|
// Preview the committed fit-point representation.
|
||||||
// control point, sampled exactly like the committed entity — not the
|
|
||||||
// straight control polygon.
|
|
||||||
let mut ctrl = self.pts.clone();
|
let mut ctrl = self.pts.clone();
|
||||||
ctrl.push(pt);
|
ctrl.push(pt);
|
||||||
Some(WireModel::solid(
|
Some(WireModel::solid(
|
||||||
"rubber_band".into(),
|
"rubber_band".into(),
|
||||||
sample_curve(&ctrl),
|
sample_curve(&ctrl, false),
|
||||||
WireModel::CYAN,
|
WireModel::CYAN,
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -96,15 +96,18 @@ fn spline_to_nurbs_with(
|
||||||
let flat = vector(v);
|
let flat = vector(v);
|
||||||
(flat[0] * flat[0] + flat[1] * flat[1] > 1e-18).then_some(flat)
|
(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,
|
||||||
|
};
|
||||||
NurbsCurve::interpolate(
|
NurbsCurve::interpolate(
|
||||||
&fit,
|
&fit,
|
||||||
tangent(&spl.begin_tangent),
|
start_tangent,
|
||||||
tangent(&spl.end_tangent),
|
end_tangent,
|
||||||
match spl.knot_parameterization {
|
parameterization,
|
||||||
2 => Parameterization::Uniform,
|
|
||||||
1 => Parameterization::Centripetal,
|
|
||||||
_ => Parameterization::Chord,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ pub enum GripShape {
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Triangle,
|
Triangle,
|
||||||
Circle,
|
Circle,
|
||||||
|
/// Screen-offset menu selector.
|
||||||
|
Dropdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describes one grip point for an entity.
|
/// Describes one grip point for an entity.
|
||||||
|
|
@ -163,6 +165,8 @@ pub enum GripMenuAction {
|
||||||
RemoveFitPoint,
|
RemoveFitPoint,
|
||||||
Refit,
|
Refit,
|
||||||
RefineVertices,
|
RefineVertices,
|
||||||
|
ShowFit,
|
||||||
|
ShowControlVertices,
|
||||||
MoveWithText,
|
MoveWithText,
|
||||||
StackText,
|
StackText,
|
||||||
UnstackText,
|
UnstackText,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,17 @@ use crate::scene::model::object::{GripDef, GripShape};
|
||||||
pub const GRIP_THRESHOLD_PX: f32 = 8.0;
|
pub const GRIP_THRESHOLD_PX: f32 = 8.0;
|
||||||
/// Half-size of the rendered grip square / diamond in pixels.
|
/// Half-size of the rendered grip square / diamond in pixels.
|
||||||
pub const GRIP_HALF_PX: f32 = 5.0;
|
pub const GRIP_HALF_PX: f32 = 5.0;
|
||||||
|
/// Screen offset for dropdown selectors anchored to a vertex.
|
||||||
|
pub const GRIP_DROPDOWN_OFFSET_X_PX: f32 = 24.0;
|
||||||
|
pub const GRIP_DROPDOWN_OFFSET_Y_PX: f32 = 27.0;
|
||||||
|
|
||||||
|
fn marker_screen_position(mut point: Point, shape: GripShape) -> Point {
|
||||||
|
if shape == GripShape::Dropdown {
|
||||||
|
point.x += GRIP_DROPDOWN_OFFSET_X_PX;
|
||||||
|
point.y += GRIP_DROPDOWN_OFFSET_Y_PX;
|
||||||
|
}
|
||||||
|
point
|
||||||
|
}
|
||||||
|
|
||||||
// ── Active drag state ─────────────────────────────────────────────────────
|
// ── Active drag state ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -106,6 +117,7 @@ pub fn grips_to_screen(
|
||||||
camera.project(g.world + dir, bounds)?,
|
camera.project(g.world + dir, bounds)?,
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
let screen = marker_screen_position(screen, g.shape);
|
||||||
(g.id, screen, g.is_midpoint, g.shape, dir)
|
(g.id, screen, g.is_midpoint, g.shape, dir)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -131,7 +143,7 @@ pub fn grips_to_screen_paper(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| {
|
.map(|g| {
|
||||||
let projected = project(g.world);
|
let projected = project(g.world);
|
||||||
let screen = Point::new(projected.x, projected.y);
|
let screen = marker_screen_position(Point::new(projected.x, projected.y), g.shape);
|
||||||
let dir = g.dir.map(|dir| {
|
let dir = g.dir.map(|dir| {
|
||||||
[
|
[
|
||||||
dir.x as f32 / (2.0 * half_w) * bounds.width,
|
dir.x as f32 / (2.0 * half_w) * bounds.width,
|
||||||
|
|
@ -157,9 +169,12 @@ pub fn find_hit_grip_paper(
|
||||||
let mut best: Option<(usize, usize, bool, DVec3)> = None;
|
let mut best: Option<(usize, usize, bool, DVec3)> = None;
|
||||||
|
|
||||||
for (index, g) in grips.iter().enumerate() {
|
for (index, g) in grips.iter().enumerate() {
|
||||||
let screen = Point::new(
|
let screen = marker_screen_position(
|
||||||
(g.world.x as f32 - tx + half_w) / (2.0 * half_w) * bounds.width,
|
Point::new(
|
||||||
(ty + half_h - g.world.y as f32) / (2.0 * half_h) * bounds.height,
|
(g.world.x as f32 - tx + half_w) / (2.0 * half_w) * bounds.width,
|
||||||
|
(ty + half_h - g.world.y as f32) / (2.0 * half_h) * bounds.height,
|
||||||
|
),
|
||||||
|
g.shape,
|
||||||
);
|
);
|
||||||
let dx = screen.x - cursor.x;
|
let dx = screen.x - cursor.x;
|
||||||
let dy = screen.y - cursor.y;
|
let dy = screen.y - cursor.y;
|
||||||
|
|
@ -187,7 +202,7 @@ pub fn find_hit_grip(
|
||||||
let Some(screen) = camera.project(g.world, bounds) else {
|
let Some(screen) = camera.project(g.world, bounds) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let screen = Point::new(screen.x, screen.y);
|
let screen = marker_screen_position(Point::new(screen.x, screen.y), g.shape);
|
||||||
let dx = screen.x - cursor.x;
|
let dx = screen.x - cursor.x;
|
||||||
let dy = screen.y - cursor.y;
|
let dy = screen.y - cursor.y;
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
|
|
@ -239,6 +254,7 @@ pub fn grips_to_screen_rte(
|
||||||
project_rte(g.world + dir, view_rot, eye, bounds)?,
|
project_rte(g.world + dir, view_rot, eye, bounds)?,
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
let screen = marker_screen_position(screen, g.shape);
|
||||||
(g.id, screen, g.is_midpoint, g.shape, dir)
|
(g.id, screen, g.is_midpoint, g.shape, dir)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -260,6 +276,7 @@ pub fn find_hit_grip_rte(
|
||||||
let Some(screen) = project_rte(g.world, view_rot, eye, bounds) else {
|
let Some(screen) = project_rte(g.world, view_rot, eye, bounds) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let screen = marker_screen_position(Point::new(screen.x, screen.y), g.shape);
|
||||||
let dx = screen.x - cursor.x;
|
let dx = screen.x - cursor.x;
|
||||||
let dy = screen.y - cursor.y;
|
let dy = screen.y - cursor.y;
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,7 @@ pub fn selection_overlay<'a>(
|
||||||
snap_ext_base: Option<Point>,
|
snap_ext_base: Option<Point>,
|
||||||
snap_ext_base2: Option<Point>,
|
snap_ext_base2: Option<Point>,
|
||||||
grips: Vec<GripMarker>,
|
grips: Vec<GripMarker>,
|
||||||
|
control_polygon: Option<(Vec<Point>, bool)>,
|
||||||
grip_clip: Option<iced::Rectangle>,
|
grip_clip: Option<iced::Rectangle>,
|
||||||
ucs_icons: Vec<UcsIconParams>,
|
ucs_icons: Vec<UcsIconParams>,
|
||||||
ost_points: Vec<OstTrackPoint>,
|
ost_points: Vec<OstTrackPoint>,
|
||||||
|
|
@ -282,6 +283,7 @@ pub fn selection_overlay<'a>(
|
||||||
snap_ext_base,
|
snap_ext_base,
|
||||||
snap_ext_base2,
|
snap_ext_base2,
|
||||||
grips,
|
grips,
|
||||||
|
control_polygon,
|
||||||
grip_clip,
|
grip_clip,
|
||||||
ucs_icons,
|
ucs_icons,
|
||||||
ost_points,
|
ost_points,
|
||||||
|
|
@ -312,6 +314,8 @@ struct SelectionCanvas {
|
||||||
/// both crossing extensions stay drawn when the crossing is caught. (#247, #259)
|
/// both crossing extensions stay drawn when the crossing is caught. (#247, #259)
|
||||||
snap_ext_base2: Option<Point>,
|
snap_ext_base2: Option<Point>,
|
||||||
grips: Vec<GripMarker>,
|
grips: Vec<GripMarker>,
|
||||||
|
/// Selected spline control polygon and closed state.
|
||||||
|
control_polygon: Option<(Vec<Point>, bool)>,
|
||||||
/// Active 3D pane / floating viewport rectangle. Grip markers are clipped
|
/// Active 3D pane / floating viewport rectangle. Grip markers are clipped
|
||||||
/// here so orbiting cannot leak them into paper space or adjacent panes.
|
/// here so orbiting cannot leak them into paper space or adjacent panes.
|
||||||
grip_clip: Option<iced::Rectangle>,
|
grip_clip: Option<iced::Rectangle>,
|
||||||
|
|
@ -401,6 +405,12 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
|
||||||
b.close();
|
b.close();
|
||||||
}),
|
}),
|
||||||
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),
|
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),
|
||||||
|
GripShape::Dropdown => canvas::Path::new(|b| {
|
||||||
|
b.move_to(Point::new(sp.x - h, sp.y - h * 0.5));
|
||||||
|
b.line_to(Point::new(sp.x + h, sp.y - h * 0.5));
|
||||||
|
b.line_to(Point::new(sp.x, sp.y + h));
|
||||||
|
b.close();
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if grip.is_hot {
|
if grip.is_hot {
|
||||||
|
|
@ -419,10 +429,12 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
|
||||||
} else {
|
} else {
|
||||||
let palette = theme.palette();
|
let palette = theme.palette();
|
||||||
let color = palette.primary.base.color;
|
let color = palette.primary.base.color;
|
||||||
frame.fill(
|
let fill = if grip.shape == GripShape::Dropdown {
|
||||||
&path,
|
color
|
||||||
palette.background.base.color.scale_alpha(0.7),
|
} else {
|
||||||
);
|
palette.background.base.color.scale_alpha(0.7)
|
||||||
|
};
|
||||||
|
frame.fill(&path, fill);
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&path,
|
&path,
|
||||||
canvas::Stroke {
|
canvas::Stroke {
|
||||||
|
|
@ -695,6 +707,31 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
height: grip_y1 - grip_y0,
|
height: grip_y1 - grip_y0,
|
||||||
};
|
};
|
||||||
frame.with_clip(grip_clip, |frame| {
|
frame.with_clip(grip_clip, |frame| {
|
||||||
|
if let Some((points, closed)) = &self.control_polygon {
|
||||||
|
if points.len() >= 2 {
|
||||||
|
let polygon = canvas::Path::new(|builder| {
|
||||||
|
builder.move_to(points[0]);
|
||||||
|
for point in points.iter().skip(1) {
|
||||||
|
builder.line_to(*point);
|
||||||
|
}
|
||||||
|
if *closed {
|
||||||
|
builder.line_to(points[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
frame.stroke(
|
||||||
|
&polygon,
|
||||||
|
canvas::Stroke {
|
||||||
|
width: 1.0,
|
||||||
|
style: canvas::Style::Solid(Color::from_rgb(0.72, 0.32, 0.15)),
|
||||||
|
line_dash: canvas::LineDash {
|
||||||
|
segments: &[4.0, 4.0],
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
for grip in &self.grips {
|
for grip in &self.grips {
|
||||||
draw_grip_marker(frame, grip, theme);
|
draw_grip_marker(frame, grip, theme);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue