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),