Improve spline fit and control vertex grips

This commit is contained in:
ramox81 committed 2026-08-19 11:02:32 +03:00
1 parent 97a6322e2a
commit 0d37d3ff78
13 files changed
+471 -80

No files matched your search

+1
View File
@@ -1357,6 +1357,7 @@ draw =
.solid-specify-second-point = SOLID Specify second point:
.solid-specify-third-point = SOLID Specify third 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]:
.sketch = Sketch
.start-center-angle = Start, Center, Angle
+1
View File
@@ -1339,6 +1339,7 @@ draw =
.solid-specify-second-point = SOLID İkinci noktayı belirtin:
.solid-specify-third-point = SOLID Üçüncü noktayı 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]:
.sketch = Serbest Çizim
.start-center-angle = Başlangıç, Merkez, Açı
+4
View File
@@ -82,6 +82,10 @@ pub struct GripPopup {
pub anchor: iced::Point,
pub items: Vec<crate::scene::model::object::GripMenuItem>,
pub selected: usize,
/// Click-opened selectors stay visible while the cursor travels from the
/// arrow to the menu. Hover-opened multifunction grips still dismiss by
/// distance as before.
pub pinned: bool,
}
/// Pending follow-up value for grip-menu actions that need a number
+36 -3
View File
@@ -653,7 +653,9 @@ impl OpenCADStudio {
screen: p,
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() {
h.screen = p;
}
@@ -672,12 +674,17 @@ impl OpenCADStudio {
use crate::entities::traits::EntityTypeOps;
let items = e.grip_menu(grip_id);
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,
items,
selected: 0,
selected,
pinned: false,
});
}
}
@@ -685,7 +692,7 @@ impl OpenCADStudio {
}
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 dy = p.y - popup.anchor.y;
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 {
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
// dropdown instead of starting a stretch drag.
if grip_id == crate::app::visibility::VIS_GRIP_ID {
+18
View File
@@ -495,6 +495,23 @@ impl OpenCADStudio {
} else {
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,
};
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() {
None
} else {
@@ -688,6 +705,7 @@ impl OpenCADStudio {
snap_ext_base,
snap_ext_base2,
grips,
control_polygon,
grip_clip,
ucs_icons,
ost_points,
+25
View File
@@ -453,6 +453,31 @@ pub fn circle_grip(id: usize, world: glam::DVec3) -> GripDef {
}
}
/// Round vertex grip. Unlike [`circle_grip`], dragging this handle edits one
/// point rather than translating the whole entity.
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
/// world-XY direction). Drawn as a small rectangle elongated along the
/// segment so the affordance reads as "stretch perpendicular".
+112 -19
View File
@@ -3,7 +3,9 @@ use crate::t;
use cadkernel::space::NurbsCurve3;
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::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
@@ -390,27 +392,60 @@ fn thomas_solve(a: &[f64], b: &[f64], c: &[f64], d: &mut [f64]) {
}
}
pub(crate) const SPLINE_MODE_GRIP_ID: usize = usize::MAX - 1;
fn control_vertices(spline: &Spline) -> Vec<acadrust::types::Vector3> {
if !spline.control_points.is_empty() {
return spline.control_points.clone();
}
let elevation = spline.fit_points.first().map(|p| p.z).unwrap_or(0.0);
crate::modules::draw::modify::spline_ops::spline_control_curve(spline)
.map(|curve| {
curve
.control_points()
.iter()
.map(|p| acadrust::types::Vector3::new(p[0], p[1], elevation))
.collect()
})
.unwrap_or_default()
}
fn grips(spline: &Spline) -> Vec<GripDef> {
// 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
// grips its control vertices.
let source = if spline.control_points.is_empty() {
&spline.fit_points
let (source, show_control_vertices) = if spline.cv_frame_visible {
(control_vertices(spline), true)
} else if !spline.fit_points.is_empty() {
(spline.fit_points.clone(), false)
} else {
&spline.control_points
(spline.control_points.clone(), true)
};
source
let mut grips: Vec<_> = source
.iter()
.enumerate()
.map(|(i, p)| square_grip(i, glam::DVec3::new(p.x, p.y, p.z)))
.collect()
.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 !spline.fit_points.is_empty() {
if let Some(first) = source.first() {
grips.push(dropdown_grip(
SPLINE_MODE_GRIP_ID,
glam::DVec3::new(first.x, first.y, first.z),
));
}
}
grips
}
fn properties(spline: &Spline) -> Vec<PropSection> {
let show = if spline.fit_points.is_empty() {
let show = if spline.cv_frame_visible || spline.fit_points.is_empty() {
"Control Vertices"
} else {
"Fit Points"
"Fit"
};
let cp0 = spline.control_points.first();
let fp0 = spline.fit_points.first();
@@ -566,13 +601,35 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) {
}
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
// point (the curve re-fits through it on the next tessellation, keeping the
// stored end tangents); a CV spline moves the control vertex.
let target = if spline.control_points.is_empty() {
spline.fit_points.get_mut(grip_id)
} else {
if grip_id == SPLINE_MODE_GRIP_ID {
return;
}
// A fit spline can display its derived control polygon without changing
// storage. The first actual CV edit converts that exact derived curve to
// control-point storage so the dragged vertex remains authoritative when
// the drawing is saved.
if spline.cv_frame_visible && spline.control_points.is_empty() {
let elevation = spline.fit_points.first().map(|p| p.z).unwrap_or(0.0);
let Some(curve) = crate::modules::draw::modify::spline_ops::spline_control_curve(spline)
else {
return;
};
spline.degree = curve.degree() as i32;
spline.knots = curve.knots().to_vec();
spline.control_points = curve
.control_points()
.iter()
.map(|p| acadrust::types::Vector3::new(p[0], p[1], elevation))
.collect();
spline.weights = curve.is_rational().then(|| curve.weights().to_vec()).unwrap_or_default();
spline.fit_points.clear();
spline.begin_tangent = acadrust::types::Vector3::ZERO;
spline.end_tangent = acadrust::types::Vector3::ZERO;
}
let target = if spline.cv_frame_visible || spline.fit_points.is_empty() {
spline.control_points.get_mut(grip_id)
} else {
spline.fit_points.get_mut(grip_id)
};
if let Some(cp) = target {
match apply {
@@ -614,8 +671,33 @@ impl crate::entities::traits::Grippable for Spline {
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
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};
if grip_id == SPLINE_MODE_GRIP_ID {
return if self.cv_frame_visible {
vec![
GripMenuItem {
label: "Fit",
action: GripMenuAction::ShowFit,
},
GripMenuItem {
label: "✓ Control Vertices",
action: GripMenuAction::ShowControlVertices,
},
]
} else {
vec![
GripMenuItem {
label: "✓ Fit",
action: GripMenuAction::ShowFit,
},
GripMenuItem {
label: "Control Vertices",
action: GripMenuAction::ShowControlVertices,
},
]
};
}
vec![
GripMenuItem {
label: "Stretch",
@@ -641,6 +723,17 @@ impl crate::entities::traits::Grippable for Spline {
action: crate::scene::model::object::GripMenuAction,
) {
use crate::scene::model::object::GripMenuAction as A;
match action {
A::ShowFit if !self.fit_points.is_empty() => {
self.cv_frame_visible = false;
return;
}
A::ShowControlVertices if control_vertices(self).len() >= 2 => {
self.cv_frame_visible = true;
return;
}
_ => {}
}
let n = self.control_points.len();
let min_cv = (self.degree as usize).saturating_add(1).max(2);
match action {
+1
View File
@@ -2192,6 +2192,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"SOLID Specify second point:" => Some(("draw", "solid-specify-second-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 point:" => Some(("draw", "spline-specify-first-point")),
"SPLINE Specify next point [%{n} pts]:" => Some(("draw", "spline-specify-next-point-n-pts")),
"SPLINEDIT Select spline:" => Some(("modify", "splinedit-select-spline")),
"SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]:" => Some(("modify", "splinedit-close-open-reverse-exit")),
+19 -47
View File
@@ -1,7 +1,7 @@
// Spline tool — ribbon definition + interactive command.
//
// 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::{EntityType, Spline};
@@ -11,7 +11,6 @@ use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use glam::DVec3;
use cadkernel::space::NurbsCurve3;
#[allow(dead_code)]
pub fn tool() -> ToolDef {
@@ -36,19 +35,14 @@ impl SplineCommand {
if self.pts.len() < 2 {
return None;
}
let control_points = self
let fit_points = self
.pts
.iter()
.map(|p| Vector3::new(p.x, p.y, p.z))
.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 {
degree,
control_points,
knots,
degree: 3,
fit_points,
..Default::default()
};
// Closed splines render with a segment bridging the last point back to
@@ -58,48 +52,26 @@ impl SplineCommand {
}
}
fn uniform_knots(n: usize, d: usize) -> Vec<f64> {
let m = n + d + 1;
(0..m)
.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 {
/// Preview through the same fit-point representation that `build()` commits.
fn sample_curve(pts: &[DVec3], closed: bool) -> Vec<[f32; 3]> {
if pts.len() < 2 {
return pts
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
}
let degree = 3_usize.min(n - 1);
let ctrl: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
let Some(curve) = NurbsCurve3::new(degree, ctrl, uniform_knots(n, degree), None) else {
return pts
let mut spline = Spline {
degree: 3,
fit_points: pts
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
.map(|p| Vector3::new(p.x, p.y, p.z))
.collect(),
..Default::default()
};
// Resolution scales with span count so long splines stay smooth.
let steps = 24 * (n - 1);
(0..=steps)
.map(|i| {
let p = curve.point_at(i as f64 / steps as f64);
[p[0] as f32, p[1] as f32, p[2] as f32]
})
spline.flags.closed = closed;
crate::entities::spline::measurement_polyline(&spline)
.into_iter()
.map(|p| [p[0] as f32, p[1] as f32, p[2] as f32])
.collect()
}
@@ -110,7 +82,7 @@ impl CadCommand for SplineCommand {
fn prompt(&self) -> String {
if self.pts.is_empty() {
t!("SPLINE Specify first control point:").into_owned()
t!("SPLINE Specify first point:").into_owned()
} else {
let n = self.pts.len();
t!("SPLINE Specify next point [%{n} pts]:", n = n).into_owned()
@@ -189,7 +161,7 @@ impl CadCommand for SplineCommand {
ctrl.push(pt);
Some(WireModel::solid(
"rubber_band".into(),
sample_curve(&ctrl),
sample_curve(&ctrl, false),
WireModel::CYAN,
false,
))
+179
View File
@@ -47,6 +47,185 @@ pub fn spline_to_nurbs(spl: &Spline) -> Option<NurbsCurve> {
spline_to_nurbs_with(spl, |p| [p.x, p.y], |v| [v.x, v.y])
}
/// Return the compact control-polygon form of a spline for interactive grips.
///
/// The kernel's fit-point interpolator intentionally stores one Bezier span
/// per fit-point interval. That is exact and convenient for evaluation, but
/// its repeated internal knots expose three controls per interval to an
/// editor. An open cubic interpolation has an equivalent simple-knot form
/// with only `fit point count + 2` controls. Solve that form here so the grip
/// polygon stays compact without changing the curve or its saved fit points.
pub fn spline_control_curve(spl: &Spline) -> Option<NurbsCurve> {
let exact = spline_to_nurbs(spl)?;
if !spl.control_points.is_empty()
|| spl.flags.closed
|| spl.flags.periodic
|| spl.fit_points.len() < 2
{
return Some(exact);
}
let fit: Vec<[f64; 2]> = spl.fit_points.iter().map(|p| [p.x, p.y]).collect();
let parameters = fit_parameters(&fit, spl.knot_parameterization);
let control_count = fit.len() + 2;
let mut knots = vec![parameters[0]; 4];
knots.extend(parameters.iter().take(fit.len() - 1).skip(1).copied());
knots.extend([parameters[fit.len() - 1]; 4]);
let mut matrix = Vec::with_capacity(control_count);
let mut right = Vec::with_capacity(control_count);
for (&parameter, &point) in parameters.iter().zip(&fit) {
matrix.push(bspline_basis(&knots, 3, parameter));
right.push(point);
}
// Scaling the derivative equations by the domain width keeps the solve
// well conditioned for drawings whose coordinates are very large/small.
let (start, end) = exact.domain();
let width = (end - start).abs().max(1e-9);
matrix.push(
bspline_basis_derivative(&knots, 3, parameters[0])
.into_iter()
.map(|value| value * width)
.collect(),
);
let start_slope = exact.derivative_at_knot(start);
right.push([start_slope[0] * width, start_slope[1] * width]);
matrix.push(
bspline_basis_derivative(&knots, 3, parameters[fit.len() - 1])
.into_iter()
.map(|value| value * width)
.collect(),
);
let end_slope = exact.derivative_at_knot(end);
right.push([end_slope[0] * width, end_slope[1] * width]);
let controls = solve_control_points(matrix, right)?;
NurbsCurve::new(3, controls, knots, None)
}
fn fit_parameters(points: &[[f64; 2]], parameterization: i32) -> Vec<f64> {
let mut parameters = vec![0.0; points.len()];
for i in 1..points.len() {
let dx = points[i][0] - points[i - 1][0];
let dy = points[i][1] - points[i - 1][1];
let chord = (dx * dx + dy * dy).sqrt().max(1e-9);
let step = match parameterization {
2 => 1.0,
1 => chord.sqrt(),
_ => chord,
};
parameters[i] = parameters[i - 1] + step;
}
parameters
}
fn bspline_basis(knots: &[f64], degree: usize, parameter: f64) -> Vec<f64> {
let count = knots.len() - degree - 1;
if (parameter - knots[knots.len() - 1]).abs() < 1e-12 {
let mut endpoint = vec![0.0; count];
let last_span = knots
.windows(2)
.rposition(|span| span[0] < span[1])
.unwrap_or(count - 1);
endpoint[last_span.min(count - 1)] = 1.0;
return endpoint;
}
let mut values: Vec<f64> = knots
.windows(2)
.map(|span| {
if span[0] <= parameter && parameter < span[1] {
1.0
} else {
0.0
}
})
.collect();
for order in 1..=degree {
let next_count = knots.len() - order - 1;
let mut next = vec![0.0; next_count];
for i in 0..next_count {
let left_span = knots[i + order] - knots[i];
let right_span = knots[i + order + 1] - knots[i + 1];
let left = if left_span.abs() < 1e-15 {
0.0
} else {
(parameter - knots[i]) / left_span * values[i]
};
let right = if right_span.abs() < 1e-15 {
0.0
} else {
(knots[i + order + 1] - parameter) / right_span * values[i + 1]
};
next[i] = left + right;
}
values = next;
}
values
}
fn bspline_basis_derivative(knots: &[f64], degree: usize, parameter: f64) -> Vec<f64> {
let lower = bspline_basis(knots, degree - 1, parameter);
let count = knots.len() - degree - 1;
(0..count)
.map(|i| {
let left_span = knots[i + degree] - knots[i];
let right_span = knots[i + degree + 1] - knots[i + 1];
let left = if left_span.abs() < 1e-15 {
0.0
} else {
degree as f64 / left_span * lower[i]
};
let right = if right_span.abs() < 1e-15 {
0.0
} else {
degree as f64 / right_span * lower[i + 1]
};
left - right
})
.collect()
}
fn solve_control_points(
mut matrix: Vec<Vec<f64>>,
mut right: Vec<[f64; 2]>,
) -> Option<Vec<[f64; 2]>> {
let count = right.len();
for column in 0..count {
let pivot = (column..count).max_by(|&a, &b| {
matrix[a][column]
.abs()
.total_cmp(&matrix[b][column].abs())
})?;
if matrix[pivot][column].abs() < 1e-14 {
return None;
}
matrix.swap(column, pivot);
right.swap(column, pivot);
for row in column + 1..count {
let factor = matrix[row][column] / matrix[column][column];
for col in column..count {
matrix[row][col] -= factor * matrix[column][col];
}
right[row][0] -= factor * right[column][0];
right[row][1] -= factor * right[column][1];
}
}
let mut result = vec![[0.0; 2]; count];
for row in (0..count).rev() {
for axis in 0..2 {
let known: f64 = (row + 1..count)
.map(|column| matrix[row][column] * result[column][axis])
.sum();
result[row][axis] = (right[row][axis] - known) / matrix[row][row];
}
}
Some(result)
}
/// [`spline_to_nurbs`] with the points expressed in `plane`'s coordinates.
///
/// The two differ only for a spline whose extrusion normal is not +Z. Where
+5
View File
@@ -83,6 +83,9 @@ pub enum GripShape {
Rectangle,
Triangle,
Circle,
/// Small down-arrow selector drawn at a fixed screen offset from its
/// world anchor. Clicking it opens the grip's choice menu.
Dropdown,
}
/// Describes one grip point for an entity.
@@ -165,6 +168,8 @@ pub enum GripMenuAction {
RemoveFitPoint,
Refit,
RefineVertices,
ShowFit,
ShowControlVertices,
MoveWithText,
StackText,
UnstackText,
+29 -7
View File
@@ -10,6 +10,19 @@ use crate::scene::model::object::{GripDef, GripShape};
pub const GRIP_THRESHOLD_PX: f32 = 8.0;
/// Half-size of the rendered grip square / diamond in pixels.
pub const GRIP_HALF_PX: f32 = 5.0;
/// Screen offset for dropdown selectors anchored to a vertex. Keeping this
/// asymmetric places the arrow below and just to the right of the endpoint,
/// clear of both the curve and its ordinary edit grip.
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 ─────────────────────────────────────────────────────
@@ -95,6 +108,7 @@ pub fn grips_to_screen(
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y),
None => Point::new(f32::NAN, f32::NAN),
};
let screen = marker_screen_position(screen, g.shape);
(g.id, screen, g.is_midpoint, g.shape, g.dir)
})
.collect()
@@ -113,9 +127,12 @@ pub fn grips_to_screen_paper(
grips
.iter()
.map(|g| {
let screen = Point::new(
(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,
let screen = marker_screen_position(
Point::new(
(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,
);
(g.id, screen, g.is_midpoint, g.shape, g.dir)
})
@@ -136,9 +153,12 @@ pub fn find_hit_grip_paper(
let mut best: Option<(usize, usize, bool, DVec3)> = None;
for (index, g) in grips.iter().enumerate() {
let screen = Point::new(
(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,
let screen = marker_screen_position(
Point::new(
(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 dy = screen.y - cursor.y;
@@ -166,7 +186,7 @@ pub fn find_hit_grip(
let Some(screen) = camera.project(g.world, bounds) else {
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 dy = screen.y - cursor.y;
let d = (dx * dx + dy * dy).sqrt();
@@ -211,6 +231,7 @@ pub fn grips_to_screen_rte(
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y),
None => Point::new(f32::NAN, f32::NAN),
};
let screen = marker_screen_position(screen, g.shape);
(g.id, screen, g.is_midpoint, g.shape, g.dir)
})
.collect()
@@ -232,6 +253,7 @@ pub fn find_hit_grip_rte(
let Some(screen) = project_rte(g.world, view_rot, eye, bounds) else {
continue;
};
let screen = marker_screen_position(Point::new(screen.x, screen.y), g.shape);
let dx = screen.x - cursor.x;
let dy = screen.y - cursor.y;
let d = (dx * dx + dy * dy).sqrt();
+41 -4
View File
@@ -263,6 +263,7 @@ pub fn selection_overlay<'a>(
snap_ext_base: Option<Point>,
snap_ext_base2: Option<Point>,
grips: Vec<GripMarker>,
control_polygon: Option<(Vec<Point>, bool)>,
grip_clip: Option<iced::Rectangle>,
ucs_icons: Vec<UcsIconParams>,
ost_points: Vec<OstTrackPoint>,
@@ -284,6 +285,7 @@ pub fn selection_overlay<'a>(
snap_ext_base,
snap_ext_base2,
grips,
control_polygon,
grip_clip,
ucs_icons,
ost_points,
@@ -314,6 +316,8 @@ struct SelectionCanvas {
/// both crossing extensions stay drawn when the crossing is caught. (#247, #259)
snap_ext_base2: Option<Point>,
grips: Vec<GripMarker>,
/// Selected spline's screen-space control polygon and whether it closes.
control_polygon: Option<(Vec<Point>, bool)>,
/// Active 3D pane / floating viewport rectangle. Grip markers are clipped
/// here so orbiting cannot leak them into paper space or adjacent panes.
grip_clip: Option<iced::Rectangle>,
@@ -392,6 +396,12 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
b.close();
}),
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 {
@@ -410,10 +420,12 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
} else {
let palette = theme.palette();
let color = palette.primary.base.color;
frame.fill(
&path,
palette.background.base.color.scale_alpha(0.7),
);
let fill = if grip.shape == GripShape::Dropdown {
color
} else {
palette.background.base.color.scale_alpha(0.7)
};
frame.fill(&path, fill);
frame.stroke(
&path,
canvas::Stroke {
@@ -686,6 +698,31 @@ impl canvas::Program<Message> for SelectionCanvas {
height: grip_y1 - grip_y0,
};
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 {
draw_grip_marker(frame, grip, theme);
}