From 5755c2e130882d403b5af84c2609ad629ce87527 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 29 May 2026 02:07:34 +0300 Subject: [PATCH] refactor(grip): square-only marker set + oriented stretch handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of bringing the grip vocabulary in line with the standard CAD convention: drop the per-position-type shape encoding (Diamond for translate, Circle for parameter) and use a plain square for every endpoint / vertex / centre grip. The behavioural midpoint vs vertex distinction stays — it lives on `GripDef::is_midpoint` and drives `Translate` vs `Absolute` in the grip-edit dispatcher — only the marker shape collapses. - `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond / Circle are gone. - `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction vector consumed by `Rectangle` to orient the box along its segment. - `entities::common` exposes `square_grip`, `center_grip` (same square marker, flagged as translate), `rectangle_grip(id, world, dir)` for oriented mid-segment handles, and a kept-for-Phase-2 `triangle_grip`. - `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir` through to the canvas painter; the Rectangle path rotates the box around the grip centre using the world-XY direction (flipping the sin for screen-Y). Every existing `diamond_grip(id, w)` call becomes `center_grip(id, w)` — the visual change is square-replacing-diamond at line midpoints / arc midpoints / circle centres / ellipse centres / viewport / hatch loop centroids / dimension grips / image corners / multileader / ray / underlay / ole2frame / solid3d. `LwPolyline` grows a stretch handle on every segment (straight or arc), drawn as the new oriented rectangle. The grip-edit path translates both segment endpoints for straight segments and adjusts the arc bulge from the new midpoint for arc segments. --- src/app/view.rs | 5 ++- src/entities/arc.rs | 6 +-- src/entities/circle.rs | 4 +- src/entities/common.rs | 43 ++++++++++----------- src/entities/dimension.rs | 44 ++++++++++----------- src/entities/ellipse.rs | 4 +- src/entities/hatch.rs | 14 +++---- src/entities/leader.rs | 4 +- src/entities/line.rs | 4 +- src/entities/lwpolyline.rs | 74 ++++++++++++++++++++++++++---------- src/entities/multileader.rs | 4 +- src/entities/ole2frame.rs | 4 +- src/entities/raster_image.rs | 16 ++++---- src/entities/ray.rs | 6 +-- src/entities/solid3d.rs | 8 ++-- src/entities/underlay.rs | 4 +- src/entities/viewport.rs | 4 +- src/scene/grip.rs | 8 ++-- src/scene/object.rs | 30 ++++++++------- src/ui/overlay.rs | 45 ++++++++++++++-------- 20 files changed, 191 insertions(+), 140 deletions(-) diff --git a/src/app/view.rs b/src/app/view.rs index 12d31591c..d04899bac 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -299,7 +299,7 @@ impl OpenCADStudio { }; screen_grips .into_iter() - .filter(|(_, screen, _, _)| { + .filter(|(_, screen, _, _, _)| { screen.x.is_finite() && screen.y.is_finite() && screen.x >= -bounds.width @@ -307,7 +307,7 @@ impl OpenCADStudio { && screen.y >= -bounds.height && screen.y <= bounds.height * 2.0 }) - .map(|(grip_id, screen, _is_midpoint, shape)| { + .map(|(grip_id, screen, _is_midpoint, shape, dir)| { let is_hot = tab .active_grip .as_ref() @@ -316,6 +316,7 @@ impl OpenCADStudio { pos: screen, shape, is_hot, + dir, } }) .collect() diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 828e5016f..4a42df3a9 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -3,7 +3,7 @@ use glam::Vec3; use truck_modeling::{builder, Point3}; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, square_grip}; use crate::entities::traits::TruckConvertible; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -118,10 +118,10 @@ fn grips(arc: &Arc) -> Vec { let ea = arc.end_angle as f32; let ma = sa + angle_span(sa, ea) * 0.5; vec![ - diamond_grip(0, ctr), + center_grip(0, ctr), square_grip(1, ctr + Vec3::new(r * sa.cos(), r * sa.sin(), 0.0)), square_grip(2, ctr + Vec3::new(r * ea.cos(), r * ea.sin(), 0.0)), - diamond_grip(3, ctr + Vec3::new(r * ma.cos(), r * ma.sin(), 0.0)), + center_grip(3, ctr + Vec3::new(r * ma.cos(), r * ma.sin(), 0.0)), ] } diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 52d6e884d..b82764b9c 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -4,7 +4,7 @@ use truck_modeling::{builder, Point3, Wire}; use crate::command::EntityTransform; use crate::entities::common::{ - diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, + center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; use crate::entities::traits::TruckConvertible; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; @@ -119,7 +119,7 @@ fn grips(circle: &Circle) -> Vec { ); let r = circle.radius as f32; vec![ - diamond_grip(0, ctr), + center_grip(0, ctr), square_grip(1, ctr + Vec3::new(r, 0.0, 0.0)), square_grip(2, ctr + Vec3::new(0.0, r, 0.0)), square_grip(3, ctr - Vec3::new(r, 0.0, 0.0)), diff --git a/src/entities/common.rs b/src/entities/common.rs index c9f98d3ec..6288f73ab 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -127,44 +127,43 @@ pub fn square_grip(id: usize, world: Vec3) -> GripDef { world, is_midpoint: false, shape: GripShape::Square, + dir: None, } } -pub fn diamond_grip(id: usize, world: Vec3) -> GripDef { +/// Centre / translate grip — same square marker as a vertex grip but +/// flagged as a "whole-object move" handle for the grip-edit code. +pub fn center_grip(id: usize, world: Vec3) -> GripDef { GripDef { id, world, is_midpoint: true, - shape: GripShape::Diamond, + shape: GripShape::Square, + dir: 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". +pub fn rectangle_grip(id: usize, world: Vec3, dir: [f32; 2]) -> GripDef { + GripDef { + id, + world, + is_midpoint: true, + shape: GripShape::Rectangle, + dir: Some(dir), + } +} + +#[allow(dead_code)] pub fn triangle_grip(id: usize, world: Vec3) -> GripDef { GripDef { id, world, is_midpoint: false, shape: GripShape::Triangle, - } -} - -#[allow(dead_code)] -pub fn rectangle_grip(id: usize, world: Vec3) -> GripDef { - GripDef { - id, - world, - is_midpoint: true, - shape: GripShape::Rectangle, - } -} - -#[allow(dead_code)] -pub fn circle_grip(id: usize, world: Vec3) -> GripDef { - GripDef { - id, - world, - is_midpoint: false, - shape: GripShape::Circle, + dir: None, } } diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 65e9f44db..50ffadf31 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -7,7 +7,7 @@ use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{ - diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, + center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -594,45 +594,45 @@ impl Grippable for Dimension { match self { Dimension::Linear(d) => vec![ square_grip(0, v3(&d.first_point)), - diamond_grip(1, v3(&d.second_point)), - diamond_grip(2, v3(&d.definition_point)), - diamond_grip(3, text), + center_grip(1, v3(&d.second_point)), + center_grip(2, v3(&d.definition_point)), + center_grip(3, text), ], Dimension::Aligned(d) => vec![ square_grip(0, v3(&d.first_point)), - diamond_grip(1, v3(&d.second_point)), - diamond_grip(2, v3(&d.definition_point)), - diamond_grip(3, text), + center_grip(1, v3(&d.second_point)), + center_grip(2, v3(&d.definition_point)), + center_grip(3, text), ], Dimension::Radius(d) => vec![ square_grip(0, v3(&d.angle_vertex)), - diamond_grip(1, v3(&d.definition_point)), - diamond_grip(2, text), + center_grip(1, v3(&d.definition_point)), + center_grip(2, text), ], Dimension::Diameter(d) => vec![ square_grip(0, v3(&d.angle_vertex)), - diamond_grip(1, v3(&d.definition_point)), - diamond_grip(2, text), + center_grip(1, v3(&d.definition_point)), + center_grip(2, text), ], Dimension::Angular2Ln(d) => vec![ square_grip(0, v3(&d.angle_vertex)), - diamond_grip(1, v3(&d.first_point)), - diamond_grip(2, v3(&d.second_point)), - diamond_grip(3, v3(&d.definition_point)), - diamond_grip(4, text), + center_grip(1, v3(&d.first_point)), + center_grip(2, v3(&d.second_point)), + center_grip(3, v3(&d.definition_point)), + center_grip(4, text), ], Dimension::Angular3Pt(d) => vec![ square_grip(0, v3(&d.angle_vertex)), - diamond_grip(1, v3(&d.first_point)), - diamond_grip(2, v3(&d.second_point)), - diamond_grip(3, v3(&d.definition_point)), - diamond_grip(4, text), + center_grip(1, v3(&d.first_point)), + center_grip(2, v3(&d.second_point)), + center_grip(3, v3(&d.definition_point)), + center_grip(4, text), ], Dimension::Ordinate(d) => vec![ square_grip(0, v3(&d.definition_point)), - diamond_grip(1, v3(&d.feature_location)), - diamond_grip(2, v3(&d.leader_endpoint)), - diamond_grip(3, text), + center_grip(1, v3(&d.feature_location)), + center_grip(2, v3(&d.leader_endpoint)), + center_grip(3, text), ], } } diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 5c1c938dc..921710c68 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -3,7 +3,7 @@ use glam::Vec3; use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire}; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::TruckConvertible; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -164,7 +164,7 @@ fn grips(ell: &Ellipse) -> Vec { ell.center.z as f32, ); vec![ - diamond_grip(0, ctr), + center_grip(0, ctr), square_grip(1, maj), square_grip(2, min), ] diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 826d157b9..d1a53f513 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -2,7 +2,7 @@ use acadrust::entities::{BoundaryEdge, Hatch}; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro}; +use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, ro_prop as ro}; use crate::entities::traits::{Grippable, FallbackTess, PropertyEditable, Transformable}; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry}; @@ -182,31 +182,31 @@ impl Grippable for Hatch { match edge { BoundaryEdge::Polyline(p) => { for v in &p.vertices { - out.push(diamond_grip(id, Vec3::new(v.x as f32, v.y as f32, elev))); + out.push(center_grip(id, Vec3::new(v.x as f32, v.y as f32, elev))); id += 1; } } BoundaryEdge::Line(l) => { - out.push(diamond_grip( + out.push(center_grip( id, Vec3::new(l.start.x as f32, l.start.y as f32, elev), )); id += 1; - out.push(diamond_grip( + out.push(center_grip( id, Vec3::new(l.end.x as f32, l.end.y as f32, elev), )); id += 1; } BoundaryEdge::CircularArc(a) => { - out.push(diamond_grip( + out.push(center_grip( id, Vec3::new(a.center.x as f32, a.center.y as f32, elev), )); id += 1; } BoundaryEdge::EllipticArc(e) => { - out.push(diamond_grip( + out.push(center_grip( id, Vec3::new(e.center.x as f32, e.center.y as f32, elev), )); @@ -219,7 +219,7 @@ impl Grippable for Hatch { s.control_points.iter().map(|p| [p.x, p.y]).collect() }; for [x, y] in pts { - out.push(diamond_grip(id, Vec3::new(x as f32, y as f32, elev))); + out.push(center_grip(id, Vec3::new(x as f32, y as f32, elev))); id += 1; } } diff --git a/src/entities/leader.rs b/src/entities/leader.rs index 125161659..b23e3cc9b 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -3,7 +3,7 @@ use acadrust::Entity; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{TruckConvertible}; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; @@ -98,7 +98,7 @@ fn grips(leader: &Leader) -> Vec { let sum = leader.vertices.iter().fold(Vec3::ZERO, |acc, v| { acc + Vec3::new(v.x as f32, v.y as f32, v.z as f32) }); - grips.push(diamond_grip(n, sum / n as f32)); + grips.push(center_grip(n, sum / n as f32)); } grips diff --git a/src/entities/line.rs b/src/entities/line.rs index d522126cc..a37961ae2 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -4,7 +4,7 @@ use truck_modeling::{builder, Point3}; use crate::command::EntityTransform; use crate::entities::common::{ - diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, + center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; use crate::entities::traits::TruckConvertible; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; @@ -74,7 +74,7 @@ fn grips(line: &Line) -> Vec { ); let e = Vec3::new(line.end.x as f32, line.end.y as f32, line.end.z as f32); let m = (s + e) * 0.5; - vec![square_grip(0, s), square_grip(1, e), diamond_grip(2, m)] + vec![square_grip(0, s), square_grip(1, e), center_grip(2, m)] } fn properties(line: &Line) -> PropSection { diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index c96d58349..f44cb2e67 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -4,7 +4,7 @@ use truck_modeling::{builder, Edge, Point3, Wire}; use crate::command::EntityTransform; use crate::entities::common::{ - diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, + edit_prop as edit, parse_f64, rectangle_grip, ro_prop as ro, square_grip, }; use crate::entities::traits::{TruckConvertible}; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; @@ -295,21 +295,31 @@ fn grips(pline: &LwPolyline) -> Vec { .map(|(i, v)| square_grip(i, Vec3::new(v.location.x as f32, v.location.y as f32, elev))) .collect(); - // Diamond midpoint grip for each arc segment + // One mid-segment stretch grip per segment (straight or arc). The + // marker is a small box rotated along the chord direction so the + // shape itself signals which way the segment runs. for i in 0..seg_count { let v0 = &pline.vertices[i]; - if v0.bulge.abs() < 1e-9 { - continue; - } let v1 = &pline.vertices[(i + 1) % n]; - let mid = arc_midpoint( - [v0.location.x, v0.location.y], - [v1.location.x, v1.location.y], - v0.bulge, - ); - out.push(diamond_grip( + let (mx, my) = if v0.bulge.abs() < 1e-9 { + ( + (v0.location.x + v1.location.x) * 0.5, + (v0.location.y + v1.location.y) * 0.5, + ) + } else { + let m = arc_midpoint( + [v0.location.x, v0.location.y], + [v1.location.x, v1.location.y], + v0.bulge, + ); + (m[0], m[1]) + }; + let dx = (v1.location.x - v0.location.x) as f32; + let dy = (v1.location.y - v0.location.y) as f32; + out.push(rectangle_grip( n + i, - Vec3::new(mid[0] as f32, mid[1] as f32, elev), + Vec3::new(mx as f32, my as f32, elev), + [dx, dy], )); } out @@ -355,7 +365,11 @@ fn apply_grip(pline: &mut LwPolyline, grip_id: usize, apply: GripApply) { } } } else { - // Arc midpoint grip for segment (grip_id - n) + // Mid-segment stretch grip for segment (grip_id - n). + // Straight segments translate both endpoints by the drag delta + // (the shared vertices then carry along whichever adjacent + // segments share them). Arc segments adjust their bulge from + // the new midpoint position. let seg = grip_id - n; let count = if pline.is_closed { n @@ -365,11 +379,31 @@ fn apply_grip(pline: &mut LwPolyline, grip_id: usize, apply: GripApply) { if seg >= count { return; } + let i0 = seg; + let i1 = (seg + 1) % n; + let is_arc = pline.vertices[i0].bulge.abs() >= 1e-9; + if !is_arc { + let d = match apply { + GripApply::Translate(d) => [d.x as f64, d.y as f64], + GripApply::Absolute(p) => { + let old_mid = ( + (pline.vertices[i0].location.x + pline.vertices[i1].location.x) * 0.5, + (pline.vertices[i0].location.y + pline.vertices[i1].location.y) * 0.5, + ); + [p.x as f64 - old_mid.0, p.y as f64 - old_mid.1] + } + }; + pline.vertices[i0].location.x += d[0]; + pline.vertices[i0].location.y += d[1]; + pline.vertices[i1].location.x += d[0]; + pline.vertices[i1].location.y += d[1]; + return; + } let new_mid: [f64; 2] = match apply { GripApply::Absolute(p) => [p.x as f64, p.y as f64], GripApply::Translate(d) => { - let v0 = &pline.vertices[seg]; - let v1 = &pline.vertices[(seg + 1) % n]; + let v0 = &pline.vertices[i0]; + let v1 = &pline.vertices[i1]; let old = arc_midpoint( [v0.location.x, v0.location.y], [v1.location.x, v1.location.y], @@ -379,15 +413,15 @@ fn apply_grip(pline: &mut LwPolyline, grip_id: usize, apply: GripApply) { } }; let p0 = [ - pline.vertices[seg].location.x, - pline.vertices[seg].location.y, + pline.vertices[i0].location.x, + pline.vertices[i0].location.y, ]; let p1 = [ - pline.vertices[(seg + 1) % n].location.x, - pline.vertices[(seg + 1) % n].location.y, + pline.vertices[i1].location.x, + pline.vertices[i1].location.y, ]; if let Some(new_bulge) = bulge_from_midpoint(p0, p1, new_mid) { - pline.vertices[seg].bulge = new_bulge.clamp(-1e6, 1e6); + pline.vertices[i0].bulge = new_bulge.clamp(-1e6, 1e6); } } } diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 3b9e272ff..ff8b8fc10 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -29,7 +29,7 @@ pub(crate) fn mleader_v_anchor(attach: TextAttachmentType) -> MTextVAnchor { use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{TruckConvertible}; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; @@ -310,7 +310,7 @@ fn grips(ml: &MultiLeader) -> Vec { if ml.content_type == LeaderContentType::MText { let tl = &ml.context.text_location; - result.push(diamond_grip( + result.push(center_grip( id, Vec3::new(tl.x as f32, tl.y as f32, tl.z as f32), )); diff --git a/src/entities/ole2frame.rs b/src/entities/ole2frame.rs index 2bb427160..f3138d460 100644 --- a/src/entities/ole2frame.rs +++ b/src/entities/ole2frame.rs @@ -2,7 +2,7 @@ use acadrust::entities::{Ole2Frame, OleObjectType}; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{TruckConvertible}; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -70,7 +70,7 @@ fn grips(ole: &Ole2Frame) -> Vec { vec![ square_grip(0, ul), square_grip(1, lr), - diamond_grip(2, center), + center_grip(2, center), ] } diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index 1152d20fe..fb270b365 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -2,7 +2,7 @@ use acadrust::entities::{RasterImage, Wipeout}; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; @@ -140,9 +140,9 @@ impl Grippable for RasterImage { ); vec![ square_grip(0, Vec3::from(to_f32_3(corners[0]))), - diamond_grip(1, Vec3::from(to_f32_3(corners[1]))), - diamond_grip(2, Vec3::from(to_f32_3(corners[2]))), - diamond_grip(3, Vec3::from(to_f32_3(corners[3]))), + center_grip(1, Vec3::from(to_f32_3(corners[1]))), + center_grip(2, Vec3::from(to_f32_3(corners[2]))), + center_grip(3, Vec3::from(to_f32_3(corners[3]))), ] } @@ -346,7 +346,7 @@ impl Grippable for Wipeout { if i == 0 { square_grip(i, Vec3::new(ox + wx, oy + wy, oz + wz)) } else { - diamond_grip(i, Vec3::new(ox + wx, oy + wy, oz + wz)) + center_grip(i, Vec3::new(ox + wx, oy + wy, oz + wz)) } }) .collect() @@ -360,9 +360,9 @@ impl Grippable for Wipeout { ); vec![ square_grip(0, Vec3::from(to_f32_3(corners[0]))), - diamond_grip(1, Vec3::from(to_f32_3(corners[1]))), - diamond_grip(2, Vec3::from(to_f32_3(corners[2]))), - diamond_grip(3, Vec3::from(to_f32_3(corners[3]))), + center_grip(1, Vec3::from(to_f32_3(corners[1]))), + center_grip(2, Vec3::from(to_f32_3(corners[2]))), + center_grip(3, Vec3::from(to_f32_3(corners[3]))), ] } } diff --git a/src/entities/ray.rs b/src/entities/ray.rs index bb60067f9..c94c5488f 100644 --- a/src/entities/ray.rs +++ b/src/entities/ray.rs @@ -2,7 +2,7 @@ use acadrust::entities::{Ray, XLine}; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -48,7 +48,7 @@ impl Grippable for Ray { let guide_dist = 10.0_f64; vec![ square_grip(0, Vec3::new(bp.x as f32, bp.y as f32, bp.z as f32)), - diamond_grip( + center_grip( 1, Vec3::new( (bp.x + dir.x * guide_dist) as f32, @@ -186,7 +186,7 @@ impl Grippable for XLine { let guide_dist = 10.0_f64; vec![ square_grip(0, Vec3::new(bp.x as f32, bp.y as f32, bp.z as f32)), - diamond_grip( + center_grip( 1, Vec3::new( (bp.x + dir.x * guide_dist) as f32, diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index accd15039..58bd5cb77 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -9,7 +9,7 @@ use acadrust::entities::{Body, Region, Solid3D}; use glam::Vec3; -use crate::entities::common::{diamond_grip, ro_prop as ro}; +use crate::entities::common::{center_grip, ro_prop as ro}; use crate::entities::traits::{Grippable, PropertyEditable}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -44,7 +44,7 @@ fn acis_size_str(has_data: bool, sat_len: usize, sab_len: usize, is_binary: bool impl Grippable for Solid3D { fn grips(&self) -> Vec { - vec![diamond_grip(0, por_to_vec3(&self.point_of_reference))] + vec![center_grip(0, por_to_vec3(&self.point_of_reference))] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { @@ -120,7 +120,7 @@ impl PropertyEditable for Solid3D { impl Grippable for Region { fn grips(&self) -> Vec { - vec![diamond_grip(0, por_to_vec3(&self.point_of_reference))] + vec![center_grip(0, por_to_vec3(&self.point_of_reference))] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { @@ -188,7 +188,7 @@ impl PropertyEditable for Region { impl Grippable for Body { fn grips(&self) -> Vec { - vec![diamond_grip(0, por_to_vec3(&self.point_of_reference))] + vec![center_grip(0, por_to_vec3(&self.point_of_reference))] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index 5145dfba2..86f95981b 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -8,7 +8,7 @@ use acadrust::entities::{Underlay, UnderlayDisplayFlags}; use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -87,7 +87,7 @@ impl Grippable for Underlay { if !self.clip_boundary_vertices.is_empty() { let world_verts = self.world_clip_boundary(); for (i, v) in world_verts.iter().enumerate() { - grips.push(diamond_grip( + grips.push(center_grip( i + 1, Vec3::new(v.x as f32, v.y as f32, v.z as f32), )); diff --git a/src/entities/viewport.rs b/src/entities/viewport.rs index 186509e8c..5c6eef377 100644 --- a/src/entities/viewport.rs +++ b/src/entities/viewport.rs @@ -3,7 +3,7 @@ use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{ - diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, + center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; @@ -97,7 +97,7 @@ fn grips(vp: &Viewport) -> Vec { let hw = (vp.width / 2.0) as f32; let hh = (vp.height / 2.0) as f32; vec![ - diamond_grip(0, Vec3::new(cx, cy, cz)), + center_grip(0, Vec3::new(cx, cy, cz)), square_grip(1, Vec3::new(cx + hw, cy + hh, cz)), square_grip(2, Vec3::new(cx - hw, cy + hh, cz)), square_grip(3, Vec3::new(cx - hw, cy - hh, cz)), diff --git a/src/scene/grip.rs b/src/scene/grip.rs index f62fdde8d..bb2f3faea 100644 --- a/src/scene/grip.rs +++ b/src/scene/grip.rs @@ -36,7 +36,7 @@ pub fn grips_to_screen( grips: &[GripDef], view_proj: Mat4, bounds: Rectangle, -) -> Vec<(usize, Point, bool, GripShape)> { +) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> { grips .iter() .map(|g| { @@ -45,7 +45,7 @@ pub fn grips_to_screen( bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width, bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height, ); - (g.id, screen, g.is_midpoint, g.shape) + (g.id, screen, g.is_midpoint, g.shape, g.dir) }) .collect() } @@ -59,7 +59,7 @@ pub fn grips_to_screen_paper( half_w: f32, half_h: f32, bounds: Rectangle, -) -> Vec<(usize, Point, bool, GripShape)> { +) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> { grips .iter() .map(|g| { @@ -67,7 +67,7 @@ pub fn grips_to_screen_paper( (g.world.x - tx + half_w) / (2.0 * half_w) * bounds.width, (ty + half_h - g.world.y) / (2.0 * half_h) * bounds.height, ); - (g.id, screen, g.is_midpoint, g.shape) + (g.id, screen, g.is_midpoint, g.shape, g.dir) }) .collect() } diff --git a/src/scene/object.rs b/src/scene/object.rs index e71f2eeb4..8ac5fcba4 100644 --- a/src/scene/object.rs +++ b/src/scene/object.rs @@ -51,23 +51,23 @@ pub struct PropSection { // ── Grip types ───────────────────────────────────────────────────────────── -/// Visual marker shape for a grip point. The complete vocabulary used -/// across entity types: -/// * `Square` — vertex / endpoint that moves a single point. -/// * `Rectangle` — direction-aware mid-segment stretch (polyline -/// straight segments, dimension extension lines). -/// * `Diamond` — midpoint of a curve / centre of a closed shape (drags -/// the whole shape or stretches the midpoint). -/// * `Triangle` — directional control (rotate / add vertex / continue). -/// * `Circle` — parametric control (radius / dimension value). +/// Visual marker shape for a grip point. The complete vocabulary that +/// matches the standard CAD grip conventions: +/// * `Square` — endpoint / vertex / centre. Drag → moves a single +/// point or translates the entity. +/// * `Rectangle` — direction-aware mid-segment stretch handle. +/// Drawn as a small box rotated along `dir` (the in-plane segment +/// direction in world XY). Used for polyline / wipeout / image / +/// dimension segment midpoints. +/// * `Triangle` — directional indicator (Phase 2: dynamic-block +/// parameters, dimension reverse-arrow flips, multi-functional +/// hover popups). #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(dead_code)] pub enum GripShape { Square, Rectangle, - Diamond, Triangle, - Circle, } /// Describes one grip point for an entity. @@ -77,11 +77,15 @@ pub struct GripDef { pub id: usize, /// World-space position of the grip. pub world: Vec3, - /// `true` → midpoint grip (diamond, translates whole object). - /// `false` → endpoint grip (square, moves a single vertex). + /// `true` → midpoint / centre grip (drags the whole shape). + /// `false` → endpoint / vertex grip (moves a single point). pub is_midpoint: bool, /// Visual marker shape for the grip. pub shape: GripShape, + /// World-XY direction vector used to orient a `Rectangle` grip + /// along its segment. `None` for shapes that don't need rotation + /// (Square, Triangle in non-directional contexts). + pub dir: Option<[f32; 2]>, } /// How to apply a grip drag result. diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 3d6897c3a..a550feebc 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -26,6 +26,10 @@ pub struct GripMarker { pub shape: GripShape, /// True → grip is currently being dragged (drawn filled red). pub is_hot: bool, + /// World-XY direction vector — only consumed by the `Rectangle` + /// shape to orient the box along its segment. `None` for grips + /// that don't need rotation. + pub dir: Option<[f32; 2]>, } // ── Grid display params ─────────────────────────────────────────────────── @@ -424,29 +428,38 @@ impl canvas::Program for SelectionCanvas { Size::new(h * 2.0, h * 2.0), ), GripShape::Rectangle => { - // Wider-than-tall box — direction-aware mid-segment - // stretch handle. - let w = h * 1.4; - let t = h * 0.7; - canvas::Path::rectangle( - Point::new(sp.x - w, sp.y - t), - Size::new(w * 2.0, t * 2.0), - ) + // Mid-segment stretch handle: small box, longer along + // the segment direction so the affordance reads as + // "stretch perpendicular to the segment". `dir` is a + // world-XY direction vector; project it onto the + // screen-X / screen-Y axes implied by the grip's + // 2-D screen position to compute the in-plane angle. + let half_long = h * 1.4; + let half_short = h * 0.7; + let (cos_t, sin_t) = match grip.dir { + Some([dx, dy]) if (dx * dx + dy * dy) > 1e-12 => { + let n = (dx * dx + dy * dy).sqrt(); + // Screen Y is inverted vs world Y → flip sin. + (dx / n, -dy / n) + } + _ => (1.0, 0.0), + }; + let ax = (cos_t * half_long, sin_t * half_long); + let ay = (-sin_t * half_short, cos_t * half_short); + canvas::Path::new(|b| { + b.move_to(Point::new(sp.x + ax.0 + ay.0, sp.y + ax.1 + ay.1)); + b.line_to(Point::new(sp.x + ax.0 - ay.0, sp.y + ax.1 - ay.1)); + b.line_to(Point::new(sp.x - ax.0 - ay.0, sp.y - ax.1 - ay.1)); + b.line_to(Point::new(sp.x - ax.0 + ay.0, sp.y - ax.1 + ay.1)); + b.close(); + }) } - GripShape::Diamond => canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - h)); - b.line_to(Point::new(sp.x + h, sp.y)); - b.line_to(Point::new(sp.x, sp.y + h)); - b.line_to(Point::new(sp.x - h, sp.y)); - b.close(); - }), 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)); b.close(); }), - GripShape::Circle => canvas::Path::circle(sp, h), }; if grip.is_hot {