Files
cadcraft/crates/engine/src/cmd/modify2/pedit.rs
T
aldozarandClaude Opus 5.5 afc12e4dce PEDIT Multiple at the command line (#2225)
* PEDIT Multiple at the command line (#2182)

`M` at "Select polyline" answered that Multiple was only available through the JSON form. It now
works as in AutoCAD: select objects (polylines, lines and arcs), turn the lines and arcs into
polylines on Yes (left out on No), then each option applies to all of them: Close, Open, Width,
Fit, Spline, Decurve, Ltype gen and Reverse. Join joins the selected objects with each other,
ends within a fuzz distance, and reports how many it added. Undo takes the last option back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Geometry parity status line for PEDIT Multiple

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* PEDIT Multiple: objects selected first, and Decurve after Fit or Spline

Several objects selected before PEDIT go straight into Multiple, as in AutoCAD, and M keeps a
selection made first. Decurve gives back each polyline's vertices from before its first Fit
or Spline, as single-polyline PEDIT does; a Join drops that record for the joined polylines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* PEDIT Multiple: the Decurve record follows Undo and survives a Join that joins nothing

Each Undo step keeps the record of the vertices from before Fit or Spline, so Fit, Join, Undo,
Decurve gives them back; a Join drops the record only for a polyline that took on segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* PEDIT: Yes/No asked again on another answer; Decurve keeps a later Width

The convert questions (single and Multiple) take Y/Yes, N/No or Enter and ask again for any
other answer instead of treating it as Yes. Width clears the per-vertex widths of the vertices
kept for Decurve too, so Decurve after Fit or Spline and Width keeps the new width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:20:58 -04:00

636 lines
27 KiB
Rust

use super::{
Accept, Chain, EntityKind, Handle, Input, Interactive, LwPolyline, MAX_GEN, PolyVertex, Prompt, Result, SelOutcome, SelectPhase, Session, Spline,
Step, Value, Vec2, as_lwpoly, bad, bool_or, curves, f64_or, f64_req, json, line, number, other, pedit_vertex, point_req, set_kind, str_param,
targets,
};
/// Fit curve: a tangent-continuous pair of arcs (biarc) per segment through every vertex,
/// following the tangent directions given per vertex (Edit vertex Tangent).
pub(crate) fn fit_vertices(vs: &[PolyVertex], closed: bool, tans: &[Option<Vec2>]) -> Vec<PolyVertex> {
let pts: Vec<Vec2> = vs.iter().map(|v| v.p).collect();
let n = pts.len();
if n < 3 {
return vs.iter().map(|v| PolyVertex { bulge: 0.0, ..*v }).collect();
}
let at = |i: usize| pts.get(i).copied().unwrap_or_default();
let tangent = |i: usize| -> Vec2 {
let (prev, next) = if closed {
(at((i + n - 1) % n), at((i + 1) % n))
} else if i == 0 {
(at(0), at(1))
} else if i + 1 == n {
(at(n - 2), at(n - 1))
} else {
(at(i - 1), at(i + 1))
};
let t = (next - prev).normalized();
if t == Vec2::ZERO { Vec2::X } else { t }
};
// Open ends: mirror the neighbouring tangent across the chord for a natural end.
let tan_at = |i: usize| -> Vec2 {
if let Some(Some(t)) = tans.get(i) {
return *t;
}
if closed || (i != 0 && i + 1 != n) {
return tangent(i);
}
let (a, b, tb) = if i == 0 { (at(0), at(1), tangent(1)) } else { (at(n - 1), at(n - 2), -tangent(n - 2)) };
let c = (b - a).normalized();
let m = c * (2.0 * tb.dot(c)) - tb;
let m = if i == 0 { m } else { -m };
if m.len() < 1e-12 { c } else { m.normalized() }
};
let count = if closed { n } else { n - 1 };
let mut out = Vec::with_capacity(count * 2 + 1);
for i in 0..count {
let p0 = at(i);
let p1 = at((i + 1) % n);
let t0 = tan_at(i);
let t1 = tan_at((i + 1) % n);
let v = p1 - p0;
let tsum = t0 + t1;
let denom = 2.0 * (1.0 - t0.dot(t1));
let d = if denom.abs() < 1e-12 {
let vt = v.dot(t1);
if vt.abs() < 1e-12 { v.len() / 2.0 } else { v.len2() / (4.0 * vt) }
} else {
let vt = v.dot(tsum);
(-vt + (vt * vt + denom * v.len2()).max(0.0).sqrt()) / denom
};
let pm = (p0 + t0 * d + p1 - t1 * d) / 2.0;
let b1 = curves::bulge_from_tangent(p0, pm, t0);
let b2 = -curves::bulge_from_tangent(p1, pm, -t1);
let w = vs.get(i).copied().unwrap_or_default();
out.push(PolyVertex { p: p0, bulge: if b1.is_finite() { b1 } else { 0.0 }, start_width: w.start_width, end_width: w.end_width });
out.push(PolyVertex { p: pm, bulge: if b2.is_finite() { b2 } else { 0.0 }, start_width: w.end_width, end_width: w.end_width });
}
if !closed {
out.push(PolyVertex::new(at(n - 1)));
}
out
}
/// Spline curve: a cubic B-spline with the vertices as its frame (control points).
pub(crate) fn spline_vertices(vs: &[PolyVertex], closed: bool, segs: usize) -> Vec<PolyVertex> {
let pts: Vec<Vec2> = vs.iter().map(|v| v.p).collect();
let n = pts.len();
if n < 3 {
return vs.to_vec();
}
let segs = segs.clamp(1, 64).min((MAX_GEN / n).max(1));
let mut out: Vec<Vec2> = Vec::new();
if closed {
// Uniform periodic cubic B-spline.
let at = |i: usize| pts.get(i % n).copied().unwrap_or_default();
for i in 0..n {
let (p0, p1, p2, p3) = (at(i + n - 1), at(i), at(i + 1), at(i + 2));
for k in 0..segs {
let t = k as f64 / segs as f64;
let (t2, t3) = (t * t, t * t * t);
let b0 = (1.0 - t).powi(3) / 6.0;
let b1 = (3.0 * t3 - 6.0 * t2 + 4.0) / 6.0;
let b2 = (-3.0 * t3 + 3.0 * t2 + 3.0 * t + 1.0) / 6.0;
let b3 = t3 / 6.0;
out.push(p0 * b0 + p1 * b1 + p2 * b2 + p3 * b3);
}
}
} else {
let sp = Spline::from_control(pts.clone(), 3);
let (lo, hi) = sp.domain();
let m = (segs * (n - 1)).min(MAX_GEN);
for k in 0..=m {
out.push(sp.eval(lo + (hi - lo) * k as f64 / m as f64));
}
}
out.into_iter().map(PolyVertex::new).collect()
}
/// Join connected lines/arcs/open polylines onto the ends of polyline `h` (kept in place).
pub(crate) fn pedit_join(s: &mut Session, h: Handle, others: &[Handle], fuzz: f64) -> Result<usize> {
let e = curves::entity(s, h)?;
let mut base = as_lwpoly(&e.kind).ok_or_else(|| other("Select a polyline."))?;
if base.closed {
return Err(other("Cannot join to a closed polyline."));
}
let mut chain = Chain::of(&EntityKind::LwPolyline(base.clone())).unwrap_or_default();
let tol = fuzz.max(1e-6);
let mut pool: Vec<(Handle, Chain)> = Vec::new();
for o in others {
if *o == h {
continue;
}
let Ok(oe) = curves::entity(s, *o) else { continue };
let ok = matches!(&oe.kind, EntityKind::Line(_) | EntityKind::Arc(_)) || matches!(&oe.kind, EntityKind::LwPolyline(p) if !p.closed);
if ok && let Some(c) = Chain::of(&oe.kind) {
pool.push((*o, c));
}
}
let mut joined = Vec::new();
while let (Some(st), Some(en)) = (chain.start(), chain.end()) {
let Some(i) = pool
.iter()
.position(|(_, c)| c.start().zip(c.end()).is_some_and(|(a, b)| a.near(en, tol) || b.near(en, tol) || a.near(st, tol) || b.near(st, tol)))
else {
break;
};
let (oh, c) = pool.remove(i);
let (Some(a), Some(b)) = (c.start(), c.end()) else { continue };
if a.near(en, tol) {
chain.segs.extend(c.segs);
} else if b.near(en, tol) {
chain.segs.extend(c.reversed().segs);
} else if b.near(st, tol) {
let mut v = c.segs;
v.extend(chain.segs);
chain.segs = v;
} else {
let mut v = c.reversed().segs;
v.extend(chain.segs);
chain.segs = v;
}
joined.push(oh);
}
if joined.is_empty() {
return Ok(0);
}
chain.closed = chain.start().zip(chain.end()).is_some_and(|(a, b)| a.near(b, tol)) && chain.segs.len() > 1;
base.vertices = chain.vertices();
base.closed = chain.closed;
let d = s.doc_mut()?;
d.modify_entity(h, |e| e.kind = EntityKind::LwPolyline(base))?;
for o in &joined {
d.remove_entity(*o);
}
Ok(joined.len())
}
pub(super) fn pedit_apply(s: &mut Session, h: Handle, opt: &str, p: &Value) -> Result<Value> {
let e = curves::entity(s, h)?;
if curves::is_locked(s, h) {
return Err(other("The object is on a locked layer."));
}
let mut pl = match &e.kind {
EntityKind::LwPolyline(pl) => pl.clone(),
EntityKind::Line(_) | EntityKind::Arc(_) if bool_or(p, "convert", true) => as_lwpoly(&e.kind).ok_or_else(|| other("Cannot convert."))?,
_ => return Err(other("Object selected is not a polyline.")),
};
match opt {
"close" | "c" => pl.closed = pl.vertices.len() > 1,
"open" | "o" => pl.closed = false,
"width" | "w" => {
let w = f64_req("pedit", p, "width")?;
if w < 0.0 {
return Err(bad("pedit", "width must be non-negative"));
}
pl.const_width = w;
for v in &mut pl.vertices {
v.start_width = 0.0;
v.end_width = 0.0;
}
}
"fit" | "f" => pl.vertices = fit_vertices(&pl.vertices, pl.closed, &pedit_vertex::tangent_param(p, pl.vertices.len())?),
"spline" | "s" => {
let segs = s.doc()?.header.i64("SPLINESEGS", 8).clamp(1, 64) as usize;
pl.vertices = spline_vertices(&pl.vertices, pl.closed, segs);
}
"decurve" | "d" => pl.vertices.iter_mut().for_each(|v| v.bulge = 0.0),
"reverse" | "r" => pl.vertices = curves::reverse_vertices(&pl.vertices, pl.closed),
"ltypegen" | "l" => pl.plinegen = bool_or(p, "on", !pl.plinegen),
"join" | "j" => {
set_kind(s, h, EntityKind::LwPolyline(pl))?;
let others: Vec<Handle> = p
.get("handles2")
.or_else(|| p.get("join"))
.and_then(Value::as_array)
.map(|a| a.iter().filter_map(curves::handle_value).collect())
.unwrap_or_default();
let n = pedit_join(s, h, &others, f64_or(p, "fuzz", 0.0))?;
return Ok(json!({ "handle": h.hex(), "joined": n }));
}
"vertex" | "e" => {
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("pedit", "`index` is required"))? as usize;
let n = pl.vertices.len();
if i >= n {
return Err(bad("pedit", "vertex index out of range"));
}
match str_param(p, "action").unwrap_or("move") {
"move" => {
let to = point_req("pedit", p, "to")?;
if let Some(v) = pl.vertices.get_mut(i) {
v.p = to;
}
}
"insert" => {
let to = point_req("pedit", p, "to")?;
if n >= MAX_GEN {
return Err(bad("pedit", "too many vertices"));
}
if let Some(v) = pl.vertices.get_mut(i) {
v.bulge = 0.0;
}
pl.vertices.insert(i + 1, PolyVertex::new(to));
}
"straighten" => {
let j = p.get("index2").and_then(Value::as_u64).map_or(i, |j| j as usize);
pedit_vertex::straighten(&mut pl, i, j)?;
}
"break" => {
let j = p.get("index2").and_then(Value::as_u64).map_or(i, |j| j as usize);
let hs = pedit_vertex::break_poly(s, h, &pl, i, j)?;
return Ok(json!({ "handle": h.hex(), "handles": hs.iter().map(|h| h.hex()).collect::<Vec<_>>() }));
}
// The display is always current; tangents only exist within an interactive PEDIT.
"regen" => {}
"width" => {
let sw = f64_or(p, "startWidth", 0.0).max(0.0);
let ew = f64_or(p, "endWidth", sw).max(0.0);
if let Some(v) = pl.vertices.get_mut(i) {
v.start_width = sw;
v.end_width = ew;
}
}
_ => return Err(bad("pedit", "action must be move, insert, straighten, break, width or regen")),
}
}
_ => return Err(bad("pedit", "unknown option")),
}
set_kind(s, h, EntityKind::LwPolyline(pl))?;
Ok(json!({ "handle": h.hex() }))
}
pub(super) fn run_pedit(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
if hs.is_empty() {
return Err(bad("pedit", "`handle` is required"));
}
let opt = str_param(p, "option").ok_or_else(|| bad("pedit", "`option` is required"))?.to_ascii_lowercase();
let mut out = Vec::new();
for h in hs {
out.push(pedit_apply(s, h, &opt, p)?);
}
Ok(if out.len() == 1 { out.pop().unwrap_or(Value::Null) } else { json!({ "results": out }) })
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum PPhase {
#[default]
Select,
Convert,
Main,
Width,
Join,
Vertex,
VMove,
VInsert,
VWidthStart,
VWidthEnd,
/// The second marker of Break / Straighten.
VMark,
VTangent,
/// Multiple ([`super::pedit_multi`]): selecting the objects, converting lines and arcs, the
/// options, the width for all, Join's fuzz distance.
MSelect,
MConvert,
MMain,
MWidth,
MFuzz,
}
#[derive(Default)]
pub(super) struct PeditM {
pub(super) phase: PPhase,
pub(super) h: Option<Handle>,
pub(super) history: Vec<EntityKind>,
original: Option<Vec<PolyVertex>>,
join: SelectPhase,
pub(super) vertex: usize,
vwidth: f64,
/// Break / Straighten: the second marked vertex, and which of the two runs.
pub(super) mark2: usize,
pub(super) breaking: bool,
/// Edit vertex Tangent directions by vertex position, used by Fit.
pub(super) tangents: Vec<(Vec2, Vec2)>,
/// Pieces split off by Break, with the history length after it (erased again by Undo).
pub(super) split_off: Vec<(usize, Vec<Handle>)>,
/// Multiple: the polylines being edited, their selection, and the drawing before each option
/// (with those polylines and their Decurve record) for Undo.
pub(super) multi: Vec<Handle>,
pub(super) msel: SelectPhase,
pub(super) mundo: Vec<(cadcraft_doc::Drawing, Vec<Handle>, std::collections::HashMap<Handle, Vec<PolyVertex>>)>,
/// Multiple: each polyline's vertices before its first Fit or Spline, which Decurve restores.
pub(super) morig: std::collections::HashMap<Handle, Vec<PolyVertex>>,
}
/// A Yes/No answer: Enter is Yes, anything but Y/Yes/N/No is refused so the question is asked again.
pub(super) fn yes_no(i: &Input) -> Result<bool> {
match i {
Input::Enter => Ok(true),
Input::Keyword(k) | Input::Text(k) => match k.trim().to_ascii_lowercase().as_str() {
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Err(other("Enter Yes or No.")),
},
_ => Err(other("Enter Yes or No.")),
}
}
/// Width sets one width for the whole polyline, so the vertices Decurve gives back take it too.
pub(super) fn clear_widths(vs: &mut [PolyVertex]) {
for v in vs {
v.start_width = 0.0;
v.end_width = 0.0;
}
}
impl PeditM {
pub(super) fn poly(&self, s: &Session) -> Option<LwPolyline> {
let e = s.doc().ok()?.entity(self.h?)?.clone();
match &e.kind {
EntityKind::LwPolyline(p) => Some(p.clone()),
_ => None,
}
}
pub(super) fn snapshot(&mut self, s: &Session) {
if let Some(h) = self.h
&& let Ok(e) = curves::entity(s, h)
{
self.history.push(e.kind);
}
}
pub(super) fn apply(&mut self, s: &mut Session, opt: &str, p: Value) -> Result<()> {
let h = self.h.ok_or_else(|| other("No polyline."))?;
self.snapshot(s);
if let Err(e) = pedit_apply(s, h, opt, &p) {
self.history.pop();
return Err(e);
}
Ok(())
}
}
impl Interactive for PeditM {
fn name(&self) -> &'static str {
"PEDIT"
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
let sel = s.selection();
// Several objects selected first: straight into Multiple, as in AutoCAD.
if sel.len() > 1 && s.settings.pickfirst {
return self.start_multi(s);
}
if let [h] = sel.as_slice() {
let k = curves::entity(s, *h)?.kind;
match k {
EntityKind::LwPolyline(_) => {
self.h = Some(*h);
self.original = self.poly(s).map(|p| p.vertices);
self.phase = PPhase::Main;
}
EntityKind::Line(_) | EntityKind::Arc(_) => {
self.h = Some(*h);
self.phase = PPhase::Convert;
}
_ => {}
}
}
Ok(Step::Continue)
}
fn prompt(&self, s: &Session) -> Prompt {
match self.phase {
PPhase::MSelect | PPhase::MConvert | PPhase::MMain | PPhase::MWidth | PPhase::MFuzz => self.multi_prompt(),
PPhase::Select => Prompt::new("Select polyline or", Accept::POINT).kw(&["Multiple"]),
PPhase::Convert => {
Prompt::new("Object selected is not a polyline. Do you want to turn it into one?", curves::KW).kw(&["Yes", "No"]).default("Y")
}
PPhase::Main => {
let closed = self.poly(s).is_some_and(|p| p.closed);
let first = if closed { "Open" } else { "Close" };
Prompt::new("Enter an option", curves::KW).kw(&[
first,
"Join",
"Width",
"Edit vertex",
"Fit",
"Spline",
"Decurve",
"Ltype gen",
"Reverse",
"Undo",
])
}
PPhase::Width => Prompt::new("Specify new width for all segments", Accept::NUMBER),
PPhase::Join => self.join.prompt(),
PPhase::Vertex => Prompt::new("Enter a vertex editing option", curves::KW)
.kw(&["Next", "Previous", "Break", "Insert", "Move", "Regen", "Straighten", "Tangent", "Width", "eXit"])
.default("N"),
PPhase::VMove => Prompt::new("Specify new location for marked vertex", Accept::POINT),
PPhase::VInsert => Prompt::new("Specify location for new vertex", Accept::POINT),
PPhase::VWidthStart => Prompt::new("Specify starting width for next segment", Accept::NUMBER),
PPhase::VWidthEnd => Prompt::new("Specify ending width for next segment", Accept::NUMBER).default(format!("{:.4}", self.vwidth)),
PPhase::VMark => Prompt::new("Enter an option", curves::KW).kw(&["Next", "Previous", "Go", "eXit"]).default("N"),
PPhase::VTangent => Prompt::new("Specify direction of vertex tangent", Accept::POINT_OR_NUMBER)
.base_opt(self.poly(s).and_then(|p| p.vertices.get(self.vertex).map(|v| v.p))),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let kw = |i: &Input| match i {
Input::Keyword(k) | Input::Text(k) => Some(k.trim().to_ascii_lowercase()),
_ => None,
};
match self.phase {
PPhase::MSelect | PPhase::MConvert | PPhase::MMain | PPhase::MWidth | PPhase::MFuzz => self.multi_input(s, i),
PPhase::Select => match i {
Input::Point(p) => {
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
let k = curves::entity(s, h)?.kind;
self.h = Some(h);
match k {
EntityKind::LwPolyline(pl) => {
self.original = Some(pl.vertices);
self.phase = PPhase::Main;
}
EntityKind::Line(_) | EntityKind::Arc(_) => self.phase = PPhase::Convert,
_ => {
self.h = None;
return Err(other("Object selected is not a polyline."));
}
}
s.set_selection(vec![h]);
Ok(Step::Continue)
}
Input::Keyword(k) if k.eq_ignore_ascii_case("multiple") || k.eq_ignore_ascii_case("m") => self.start_multi(s),
Input::Keyword(_) => Err(other("Invalid option keyword.")),
Input::Enter => Ok(Step::Cancel),
_ => Ok(Step::Continue),
},
PPhase::Convert => {
if !yes_no(&i)? {
return Ok(Step::Done);
}
let h = self.h.ok_or_else(|| other("No object."))?;
let k = curves::entity(s, h)?.kind;
let pl = as_lwpoly(&k).ok_or_else(|| other("Cannot convert."))?;
self.original = Some(pl.vertices.clone());
set_kind(s, h, EntityKind::LwPolyline(pl))?;
self.phase = PPhase::Main;
Ok(Step::Continue)
}
PPhase::Main => {
let Some(k) = kw(&i) else {
if i == Input::Enter {
s.set_selection(Vec::new());
return Ok(Step::Done);
}
return Ok(Step::Continue);
};
match k.as_str() {
"open" | "o" => self.apply(s, "open", json!({}))?,
"close" | "c" => self.apply(s, "close", json!({}))?,
"join" | "j" => {
self.join = SelectPhase::default();
self.phase = PPhase::Join;
}
"width" | "w" => self.phase = PPhase::Width,
"edit vertex" | "e" => {
self.vertex = 0;
self.phase = PPhase::Vertex;
}
"fit" | "f" => self.apply(s, "fit", json!({ "tangents": self.tangents_json(s) }))?,
"spline" | "s" => self.apply(s, "spline", json!({}))?,
"decurve" | "d" => {
// Within the session, Decurve restores the frame before Fit/Spline.
let h = self.h.ok_or_else(|| other("No polyline."))?;
self.snapshot(s);
match (self.poly(s), self.original.clone()) {
(Some(mut pl), Some(orig)) => {
pl.vertices = orig.into_iter().map(|v| PolyVertex { bulge: 0.0, ..v }).collect();
set_kind(s, h, EntityKind::LwPolyline(pl))?;
}
_ => {
self.history.pop();
self.apply(s, "decurve", json!({}))?;
}
}
}
"ltype gen" | "l" => self.apply(s, "ltypegen", json!({}))?,
"reverse" | "r" => self.apply(s, "reverse", json!({}))?,
"undo" | "u" => {
self.undo_split(s)?;
if let (Some(h), Some(k)) = (self.h, self.history.pop()) {
set_kind(s, h, k)?;
}
}
_ => return Err(other("Invalid option keyword.")),
}
Ok(Step::Continue)
}
PPhase::Width => {
if let Input::Text(t) = &i {
let w = number(t).filter(|w| *w >= 0.0).ok_or_else(|| other("Requires a non-negative width."))?;
self.apply(s, "width", json!({ "width": w }))?;
if let Some(o) = &mut self.original {
clear_widths(o);
}
}
self.phase = PPhase::Main;
Ok(Step::Continue)
}
PPhase::Join => match self.join.feed(s, &i)? {
SelOutcome::More => Ok(Step::Continue),
SelOutcome::Empty => {
self.phase = PPhase::Main;
Ok(Step::Continue)
}
SelOutcome::Done(hs) => {
let h = self.h.ok_or_else(|| other("No polyline."))?;
self.snapshot(s);
let n = pedit_join(s, h, &hs, 0.0)?;
s.echo(format!("{n} segments added to polyline"));
s.set_selection(vec![h]);
self.phase = PPhase::Main;
Ok(Step::Continue)
}
},
PPhase::Vertex => {
let n = self.poly(s).map(|p| p.vertices.len()).unwrap_or(0).max(1);
let k = kw(&i).unwrap_or_else(|| if i == Input::Enter { "n".into() } else { String::new() });
match k.as_str() {
"next" | "n" => self.vertex = (self.vertex + 1) % n,
"previous" | "p" => self.vertex = (self.vertex + n - 1) % n,
"move" | "m" => self.phase = PPhase::VMove,
"insert" | "i" => self.phase = PPhase::VInsert,
"straighten" | "s" | "break" | "b" => {
self.breaking = k.starts_with('b');
self.mark2 = self.vertex;
self.phase = PPhase::VMark;
}
"tangent" | "t" => self.phase = PPhase::VTangent,
// The display is always current.
"regen" | "r" => {}
"width" | "w" => self.phase = PPhase::VWidthStart,
"exit" | "x" => self.phase = PPhase::Main,
"" => {}
_ => return Err(other("Invalid option keyword.")),
}
Ok(Step::Continue)
}
PPhase::VMove | PPhase::VInsert => {
if let Input::Point(p) = i {
let action = if self.phase == PPhase::VMove { "move" } else { "insert" };
self.apply(s, "vertex", json!({ "action": action, "index": self.vertex, "to": [p.x, p.y] }))?;
if action == "insert" {
self.vertex += 1;
}
}
self.phase = PPhase::Vertex;
Ok(Step::Continue)
}
PPhase::VWidthStart => {
if let Input::Text(t) = &i {
self.vwidth = number(t).filter(|w| *w >= 0.0).ok_or_else(|| other("Requires a non-negative width."))?;
self.phase = PPhase::VWidthEnd;
} else {
self.phase = PPhase::Vertex;
}
Ok(Step::Continue)
}
PPhase::VMark => {
let k = kw(&i).unwrap_or_else(|| if i == Input::Enter { "n".into() } else { String::new() });
self.mark_input(s, &k)?;
Ok(Step::Continue)
}
PPhase::VTangent => {
self.tangent_input(s, &i)?;
Ok(Step::Continue)
}
PPhase::VWidthEnd => {
let ew = match &i {
Input::Text(t) => number(t).filter(|w| *w >= 0.0).ok_or_else(|| other("Requires a non-negative width."))?,
_ => self.vwidth,
};
self.apply(s, "vertex", json!({ "action": "width", "index": self.vertex, "startWidth": self.vwidth, "endWidth": ew }))?;
self.phase = PPhase::Vertex;
Ok(Step::Continue)
}
}
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
if !matches!(
self.phase,
PPhase::Vertex | PPhase::VMove | PPhase::VInsert | PPhase::VWidthStart | PPhase::VWidthEnd | PPhase::VMark | PPhase::VTangent
) {
return Vec::new();
}
let Some(v) = self.poly(s).and_then(|p| p.vertices.get(self.vertex).copied()) else { return Vec::new() };
let k = s.pixel_size() * 8.0;
let mut out = vec![line(v.p - Vec2::new(k, k), v.p + Vec2::new(k, k)), line(v.p - Vec2::new(k, -k), v.p + Vec2::new(k, -k))];
if matches!(self.phase, PPhase::VMove | PPhase::VInsert | PPhase::VTangent) {
out.push(line(v.p, c));
}
if let (PPhase::VMark, Some(m)) = (self.phase, self.poly(s).and_then(|p| p.vertices.get(self.mark2).copied())) {
out.extend([line(m.p - Vec2::new(k, k), m.p + Vec2::new(k, k)), line(m.p - Vec2::new(k, -k), m.p + Vec2::new(k, -k))]);
}
out
}
}