mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-01 06:46:50 -06:00
Squash of PR #553 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
543 lines
21 KiB
Rust
543 lines
21 KiB
Rust
//! Grip editing: dragging an object's grips (the points from [`EntityKind::grips`]) the way
|
|
//! AutoCAD does, plus the multi-functional grip modes (move, rotate, scale, mirror about a grip).
|
|
//!
|
|
//! The UI calls [`Session::grip_edit`]; every edit runs through a `grip.*` command so it is
|
|
//! undoable and reachable from scripts, the control channel and MCP.
|
|
|
|
use cadcraft_doc::{DimKind, EntityKind, Handle, LwPolyline};
|
|
use cadcraft_geom::{Arc, EPS, Mat3, Polyline, Vec2, Vec3, norm_angle};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::json;
|
|
|
|
use crate::{EngineError, Result, Session};
|
|
|
|
/// What dragging a hot grip does (AutoCAD's grip modes, cycled with Space/Enter).
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum GripMode {
|
|
/// Move the grip itself (the object's shape changes).
|
|
#[default]
|
|
Stretch,
|
|
/// Move the whole object by the grip displacement.
|
|
Move,
|
|
/// Rotate the object about the grip; the angle is from the grip to the new point.
|
|
Rotate,
|
|
/// Scale the object about the grip; the factor is the distance from the grip to the new point.
|
|
Scale,
|
|
/// Mirror the object about the line from the grip to the new point.
|
|
Mirror,
|
|
}
|
|
|
|
fn set_xy(p: &mut Vec3, q: Vec2) {
|
|
p.x = q.x;
|
|
p.y = q.y;
|
|
}
|
|
|
|
fn translated(k: &EntityKind, d: Vec2) -> EntityKind {
|
|
let mut k = k.clone();
|
|
k.transform(&Mat3::translate(d));
|
|
k
|
|
}
|
|
|
|
/// What a polyline grip index refers to (matches the order of `EntityKind::grips`).
|
|
enum PolyGrip {
|
|
Vertex(usize),
|
|
/// Midpoint of the segment starting at this vertex.
|
|
Mid(usize),
|
|
}
|
|
|
|
fn poly_grip(p: &LwPolyline, index: usize) -> Option<PolyGrip> {
|
|
let n = p.vertices.len();
|
|
// Start vertices of the non-degenerate segments, in the same order as Polyline::segments().
|
|
let count = if p.closed { n } else { n.saturating_sub(1) };
|
|
let mut seg_starts = Vec::with_capacity(count);
|
|
for i in 0..count {
|
|
let (Some(v), Some(w)) = (p.vertices.get(i), p.vertices.get((i + 1) % n.max(1))) else { continue };
|
|
if v.p.near(w.p, EPS) && v.bulge.abs() < 1e-12 {
|
|
continue;
|
|
}
|
|
seg_starts.push(i);
|
|
}
|
|
let mut k = 0usize;
|
|
for i in 0..n {
|
|
if k == index {
|
|
return Some(PolyGrip::Vertex(i));
|
|
}
|
|
k += 1;
|
|
if let Some(start) = seg_starts.get(i) {
|
|
if k == index {
|
|
return Some(PolyGrip::Mid(*start));
|
|
}
|
|
k += 1;
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// The shape after dragging grip `index` to `to` (STRETCH mode). `None` when the grip does not
|
|
/// exist or the result would be degenerate.
|
|
pub fn stretch_grip(kind: &EntityKind, index: usize, to: Vec2) -> Option<EntityKind> {
|
|
if !to.is_finite() {
|
|
return None;
|
|
}
|
|
let grips = kind.grips();
|
|
let from = *grips.get(index)?;
|
|
let delta = to - from;
|
|
let mut k = kind.clone();
|
|
// The further parts of a broken table move on their own.
|
|
if index > 0
|
|
&& let EntityKind::Table(t) = &mut k
|
|
&& let Some(b) = &mut t.breaks
|
|
{
|
|
let i = index - 1;
|
|
if b.offsets.len() <= i {
|
|
b.offsets.resize(i + 1, Vec2::ZERO);
|
|
}
|
|
if let Some(o) = b.offsets.get_mut(i) {
|
|
*o += delta;
|
|
}
|
|
return Some(k);
|
|
}
|
|
match &mut k {
|
|
EntityKind::Line(l) => match index {
|
|
0 => set_xy(&mut l.a, to),
|
|
1 => return Some(translated(kind, delta)),
|
|
_ => set_xy(&mut l.b, to),
|
|
},
|
|
EntityKind::Point(_) | EntityKind::Text(_) | EntityKind::MText(_) | EntityKind::AttDef(_) | EntityKind::Image(_) | EntityKind::Table(_) => {
|
|
return Some(translated(kind, delta));
|
|
}
|
|
EntityKind::Hatch(_) | EntityKind::Tolerance(_) => return Some(translated(kind, delta)),
|
|
EntityKind::Circle(c) => {
|
|
if index == 0 {
|
|
return Some(translated(kind, delta));
|
|
}
|
|
let r = c.center.xy().dist(to);
|
|
if r <= EPS {
|
|
return None;
|
|
}
|
|
c.radius = r;
|
|
}
|
|
EntityKind::Arc(a) => {
|
|
let g = Arc::new(a.center.xy(), a.radius, a.start, a.end);
|
|
let (mut s, mut m, mut e) = (g.start_point(), g.mid_point(), g.end_point());
|
|
match index {
|
|
0 => s = to,
|
|
1 => m = to,
|
|
2 => e = to,
|
|
_ => return Some(translated(kind, delta)),
|
|
}
|
|
let na = Arc::from_3_points(s, m, e)?;
|
|
return Some(crate::cmd::helpers::arc(&na));
|
|
}
|
|
EntityKind::Ellipse(e) => {
|
|
let c = e.center.xy();
|
|
let mj = e.major.xy();
|
|
let a = mj.len();
|
|
if a <= EPS {
|
|
return None;
|
|
}
|
|
let b = a * e.ratio;
|
|
match index {
|
|
0 => return Some(translated(kind, delta)),
|
|
1 | 3 => {
|
|
let v = if index == 1 { to - c } else { c - to };
|
|
let l = v.len();
|
|
if l <= EPS {
|
|
return None;
|
|
}
|
|
if l >= b {
|
|
e.major = v.to3(0.0);
|
|
e.ratio = (b / l).clamp(1e-9, 1.0);
|
|
} else {
|
|
e.major = (v.normalized().perp() * b).to3(0.0);
|
|
e.ratio = (l / b).clamp(1e-9, 1.0);
|
|
shift_params(e, -std::f64::consts::FRAC_PI_2);
|
|
}
|
|
}
|
|
_ => {
|
|
let u = mj / a;
|
|
let l = (to - c).dot(u.perp()).abs();
|
|
if l <= EPS {
|
|
return None;
|
|
}
|
|
if l <= a {
|
|
e.ratio = (l / a).clamp(1e-9, 1.0);
|
|
} else {
|
|
e.major = (u.perp() * l).to3(0.0);
|
|
e.ratio = (a / l).clamp(1e-9, 1.0);
|
|
shift_params(e, -std::f64::consts::FRAC_PI_2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EntityKind::LwPolyline(p) => {
|
|
let n = p.vertices.len();
|
|
match poly_grip(p, index)? {
|
|
PolyGrip::Vertex(i) => p.vertices.get_mut(i)?.p = to,
|
|
PolyGrip::Mid(i) => {
|
|
let j = (i + 1) % n.max(1);
|
|
let (a, b) = (p.vertices.get(i)?.p, p.vertices.get(j)?.p);
|
|
if p.vertices.get(i)?.bulge.abs() > 1e-12 {
|
|
// Arc segment: reshape through the dragged midpoint.
|
|
p.vertices.get_mut(i)?.bulge = crate::cmd::curves::bulge_through(a, to, b);
|
|
} else {
|
|
p.vertices.get_mut(i)?.p = a + delta;
|
|
p.vertices.get_mut(j)?.p = b + delta;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EntityKind::Polyline3d(p) => set_xy(p.points.get_mut(index)?, to),
|
|
EntityKind::Spline(sp) => {
|
|
if sp.fit.is_empty() {
|
|
*sp.control.get_mut(index)? = to;
|
|
} else {
|
|
let mut fit = sp.fit.clone();
|
|
*fit.get_mut(index)? = to;
|
|
*sp = sp.refit(&fit, sp.closed);
|
|
}
|
|
}
|
|
// Centre, base radius (and start angle), top radius.
|
|
EntityKind::Helix(h) => match index {
|
|
0 => return Some(translated(kind, delta)),
|
|
1 => {
|
|
let r = h.center.xy().dist(to);
|
|
if r <= EPS {
|
|
return None;
|
|
}
|
|
h.base_radius = r;
|
|
h.start_angle = h.center.xy().angle_to(to);
|
|
}
|
|
_ => h.top_radius = h.center.xy().dist(to),
|
|
},
|
|
EntityKind::Ray(r) | EntityKind::XLine(r) => {
|
|
if index == 0 {
|
|
return Some(translated(kind, delta));
|
|
}
|
|
let d = (to - r.base.xy()).normalized();
|
|
if d == Vec2::ZERO {
|
|
return None;
|
|
}
|
|
r.dir = d.to3(0.0);
|
|
}
|
|
EntityKind::Insert(i) => {
|
|
if index == 0 {
|
|
return Some(translated(kind, delta));
|
|
}
|
|
let a = i.attribs.get_mut(index - 1)?;
|
|
let ip = a.text.insert.xy() + delta;
|
|
set_xy(&mut a.text.insert, ip);
|
|
if let Some(al) = &mut a.text.align_pt {
|
|
let q = al.xy() + delta;
|
|
set_xy(al, q);
|
|
}
|
|
}
|
|
EntityKind::Dimension(d) => {
|
|
match index {
|
|
0 => set_xy(&mut d.p13, to),
|
|
1 => set_xy(&mut d.p14, to),
|
|
2 => {
|
|
set_xy(&mut d.defpt, to);
|
|
if matches!(d.kind, DimKind::Radius | DimKind::Diameter) {
|
|
d.user_text_pos = false;
|
|
}
|
|
}
|
|
_ => {
|
|
set_xy(&mut d.text_mid, to);
|
|
d.user_text_pos = true;
|
|
}
|
|
}
|
|
d.block = None;
|
|
}
|
|
EntityKind::Leader(l) => set_xy(l.vertices.get_mut(index)?, to),
|
|
EntityKind::MLine(m) => set_xy(&mut m.vertices.get_mut(index)?.p, to),
|
|
EntityKind::MLeader(m) => {
|
|
let mut idx = 0usize;
|
|
let mut done = false;
|
|
'outer: for l in &mut m.leaders {
|
|
for v in l.iter_mut() {
|
|
if idx == index {
|
|
set_xy(v, to);
|
|
done = true;
|
|
break 'outer;
|
|
}
|
|
idx += 1;
|
|
}
|
|
}
|
|
if !done {
|
|
// The landing grip moves the landing and its content.
|
|
let ld = m.landing.xy() + delta;
|
|
set_xy(&mut m.landing, ld);
|
|
if let Some(t) = &mut m.text {
|
|
let q = t.insert.xy() + delta;
|
|
set_xy(&mut t.insert, q);
|
|
}
|
|
if let Some(b) = &mut m.block {
|
|
let q = b.insert.xy() + delta;
|
|
set_xy(&mut b.insert, q);
|
|
}
|
|
}
|
|
}
|
|
EntityKind::Solid(s) | EntityKind::Trace(s) => set_xy(s.corners.get_mut(index)?, to),
|
|
EntityKind::Face3d(f) => set_xy(f.corners.get_mut(index)?, to),
|
|
EntityKind::Region(_) => return Some(translated(kind, delta)),
|
|
EntityKind::Viewport(v) => {
|
|
let opposite = *grips.get((index + 2) % 4)?;
|
|
let w = (to.x - opposite.x).abs();
|
|
let h = (to.y - opposite.y).abs();
|
|
if w <= EPS || h <= EPS {
|
|
return None;
|
|
}
|
|
let c = to.mid(opposite);
|
|
set_xy(&mut v.center, c);
|
|
v.width = w;
|
|
v.height = h;
|
|
}
|
|
EntityKind::Wipeout(w) => *w.boundary.get_mut(index)? = to,
|
|
EntityKind::Unknown(_) => return None,
|
|
}
|
|
Some(k)
|
|
}
|
|
|
|
fn shift_params(e: &mut cadcraft_doc::Ellipse, by: f64) {
|
|
let full = (cadcraft_geom::ccw_sweep(e.start, e.end) - cadcraft_geom::TAU).abs() < 1e-9;
|
|
if !full {
|
|
e.start = norm_angle(e.start + by);
|
|
e.end = norm_angle(e.end + by);
|
|
}
|
|
}
|
|
|
|
/// A multifunctional grip option besides Stretch (the menu a polyline, line or arc grip shows).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum GripAction {
|
|
/// Polyline: insert a vertex after this vertex, or on this segment.
|
|
AddVertex,
|
|
/// Polyline: remove this vertex.
|
|
RemoveVertex,
|
|
/// Polyline: make this straight segment an arc through a picked point.
|
|
ToArc,
|
|
/// Polyline: make this arc segment straight.
|
|
ToLine,
|
|
/// Line or arc end: lengthen or shorten the object along itself.
|
|
Lengthen,
|
|
/// Arc midpoint: change the radius, keeping the center and angles.
|
|
Radius,
|
|
}
|
|
|
|
impl GripAction {
|
|
/// The command-line keyword at the hot-grip prompt.
|
|
pub fn keyword(self) -> &'static str {
|
|
match self {
|
|
GripAction::AddVertex => "Add vertex",
|
|
GripAction::RemoveVertex => "Remove vertex",
|
|
GripAction::ToArc => "conVert to arc",
|
|
GripAction::ToLine => "conVert to line",
|
|
GripAction::Lengthen => "Lengthen",
|
|
GripAction::Radius => "Radius",
|
|
}
|
|
}
|
|
/// The grip menu label.
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
GripAction::AddVertex => "Add Vertex",
|
|
GripAction::RemoveVertex => "Remove Vertex",
|
|
GripAction::ToArc => "Convert to Arc",
|
|
GripAction::ToLine => "Convert to Line",
|
|
GripAction::Lengthen => "Lengthen",
|
|
GripAction::Radius => "Radius",
|
|
}
|
|
}
|
|
/// The mode name on the command line ("** ADD VERTEX **").
|
|
pub fn title(self) -> &'static str {
|
|
match self {
|
|
GripAction::AddVertex => "** ADD VERTEX **",
|
|
GripAction::RemoveVertex => "** REMOVE VERTEX **",
|
|
GripAction::ToArc => "** CONVERT TO ARC **",
|
|
GripAction::ToLine => "** CONVERT TO LINE **",
|
|
GripAction::Lengthen => "** LENGTHEN **",
|
|
GripAction::Radius => "** RADIUS **",
|
|
}
|
|
}
|
|
/// Whether the option needs a point (or value); the others act at once.
|
|
pub fn needs_point(self) -> bool {
|
|
!matches!(self, GripAction::RemoveVertex | GripAction::ToLine)
|
|
}
|
|
}
|
|
|
|
/// The multifunctional options of grip `index` of `kind` (empty when it has only Stretch):
|
|
/// polyline vertices Add/Remove Vertex, polyline segment midpoints Add Vertex and Convert to
|
|
/// Arc/Line, line and arc ends Lengthen, the arc midpoint Radius.
|
|
pub fn grip_actions(kind: &EntityKind, index: usize) -> Vec<GripAction> {
|
|
match kind {
|
|
EntityKind::LwPolyline(p) => match poly_grip(p, index) {
|
|
Some(PolyGrip::Vertex(_)) if p.vertices.len() > 2 => vec![GripAction::AddVertex, GripAction::RemoveVertex],
|
|
Some(PolyGrip::Vertex(_)) => vec![GripAction::AddVertex],
|
|
Some(PolyGrip::Mid(i)) => {
|
|
let arc = p.vertices.get(i).is_some_and(|v| v.bulge.abs() > 1e-12);
|
|
vec![GripAction::AddVertex, if arc { GripAction::ToLine } else { GripAction::ToArc }]
|
|
}
|
|
None => Vec::new(),
|
|
},
|
|
EntityKind::Line(_) if index == 0 || index == 2 => vec![GripAction::Lengthen],
|
|
EntityKind::Arc(_) if index == 0 || index == 2 => vec![GripAction::Lengthen],
|
|
EntityKind::Arc(_) if index == 1 => vec![GripAction::Radius],
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// The object after the multifunctional `action` on grip `index`, with the picked point `to`
|
|
/// (Add Vertex: the new vertex; Convert to Arc: a point on the arc; Lengthen: the new end,
|
|
/// projected onto the object; Radius: a point on the new circle) or the typed `value` (Lengthen:
|
|
/// the change in length; Radius: the new radius). `None` when the option doesn't apply or the
|
|
/// result would be degenerate.
|
|
pub fn grip_action(kind: &EntityKind, index: usize, action: GripAction, to: Option<Vec2>, value: Option<f64>) -> Option<EntityKind> {
|
|
if !grip_actions(kind, index).contains(&action) || to.is_some_and(|p| !p.is_finite()) || value.is_some_and(|v| !v.is_finite()) {
|
|
return None;
|
|
}
|
|
let mut k = kind.clone();
|
|
match (&mut k, action) {
|
|
(EntityKind::LwPolyline(p), GripAction::AddVertex) => {
|
|
let to = to?;
|
|
let at = match poly_grip(p, index)? {
|
|
PolyGrip::Vertex(i) | PolyGrip::Mid(i) => i,
|
|
};
|
|
if at >= p.vertices.len() {
|
|
return None;
|
|
}
|
|
let v = p.vertices.get_mut(at)?;
|
|
// The segment is split in two straight ones.
|
|
v.bulge = 0.0;
|
|
let nv = cadcraft_geom::PolyVertex { p: to, bulge: 0.0, ..*v };
|
|
p.vertices.insert(at + 1, nv);
|
|
}
|
|
(EntityKind::LwPolyline(p), GripAction::RemoveVertex) => {
|
|
let PolyGrip::Vertex(i) = poly_grip(p, index)? else { return None };
|
|
if p.vertices.len() <= 2 || i >= p.vertices.len() {
|
|
return None;
|
|
}
|
|
p.vertices.remove(i);
|
|
// The segment into the removed vertex now runs to the next one: straight.
|
|
let n = p.vertices.len();
|
|
let prev = if i == 0 { if p.closed { n - 1 } else { return Some(k) } } else { i - 1 };
|
|
p.vertices.get_mut(prev)?.bulge = 0.0;
|
|
}
|
|
(EntityKind::LwPolyline(p), GripAction::ToArc) => {
|
|
let PolyGrip::Mid(i) = poly_grip(p, index)? else { return None };
|
|
let j = (i + 1) % p.vertices.len().max(1);
|
|
let (a, b) = (p.vertices.get(i)?.p, p.vertices.get(j)?.p);
|
|
let bulge = crate::cmd::curves::bulge_through(a, to?, b);
|
|
if bulge.abs() < 1e-12 || !bulge.is_finite() {
|
|
return None;
|
|
}
|
|
p.vertices.get_mut(i)?.bulge = bulge;
|
|
}
|
|
(EntityKind::LwPolyline(p), GripAction::ToLine) => {
|
|
let PolyGrip::Mid(i) = poly_grip(p, index)? else { return None };
|
|
p.vertices.get_mut(i)?.bulge = 0.0;
|
|
}
|
|
(EntityKind::Line(l), GripAction::Lengthen) => {
|
|
let (fixed, end) = if index == 0 { (l.b.xy(), l.a.xy()) } else { (l.a.xy(), l.b.xy()) };
|
|
let len = fixed.dist(end);
|
|
let dir = (end - fixed).normalized();
|
|
if dir == Vec2::ZERO {
|
|
return None;
|
|
}
|
|
let new_len = match (value, to) {
|
|
(Some(d), _) => len + d,
|
|
(None, Some(p)) => (p - fixed).dot(dir),
|
|
_ => return None,
|
|
};
|
|
if !(new_len > EPS && new_len.is_finite()) {
|
|
return None;
|
|
}
|
|
let q = fixed + dir * new_len;
|
|
set_xy(if index == 0 { &mut l.a } else { &mut l.b }, q);
|
|
}
|
|
(EntityKind::Arc(a), GripAction::Lengthen) => {
|
|
let c = a.center.xy();
|
|
let ang = match (value, to) {
|
|
(Some(d), _) if a.radius > EPS => {
|
|
let da = d / a.radius;
|
|
if index == 0 { a.start - da } else { a.end + da }
|
|
}
|
|
(None, Some(p)) if !p.near(c, EPS) => c.angle_to(p),
|
|
_ => return None,
|
|
};
|
|
let ang = norm_angle(ang);
|
|
if index == 0 {
|
|
a.start = ang;
|
|
} else {
|
|
a.end = ang;
|
|
}
|
|
let sweep = cadcraft_geom::ccw_sweep(a.start, a.end);
|
|
if !(sweep > 1e-9 && sweep < cadcraft_geom::TAU - 1e-9) {
|
|
return None;
|
|
}
|
|
}
|
|
(EntityKind::Arc(a), GripAction::Radius) => {
|
|
let r = match (value, to) {
|
|
(Some(r), _) => r,
|
|
(None, Some(p)) => a.center.xy().dist(p),
|
|
_ => return None,
|
|
};
|
|
if !(r > EPS && r.is_finite()) {
|
|
return None;
|
|
}
|
|
a.radius = r;
|
|
}
|
|
_ => return None,
|
|
}
|
|
Some(k)
|
|
}
|
|
|
|
/// The transform for a multi-functional grip mode about `base` with the dragged point `to`.
|
|
pub fn mode_matrix(mode: GripMode, base: Vec2, to: Vec2) -> Option<Mat3> {
|
|
match mode {
|
|
GripMode::Stretch => None,
|
|
GripMode::Move => Some(Mat3::translate(to - base)),
|
|
GripMode::Rotate => Some(Mat3::rotate_about(base, base.angle_to(to))),
|
|
GripMode::Scale => {
|
|
let f = base.dist(to);
|
|
(f > EPS && f.is_finite()).then(|| Mat3::scale_about(base, f))
|
|
}
|
|
GripMode::Mirror => (!base.near(to, EPS)).then(|| Mat3::mirror(base, to)),
|
|
}
|
|
}
|
|
|
|
impl Session {
|
|
/// Drag grip `grip_index` of `handle` to `new_point` in `mode` (one undo step).
|
|
/// The base point of the rotate/scale/mirror modes is the grip itself. As in AutoCAD the edit
|
|
/// covers the selection: a stretch drags the grips of other selected objects that sit on the
|
|
/// same point, and the other modes transform every selected object.
|
|
pub fn grip_edit(&mut self, handle: Handle, grip_index: usize, new_point: Vec2, mode: GripMode) -> Result<()> {
|
|
let to = [new_point.x, new_point.y];
|
|
let hs: Vec<String> = std::iter::once(handle).chain(self.selection().into_iter().filter(|h| *h != handle)).map(|h| h.hex()).collect();
|
|
let r = match mode {
|
|
GripMode::Stretch => self.execute("grip.move", &json!({ "handle": handle.hex(), "handles": hs, "index": grip_index, "to": to })),
|
|
GripMode::Move => {
|
|
self.execute("grip.move", &json!({ "handles": hs, "baseHandle": handle.hex(), "index": grip_index, "to": to, "mode": "move" }))
|
|
}
|
|
GripMode::Rotate => self.execute("grip.rotate", &json!({ "handles": hs, "baseHandle": handle.hex(), "index": grip_index, "to": to })),
|
|
GripMode::Scale => self.execute("grip.scale", &json!({ "handles": hs, "baseHandle": handle.hex(), "index": grip_index, "to": to })),
|
|
GripMode::Mirror => self.execute("grip.mirror", &json!({ "handles": hs, "baseHandle": handle.hex(), "index": grip_index, "to": to })),
|
|
};
|
|
r.map(|_| ())
|
|
}
|
|
|
|
/// Grip points of an object (the indices `grip_edit` takes).
|
|
pub fn grips_of(&self, handle: Handle) -> Result<Vec<Vec2>> {
|
|
self.doc()?.entity(handle).map(|e| e.kind.grips()).ok_or_else(|| EngineError::Other("no such object".into()))
|
|
}
|
|
}
|
|
|
|
/// Polyline helper kept public for the UI: segment midpoints are grips too.
|
|
pub fn polyline_grip_count(p: &LwPolyline) -> usize {
|
|
p.vertices.len() + Polyline { vertices: p.vertices.clone(), closed: p.closed }.segments().len().min(p.vertices.len())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "grips_tests.rs"]
|
|
mod tests;
|