feat(ellipse): add editable arc endpoint grips

This commit is contained in:
ramox81 committed 2026-08-19 10:02:38 +03:00
1 parent 3f9c3a9e9a
commit 283e6db324
3 files changed
+100 -20

No files matched your search

+14
View File
@@ -479,6 +479,20 @@ pub fn triangle_grip(id: usize, world: glam::DVec3) -> GripDef {
}
}
/// Directional triangle grip. `dir` is a world-XY direction and points toward
/// the triangle tip. Existing non-directional triangle grips keep their
/// screen-up orientation through [`triangle_grip`].
pub fn oriented_triangle_grip(id: usize, world: glam::DVec3, dir: [f32; 2]) -> GripDef {
GripDef {
id,
world,
is_midpoint: false,
shape: GripShape::Triangle,
dir: Some(dir),
axis: None,
}
}
/// Editable ANGLE row: displays via AUNITS/AUPREC. Angle rows used the
/// LINEAR formatter, so LUNITS=Architectural showed a block rotation as
/// feet-and-inches and the string wouldn't parse back (#297). Value in
+72 -17
View File
@@ -5,7 +5,8 @@ use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
center_grip, edit_prop as edit, oriented_triangle_grip, parse_f64, ro_prop as ro,
square_grip,
};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
@@ -64,26 +65,72 @@ fn to_render(ell: &Ellipse) -> RenderEntity {
/// hand-built sampling produced, without restating how a B-spline is put
/// together at each site.
fn grips(ell: &Ellipse) -> Vec<GripDef> {
fn axis_vectors(ell: &Ellipse) -> (glam::DVec3, glam::DVec3, glam::DVec3) {
let ctr = glam::DVec3::new(ell.center.x, ell.center.y, ell.center.z);
let major = glam::DVec3::new(ell.major_axis.x, ell.major_axis.y, ell.major_axis.z);
let major_xy =
((ell.major_axis.x * ell.major_axis.x + ell.major_axis.y * ell.major_axis.y) as f64).sqrt();
let (px, py) = if major_xy > 1e-10 {
let s = ell.major_axis_length() * ell.minor_axis_ratio / major_xy;
(-ell.major_axis.y * s, ell.major_axis.x * s)
} else {
(0.0, ell.major_axis_length() * ell.minor_axis_ratio)
};
let minor = glam::DVec3::new(px, py, 0.0);
let major_dir = major.normalize_or(glam::DVec3::X);
let normal = glam::DVec3::new(ell.normal.x, ell.normal.y, ell.normal.z)
.normalize_or(glam::DVec3::Z);
let fallback_minor = glam::DVec3::Z
.cross(major_dir)
.normalize_or(glam::DVec3::Y);
let minor_dir = normal.cross(major_dir).normalize_or(fallback_minor);
let minor = minor_dir * ell.minor_axis_length();
(ctr, major, minor)
}
fn ellipse_point(
ctr: glam::DVec3,
major: glam::DVec3,
minor: glam::DVec3,
t: f64,
) -> glam::DVec3 {
ctr + major * t.cos() + minor * t.sin()
}
fn is_closed(ell: &Ellipse) -> bool {
crate::entities::curve::ellipse_curve(ell).is_some_and(|curve| curve.curve.is_closed())
}
fn parameter_at_point(ell: &Ellipse, point: glam::DVec3) -> Option<f64> {
let (ctr, major, minor) = axis_vectors(ell);
let major_len = major.length();
let minor_len = minor.length();
if major_len <= 1e-10 || minor_len <= 1e-10 {
return None;
}
let delta = point - ctr;
let along_major = delta.dot(major / major_len) / major_len;
let along_minor = delta.dot(minor / minor_len) / minor_len;
Some(along_minor.atan2(along_major).rem_euclid(std::f64::consts::TAU))
}
fn grips(ell: &Ellipse) -> Vec<GripDef> {
let (ctr, major, minor) = axis_vectors(ell);
let mut grips = vec![
center_grip(0, ctr),
square_grip(1, ctr + major),
square_grip(2, ctr + minor),
square_grip(3, ctr - major),
square_grip(4, ctr - minor),
];
if ell.is_full() {
grips.push(square_grip(3, ctr - major));
grips.push(square_grip(4, ctr - minor));
if !is_closed(ell) {
let start_t = ell.start_parameter;
let end_t = ell.end_parameter;
let start = ellipse_point(ctr, major, minor, start_t);
let end = ellipse_point(ctr, major, minor, end_t);
let start_outward = major * start_t.sin() - minor * start_t.cos();
let end_outward = -major * end_t.sin() + minor * end_t.cos();
grips.push(oriented_triangle_grip(
5,
start,
[start_outward.x as f32, start_outward.y as f32],
));
grips.push(oriented_triangle_grip(
6,
end,
[end_outward.x as f32, end_outward.y as f32],
));
}
grips
}
@@ -115,9 +162,7 @@ fn properties(ell: &Ellipse) -> Vec<PropSection> {
let minor_vec = v * r_minor;
let start_angle = ell.start_parameter.to_degrees().rem_euclid(360.0);
let end_angle = if crate::entities::curve::ellipse_curve(ell)
.is_some_and(|curve| curve.curve.is_closed())
{
let end_angle = if is_closed(ell) {
360.0
} else {
ell.end_parameter.to_degrees().rem_euclid(360.0)
@@ -261,6 +306,16 @@ fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
ell.minor_axis_ratio = (dragged_len / fixed_len).clamp(0.001, 1.0);
}
}
(5 | 6, GripApply::Absolute(p)) => {
let Some(parameter) = parameter_at_point(ell, p) else {
return;
};
if grip_id == 5 {
ell.start_parameter = parameter;
} else {
ell.end_parameter = parameter;
}
}
_ => {}
}
}
+14 -3
View File
@@ -386,9 +386,20 @@ fn draw_grip_marker(frame: &mut canvas::Frame, grip: &GripMarker, theme: &Theme)
})
}
GripShape::Triangle => canvas::Path::new(|b| {
b.move_to(Point::new(sp.x, sp.y - h));
b.line_to(Point::new(sp.x + h, sp.y + h));
b.line_to(Point::new(sp.x - h, sp.y + h));
let (forward_x, forward_y) = match grip.dir {
Some([dx, dy]) if (dx * dx + dy * dy) > 1e-12 => {
let length = (dx * dx + dy * dy).sqrt();
(dx / length, -dy / length)
}
_ => (0.0, -1.0),
};
let side_x = -forward_y;
let side_y = forward_x;
let tip = Point::new(sp.x + forward_x * h, sp.y + forward_y * h);
let base = Point::new(sp.x - forward_x * h, sp.y - forward_y * h);
b.move_to(tip);
b.line_to(Point::new(base.x + side_x * h, base.y + side_y * h));
b.line_to(Point::new(base.x - side_x * h, base.y - side_y * h));
b.close();
}),
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),