Files
cadcraft/crates/engine/src/cmd/edit.rs
T
2026-10-12 03:21:18 -04:00

699 lines
26 KiB
Rust

//! Edit menu: undo/redo, clipboard, selection.
use cadcraft_doc::{Entity, Handle, entity_bounds};
use cadcraft_geom::{Bounds2, Mat3, Vec2};
use serde_json::{Value, json};
use super::machines::{SelOutcome, SelectPhase, SelectRun};
use super::*;
use crate::undoctl::Control;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
vec![
// U undoes the last operation at once (Edit > Undo, Cmd+Z); UNDO asks how many.
CommandSpec::new("u", "Undo", run_u).menu(&["Edit", "Undo"]).key("Cmd+Z").params("{}").noundo(),
CommandSpec::new("undo", "Undo", run_undo)
.params(
"{count?: n} | {mode: mark|begin|end} | {mode: back, confirm?: bool (no mark: undo everything; default true)} | {mode: auto, on: bool} | {mode: control, control: all|none|one|combine|layer, on?: bool (combine/layer)}",
)
.noundo()
.interactive(|_| Ok(Box::new(UndoM::default()))),
CommandSpec::new("redo", "Redo", run_redo).menu(&["Edit", "Redo"]).key("Cmd+Shift+Z").params("{count?: n}").noundo(),
CommandSpec::new("mredo", "Redo", run_mredo).params("{count?: n} | {all: true}").noundo().interactive(|_| Ok(Box::new(MredoM))),
CommandSpec::new("cutclip", "Cut", run_cut)
.menu(&["Edit", "Cut"])
.key("Cmd+X")
.params("{handles?}")
.interactive(|_| Ok(Box::new(SelectRun::new("cutclip", "CUTCLIP")))),
CommandSpec::new("copyclip", "Copy", run_copyclip)
.menu(&["Edit", "Copy"])
.key("Cmd+C")
.params("{handles?}")
.noundo()
.interactive(|_| Ok(Box::new(SelectRun::new("copyclip", "COPYCLIP")))),
CommandSpec::new("copybase", "Copy with Base Point", run_copybase)
.menu(&["Edit", "Copy with Base Point"])
.key("Cmd+Shift+C")
.params("{handles?, base: [x,y]}")
.noundo()
.interactive(|_| Ok(Box::new(CopyBaseM::default()))),
CommandSpec::new("pasteclip", "Paste", run_paste)
.menu(&["Edit", "Paste"])
.key("Cmd+V")
.params("{at?: [x,y]} (where the base point lands; default: the original coordinates)")
.enabled(clipboard::has_clip)
.interactive(|_| Ok(Box::new(PasteM { mode: PasteMode::Clip }))),
CommandSpec::new("pasteblock", "Paste as Block", run_pasteblock)
.menu(&["Edit", "Paste as Block"])
.key("Cmd+Shift+V")
.params("{at?: [x,y]} (insertion point; default: the original coordinates)")
.enabled(clipboard::has_clip)
.interactive(|_| Ok(Box::new(PasteM { mode: PasteMode::Block }))),
CommandSpec::new("pasteorig", "Paste to Original Coordinates", run_pasteorig)
.menu(&["Edit", "Paste to Original Coordinates"])
.params("{} (only into a drawing other than the one copied from)")
.enabled(clipboard::can_paste_orig)
.interactive(|_| Ok(Box::new(PasteM { mode: PasteMode::Orig }))),
CommandSpec::new("erase.selection", "Clear", run_clear).menu(&["Edit", "Clear"]).key("Delete").enabled(has_selection).params("{}"),
CommandSpec::new("selectall", "Select All", run_selectall).menu(&["Edit", "Select All"]).key("Cmd+A").alias(&["ai_selall"]).noundo(),
CommandSpec::new("ai_deselect", "Deselect All", run_deselect).params("{}").noundo(),
CommandSpec::new("select", "Select", run_select)
.params("{handles: [hex]} | {window: [[x,y],[x,y]], crossing?: bool} | {at: [x,y]} | {clear: true} | {add?: bool}")
.noundo()
.interactive(|_| Ok(Box::new(SelectM::default()))),
]
}
/// A programmatic undo/redo (`engine.execute`, MCP, control channel, scripts) first ends a running
/// command the way Esc does, exactly as `Session::start` does for Cmd+Z, the menu or the command
/// line. Otherwise it would swap the drawing under the command and pop the previous command's step.
fn end_running_command(s: &mut Session) {
if s.running.is_some() {
s.cancel();
}
}
/// The UNDO options of the JSON form (`mode`) and the command line (keywords).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum UndoMode {
Count,
Mark,
Back,
Begin,
End,
Auto,
Control,
}
fn undo_mode(m: &str) -> Option<UndoMode> {
Some(match m.trim().to_ascii_lowercase().as_str() {
"" | "count" => UndoMode::Count,
"mark" => UndoMode::Mark,
"back" => UndoMode::Back,
"begin" | "group" => UndoMode::Begin,
"end" => UndoMode::End,
"auto" => UndoMode::Auto,
"control" => UndoMode::Control,
_ => return None,
})
}
fn on_param(p: &Value, key: &str) -> Option<bool> {
let v = p.get(key)?;
v.as_bool().or_else(|| v.as_i64().map(|i| i != 0)).or_else(|| match v.as_str()?.trim().to_ascii_lowercase().as_str() {
"on" | "yes" | "y" | "true" | "1" => Some(true),
"off" | "no" | "n" | "false" | "0" => Some(false),
_ => None,
})
}
/// The JSON result of an UNDO option: `message` plus the new UNDOCTL bits and mark count.
fn undo_state(s: &Session, message: impl Into<String>) -> Result<Value> {
let st = s.state()?;
Ok(json!({ "message": message.into(), "undoctl": st.undo_ctl.bits(), "marks": st.undo_marks() }))
}
fn undo_off() -> EngineError {
EngineError::Other("Undo is turned off (UNDO Control None); UNDO Control All turns it on.".into())
}
fn run_undo(s: &mut Session, p: &Value) -> Result<Value> {
end_running_command(s);
let mode = match p.get("mode") {
None | Some(Value::Null) => UndoMode::Count,
Some(v) => v.as_str().and_then(undo_mode).ok_or_else(|| bad("undo", "mode: count, mark, back, begin, end, auto or control"))?,
};
let control = s.state()?.undo_ctl.control;
if control != Control::All && !matches!(mode, UndoMode::Count | UndoMode::Control | UndoMode::Auto) {
return Err(EngineError::Other(format!("UNDO {mode:?} needs UNDO Control All.")));
}
match mode {
UndoMode::Count => {
if control == Control::None {
return Err(undo_off());
}
let n = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 1000);
// Undoing inside a BEgin group ends it: the whole group is undone as one step.
s.state_mut()?.end_undo_group();
let mut labels = Vec::new();
for _ in 0..n {
match s.undo()? {
Some(l) => labels.push(l),
None => break,
}
}
if labels.is_empty() {
return Ok(json!({ "message": "Everything has been undone" }));
}
Ok(json!({ "undone": labels, "message": labels.join(", ").to_string() }))
}
UndoMode::Mark => {
s.state_mut()?.undo_mark();
undo_state(s, "Mark set")
}
UndoMode::Back => undo_back(s, on_param(p, "confirm").unwrap_or(true)),
UndoMode::Begin => {
let ended = s.state_mut()?.begin_undo_group();
undo_state(s, if ended { "Group ended; new group begun" } else { "Group begun" })
}
UndoMode::End => {
let ended = s.state_mut()?.end_undo_group();
undo_state(s, if ended { "Group ended" } else { "No group to end" })
}
UndoMode::Auto => {
let on = on_param(p, "on").ok_or_else(|| bad("undo", "`on` (true/false) is required for mode auto"))?;
s.state_mut()?.undo_ctl.auto = on;
undo_state(s, format!("UNDO Auto {}", if on { "on" } else { "off" }))
}
UndoMode::Control => {
let c = str_param(p, "control").unwrap_or("all").trim().to_ascii_lowercase();
let st = s.state_mut()?;
let msg = match c.as_str() {
"all" => {
st.set_undo_control(Control::All);
"UNDO Control All"
}
"none" => {
st.set_undo_control(Control::None);
"UNDO Control None: undo is off"
}
"one" => {
st.set_undo_control(Control::One);
"UNDO Control One: only the last step can be undone"
}
"combine" => {
st.undo_ctl.combine_view = on_param(p, "on").unwrap_or(true);
"UNDO Control Combine set"
}
"layer" => {
st.undo_ctl.combine_layer = on_param(p, "on").unwrap_or(true);
"UNDO Control Layer set"
}
_ => return Err(bad("undo", "control: all, none, one, combine or layer")),
};
undo_state(s, msg)
}
}
}
/// UNDO Back: undo to the last mark (which goes with it), or with no mark everything when
/// `confirm` is true.
fn undo_back(s: &mut Session, confirm: bool) -> Result<Value> {
let st = s.state_mut()?;
st.end_undo_group();
let depth = st.undo.len();
st.undo_ctl.marks.retain(|m| *m <= depth);
let target = match st.undo_ctl.marks.pop() {
Some(m) => m,
None if confirm => 0,
None => return undo_state(s, "Nothing undone"),
};
let mut labels = Vec::new();
// Each step pops one entry, so this ends within the stack's depth.
while s.state()?.undo.len() > target {
match s.undo()? {
Some(l) => labels.push(l),
None => break,
}
}
let msg = match (labels.is_empty(), target) {
(true, 0) => "Everything has been undone".to_string(),
(true, _) => "Back at the mark".to_string(),
(false, _) => labels.join(", "),
};
let mut r = undo_state(s, msg)?;
r["undone"] = json!(labels);
Ok(r)
}
fn run_u(s: &mut Session, _p: &Value) -> Result<Value> {
run_undo(s, &json!({}))
}
/// What the UNDO prompt machine asks.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum UndoAsk {
/// "Enter the number of operations to undo or [Auto/Control/BEgin/End/Mark/Back]".
#[default]
Main,
Auto,
Control,
Combine,
Layer,
/// Back with no mark: undo everything?
BackAll,
}
/// UNDO at the command line: a number of operations (default 1) or Auto, Control (All, None, One,
/// Combine, Layer), BEgin, End, Mark or Back. With Control None only Control is offered, with One
/// a count or Control.
#[derive(Default)]
struct UndoM {
ask: UndoAsk,
}
impl UndoM {
fn finish(s: &mut Session, r: Result<Value>) -> Result<Step> {
match r {
Ok(v) => {
if let Some(m) = v.get("message").and_then(Value::as_str) {
s.echo(m.to_string());
}
}
Err(e) => s.echo(e.to_string()),
}
Ok(Step::Done)
}
}
impl Interactive for UndoM {
fn name(&self) -> &'static str {
"UNDO"
}
fn prompt(&self, s: &Session) -> Prompt {
let control = s.state().map(|st| st.undo_ctl.control).unwrap_or_default();
let ask = if control == Control::None && self.ask == UndoAsk::Main { UndoAsk::Control } else { self.ask };
let yes_no = |b: bool| if b { "Yes" } else { "No" };
let ctl = s.state().map(|st| st.undo_ctl.clone()).unwrap_or_default();
match ask {
UndoAsk::Auto => Prompt::new("Enter UNDO Auto mode", Accept::TEXT).kw(&["ON", "OFF"]).default(if ctl.auto { "ON" } else { "OFF" }),
UndoAsk::Control => {
Prompt::new("Enter an UNDO control option", Accept::TEXT).kw(&["All", "None", "One", "Combine", "Layer"]).default("All")
}
UndoAsk::Combine => {
Prompt::new("Combine zooms and pans into one undo step?", Accept::TEXT).kw(&["Yes", "No"]).default(yes_no(ctl.combine_view))
}
UndoAsk::Layer => {
Prompt::new("Combine layer property changes into one undo step?", Accept::TEXT).kw(&["Yes", "No"]).default(yes_no(ctl.combine_layer))
}
UndoAsk::BackAll => Prompt::new("No mark: undo everything?", Accept::TEXT).kw(&["Yes", "No"]).default("Yes"),
UndoAsk::Main if control == Control::One => {
Prompt::new("Enter the number of operations to undo", Accept::NUMBER).kw(&["Control"]).default("1")
}
UndoAsk::Main => Prompt::new("Enter the number of operations to undo", Accept::NUMBER)
.kw(&["Auto", "Control", "BEgin", "End", "Mark", "Back"])
.default("1"),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let prompt = self.prompt(s);
if s.state()?.undo_ctl.control == Control::None && self.ask == UndoAsk::Main {
self.ask = UndoAsk::Control;
}
let kw = match &i {
Input::Cancel => return Ok(Step::Cancel),
Input::Enter => prompt.default.clone(),
Input::Keyword(k) | Input::Text(k) => prompt.match_keyword(k),
_ => return Ok(Step::Continue),
};
let yes = kw.as_deref() == Some("Yes");
match self.ask {
UndoAsk::Main => {
// Enter takes the default count, not a keyword.
if let Some(k) = kw.as_deref().filter(|_| !matches!(i, Input::Enter)) {
match k {
"Auto" => self.ask = UndoAsk::Auto,
"Control" => self.ask = UndoAsk::Control,
"Back" if s.state()?.undo_marks() == 0 => self.ask = UndoAsk::BackAll,
_ => {
let r = run_undo(s, &json!({ "mode": k.to_ascii_lowercase() }));
return Self::finish(s, r);
}
}
return Ok(Step::Continue);
}
let n = match &i {
Input::Text(t) => {
t.trim().parse::<u64>().ok().filter(|n| *n >= 1).ok_or_else(|| EngineError::Other("Requires a positive integer.".into()))?
}
_ => 1,
};
let r = run_undo(s, &json!({ "count": n }));
Self::finish(s, r)
}
UndoAsk::Auto => {
let on = match kw.as_deref() {
Some("ON") => true,
Some("OFF") => false,
_ => return Err(EngineError::Other("Enter ON or OFF.".into())),
};
let r = run_undo(s, &json!({ "mode": "auto", "on": on }));
Self::finish(s, r)
}
UndoAsk::Control => match kw.as_deref() {
Some("Combine") => {
self.ask = UndoAsk::Combine;
Ok(Step::Continue)
}
Some("Layer") => {
self.ask = UndoAsk::Layer;
Ok(Step::Continue)
}
Some(c) => {
let r = run_undo(s, &json!({ "mode": "control", "control": c.to_ascii_lowercase() }));
Self::finish(s, r)
}
None => Err(EngineError::Other("Enter All, None, One, Combine or Layer.".into())),
},
UndoAsk::Combine | UndoAsk::Layer => {
if kw.is_none() {
return Err(EngineError::Other("Answer Yes or No.".into()));
}
let control = if self.ask == UndoAsk::Combine { "combine" } else { "layer" };
let r = run_undo(s, &json!({ "mode": "control", "control": control, "on": yes }));
Self::finish(s, r)
}
UndoAsk::BackAll => {
if kw.is_none() {
return Err(EngineError::Other("Answer Yes or No.".into()));
}
let r = run_undo(s, &json!({ "mode": "back", "confirm": yes }));
Self::finish(s, r)
}
}
}
}
/// Redo up to `n` undone steps.
fn redo_steps(s: &mut Session, n: u64) -> Result<Value> {
end_running_command(s);
let mut labels = Vec::new();
for _ in 0..n {
match s.redo()? {
Some(l) => labels.push(l),
None => break,
}
}
Ok(json!({ "redone": labels, "message": if labels.is_empty() { "Nothing to redo".to_string() } else { labels.join(", ") } }))
}
fn run_redo(s: &mut Session, p: &Value) -> Result<Value> {
redo_steps(s, p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 1000))
}
/// MREDO: `{count}` steps, or `{all: true}` every undone step.
fn run_mredo(s: &mut Session, p: &Value) -> Result<Value> {
if p.get("all").and_then(Value::as_bool).unwrap_or(false) {
let n = s.state()?.redo.len() as u64;
return redo_steps(s, n.max(1));
}
run_redo(s, p)
}
/// MREDO at the command line: a number of actions, All or Last.
struct MredoM;
impl Interactive for MredoM {
fn name(&self) -> &'static str {
"MREDO"
}
fn prompt(&self, _s: &Session) -> Prompt {
Prompt::new("Enter number of actions", Accept::NUMBER).kw(&["All", "Last"])
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let p = match &i {
Input::Cancel => return Ok(Step::Cancel),
Input::Enter => return Ok(Step::Done),
Input::Keyword(k) | Input::Text(k) => match self.prompt(s).match_keyword(k).as_deref() {
Some("All") => json!({ "all": true }),
Some(_) => json!({ "count": 1 }),
None => {
let n =
k.trim().parse::<u64>().ok().filter(|n| *n >= 1).ok_or_else(|| EngineError::Other("Requires a positive integer.".into()))?;
json!({ "count": n })
}
},
_ => return Ok(Step::Continue),
};
let r = run_mredo(s, &p)?;
if let Some(m) = r.get("message").and_then(Value::as_str) {
s.echo(m.to_string());
}
Ok(Step::Done)
}
}
fn copy_to_clip(s: &mut Session, hs: &[Handle], base: Option<Vec2>) -> Result<usize> {
let d = s.doc()?;
let ents: Vec<Entity> = hs.iter().filter_map(|h| d.entity(*h).map(|e| (**e).clone())).collect();
let b = ents.iter().fold(Bounds2::EMPTY, |acc, e| acc.union(&entity_bounds(d, e, 0)));
let base = base.unwrap_or(if b.min.is_finite() { b.min } else { Vec2::ZERO });
let n = ents.len();
clipboard::copy(s, ents, base)?;
Ok(n)
}
fn run_copyclip(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
let n = copy_to_clip(s, &hs, None)?;
Ok(json!({ "copied": n }))
}
fn run_copybase(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
let base = point_param(p, "base");
let n = copy_to_clip(s, &hs, base)?;
Ok(json!({ "copied": n }))
}
fn run_cut(s: &mut Session, p: &Value) -> Result<Value> {
let mut hs = targets(s, p)?;
hs.retain(|h| !super::curves::is_locked(s, *h));
let n = copy_to_clip(s, &hs, None)?;
let d = s.doc_mut()?;
for h in &hs {
d.remove_entity(*h);
}
s.set_selection(Vec::new());
Ok(json!({ "cut": n }))
}
/// Paste the clipboard moved by `offset` (the clipboard brings the layers, styles and blocks its
/// objects need).
fn paste(s: &mut Session, offset: Vec2) -> Result<Vec<Handle>> {
clipboard::paste(s, &Mat3::translate(offset))
}
fn run_paste(s: &mut Session, p: &Value) -> Result<Value> {
if s.clipboard.is_empty() {
return Err(bad("pasteclip", "the clipboard is empty"));
}
let off = match point_param(p, "at") {
Some(at) => at - s.clipboard_base,
None => Vec2::ZERO,
};
let r = paste(s, off)?;
Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::<Vec<_>>() }))
}
fn run_pasteblock(s: &mut Session, p: &Value) -> Result<Value> {
let at = point_param(p, "at").unwrap_or(s.clipboard_base);
let h = clipboard::paste_block(s, at)?;
let block = match s.doc()?.entity(h).map(|e| &e.kind) {
Some(cadcraft_doc::EntityKind::Insert(i)) => i.block.clone(),
_ => String::new(),
};
Ok(json!({ "handle": h.hex(), "block": block }))
}
fn run_pasteorig(s: &mut Session, _p: &Value) -> Result<Value> {
let r = paste(s, Vec2::ZERO)?;
Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::<Vec<_>>() }))
}
fn run_clear(s: &mut Session, _p: &Value) -> Result<Value> {
let mut hs = s.selection();
hs.retain(|h| !super::curves::is_locked(s, *h));
let d = s.doc_mut()?;
let mut n = 0;
for h in hs {
if d.remove_entity(h).is_some() {
n += 1;
}
}
s.set_selection(Vec::new());
Ok(json!({ "erased": n }))
}
fn run_selectall(s: &mut Session, _p: &Value) -> Result<Value> {
let space = s.space();
let d = s.doc()?;
let hs: Vec<Handle> = d
.space(&space)
.map(|st| st.iter().filter(|e| d.is_visible(e) && d.layer(&e.common.layer).is_none_or(|l| !l.locked)).map(|e| e.handle).collect())
.unwrap_or_default();
let n = hs.len();
s.set_selection(hs);
Ok(json!({ "selected": n }))
}
fn run_deselect(s: &mut Session, _p: &Value) -> Result<Value> {
s.set_selection(Vec::new());
Ok(json!({ "selected": 0 }))
}
fn run_select(s: &mut Session, p: &Value) -> Result<Value> {
if bool_or(p, "clear", false) {
s.set_selection(Vec::new());
return Ok(json!({ "selected": 0 }));
}
let space = s.space();
let add = bool_or(p, "add", false);
let picked: Vec<Handle> = if let Some(w) = points_param(p, "window") {
let (Some(a), Some(b)) = (w.first(), w.get(1)) else { return Err(bad("select", "window needs 2 corners")) };
crate::select::select_window(s.doc()?, &space, Bounds2::new(*a, *b), bool_or(p, "crossing", false))
} else if let Some(at) = point_param(p, "at") {
let ap = f64_or(p, "aperture", s.pixel_size() * s.settings.pickbox * 1.5);
crate::select::pick(s.doc()?, &space, at, ap).into_iter().collect()
} else if let Some(f) = points_param(p, "fence") {
crate::select::select_fence(s.doc()?, &space, &f)
} else {
targets(s, p)?
};
let mut sel = if add { s.selection() } else { Vec::new() };
sel.extend(picked);
s.set_selection(sel);
let sel = s.selection();
Ok(json!({ "selected": sel.len(), "handles": sel.iter().map(|h| h.hex()).collect::<Vec<_>>() }))
}
#[derive(Clone, Copy, PartialEq)]
enum PasteMode {
/// PASTECLIP: at an insertion point.
Clip,
/// PASTEBLOCK: as a block, at an insertion point.
Block,
/// PASTEORIG: at the original coordinates, without asking.
Orig,
}
struct PasteM {
mode: PasteMode,
}
impl Interactive for PasteM {
fn name(&self) -> &'static str {
match self.mode {
PasteMode::Clip => "PASTECLIP",
PasteMode::Block => "PASTEBLOCK",
PasteMode::Orig => "PASTEORIG",
}
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
if s.clipboard.is_empty() {
s.echo("Clipboard is empty.");
return Ok(Step::Done);
}
if self.mode == PasteMode::Orig {
paste(s, Vec2::ZERO)?;
return Ok(Step::Done);
}
Ok(Step::Continue)
}
fn prompt(&self, _s: &Session) -> Prompt {
Prompt::new("Specify insertion point", Accept::POINT)
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match i {
Input::Point(p) if self.mode == PasteMode::Block => {
clipboard::paste_block(s, p)?;
Ok(Step::Done)
}
Input::Point(p) => {
let base = s.clipboard_base;
paste(s, p - base)?;
Ok(Step::Done)
}
Input::Enter => Ok(Step::Done),
_ => Ok(Step::Continue),
}
}
fn drag(&self, s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
if self.mode == PasteMode::Orig || s.clipboard.is_empty() {
return None;
}
Some(crate::prompt::Drag { handles: Vec::new(), matrix: Mat3::translate(c - s.clipboard_base), extra: Vec::new(), clipboard: true })
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<cadcraft_doc::EntityKind> {
let m = Mat3::translate(c - s.clipboard_base);
s.clipboard
.iter()
.take(500)
.map(|e| {
let mut k = e.kind.clone();
k.transform(&m);
k
})
.collect()
}
}
#[derive(Default)]
struct SelectM {
sel: SelectPhase,
}
impl Interactive for SelectM {
fn name(&self) -> &'static str {
"SELECT"
}
fn prompt(&self, _s: &Session) -> Prompt {
self.sel.prompt()
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match self.sel.feed(s, &i)? {
SelOutcome::More => Ok(Step::Continue),
_ => Ok(Step::Done),
}
}
}
/// COPYBASE: base point first, then "Select objects:" (skipped with a pickfirst selection).
#[derive(Default)]
struct CopyBaseM {
base: Option<Vec2>,
sel: SelectPhase,
}
impl CopyBaseM {
fn copy(&self, s: &mut Session, hs: &[Handle]) -> Result<Step> {
copy_to_clip(s, hs, self.base)?;
Ok(Step::Done)
}
}
impl Interactive for CopyBaseM {
fn name(&self) -> &'static str {
"COPYBASE"
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
self.sel = SelectPhase::begin(s);
Ok(Step::Continue)
}
fn prompt(&self, _s: &Session) -> Prompt {
if self.base.is_none() { Prompt::new("Specify base point", Accept::POINT) } else { self.sel.prompt() }
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if self.base.is_none() {
if let Input::Point(p) = i {
self.base = Some(p);
if self.sel.done {
let hs = self.sel.picked.clone();
return self.copy(s, &hs);
}
}
return Ok(Step::Continue);
}
match self.sel.feed(s, &i)? {
SelOutcome::More => Ok(Step::Continue),
SelOutcome::Empty => Ok(Step::Done),
SelOutcome::Done(hs) => {
let step = self.copy(s, &hs)?;
s.set_selection(Vec::new());
Ok(step)
}
}
}
}