From 3f9c3a9e9ae9fe434c9bd071a372f295e93da8c6 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:28:18 +0300 Subject: [PATCH 1/2] fix(ellipse): add symmetric axis grips --- src/entities/ellipse.rs | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 144c0fce1..ad838b884 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -66,11 +66,7 @@ fn to_render(ell: &Ellipse) -> RenderEntity { fn grips(ell: &Ellipse) -> Vec { let ctr = glam::DVec3::new(ell.center.x, ell.center.y, ell.center.z); - let maj = glam::DVec3::new( - ell.center.x + ell.major_axis.x, - ell.center.y + ell.major_axis.y, - ell.center.z + ell.major_axis.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 { @@ -79,12 +75,17 @@ fn grips(ell: &Ellipse) -> Vec { } else { (0.0, ell.major_axis_length() * ell.minor_axis_ratio) }; - let min = glam::DVec3::new(ell.center.x + px, ell.center.y + py, ell.center.z); - vec![ + let minor = glam::DVec3::new(px, py, 0.0); + let mut grips = vec![ center_grip(0, ctr), - square_grip(1, maj), - square_grip(2, min), - ] + square_grip(1, ctr + major), + square_grip(2, ctr + minor), + ]; + if ell.is_full() { + grips.push(square_grip(3, ctr - major)); + grips.push(square_grip(4, ctr - minor)); + } + grips } fn properties(ell: &Ellipse) -> Vec { @@ -200,12 +201,22 @@ fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) { ell.center.y = p.y as f64; ell.center.z = p.z as f64; } - (1, GripApply::Absolute(p)) => { - ell.major_axis.x = p.x as f64 - ell.center.x; - ell.major_axis.y = p.y as f64 - ell.center.y; - ell.major_axis.z = p.z as f64 - ell.center.z; + (1 | 3, GripApply::Absolute(p)) => { + let direction = if grip_id == 1 { 1.0 } else { -1.0 }; + ell.major_axis.x = (p.x as f64 - ell.center.x) * direction; + ell.major_axis.y = (p.y as f64 - ell.center.y) * direction; + ell.major_axis.z = (p.z as f64 - ell.center.z) * direction; } - (2, GripApply::Absolute(p)) => { + (2 | 4, GripApply::Absolute(p)) => { + let p = if grip_id == 4 { + glam::DVec3::new( + 2.0 * ell.center.x - p.x, + 2.0 * ell.center.y - p.y, + 2.0 * ell.center.z - p.z, + ) + } else { + p + }; // Minor-axis grip. The two axes are always perpendicular, so this // grip stretches one of them along its fixed direction while the // other stays put. Work in the ellipse's view plane (XY). From 283e6db324c72ec4a5cf8c545c571f3ec5630b8d Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:02:38 +0300 Subject: [PATCH 2/2] feat(ellipse): add editable arc endpoint grips --- src/entities/common.rs | 14 +++++++ src/entities/ellipse.rs | 89 +++++++++++++++++++++++++++++++++-------- src/ui/overlay.rs | 17 ++++++-- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/src/entities/common.rs b/src/entities/common.rs index 09a416c46..873a5f0cf 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -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 diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index ad838b884..7723d5e3b 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -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 { +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 { + 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 { + 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 { 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; + } + } _ => {} } } diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 5b1d41d3d..961ec2430 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -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),