refactor(grip): square-only marker set + oriented stretch handles

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.
This commit is contained in:
Hakan Seven 2026-05-29 02:07:34 +03:00
commit 5755c2e130
20 changed files with 184 additions and 133 deletions

View file

@ -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()

View file

@ -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<GripDef> {
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)),
]
}

View file

@ -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<GripDef> {
);
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)),

View file

@ -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,
}
}

View file

@ -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),
],
}
}

View file

@ -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<GripDef> {
ell.center.z as f32,
);
vec![
diamond_grip(0, ctr),
center_grip(0, ctr),
square_grip(1, maj),
square_grip(2, min),
]

View file

@ -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;
}
}

View file

@ -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<GripDef> {
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

View file

@ -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<GripDef> {
);
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 {

View file

@ -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<GripDef> {
.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);
}
}
}

View file

@ -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<GripDef> {
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),
));

View file

@ -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<GripDef> {
vec![
square_grip(0, ul),
square_grip(1, lr),
diamond_grip(2, center),
center_grip(2, center),
]
}

View file

@ -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]))),
]
}
}

View file

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

View file

@ -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<GripDef> {
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<GripDef> {
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<GripDef> {
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) {

View file

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

View file

@ -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<GripDef> {
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)),

View file

@ -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()
}

View file

@ -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.

View file

@ -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<Message> 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 {