mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 03:50:04 -06:00
# Conflicts: # crates/engine/src/prompt.rs # crates/engine/src/session/execute.rs # crates/io/src/dxf_read/entities.rs # crates/ui-egui/src/canvas.rs # crates/ui-egui/src/cmdline.rs
549 lines
24 KiB
Rust
549 lines
24 KiB
Rust
use crate::DisplayOnly;
|
|
|
|
use super::{
|
|
Arc, Bounds2, Drag, Drawing, EngineError, Entity, Handle, Input, PendingWindow, Prompt, Result, Running, Session, Snapshot, Step, Value, Vec2,
|
|
assoc, cmd, find_command, guard, pointmod, select, snap, sysvars,
|
|
};
|
|
|
|
impl Session {
|
|
// ---------------- interactive commands ----------------
|
|
|
|
/// Start a command by name or alias as if typed at the command line.
|
|
pub fn start(&mut self, name: &str) -> Result<()> {
|
|
self.last_polar = None;
|
|
self.pick_hint = None;
|
|
let name = name.trim();
|
|
let (transparent, name) = match name.strip_prefix('\'') {
|
|
Some(n) => (true, n),
|
|
None => (false, name),
|
|
};
|
|
let lower = name.trim_start_matches(['_', '.', '-']).to_ascii_lowercase();
|
|
// A command-line form with an id of its own (-ATTEDIT) is not the command it prefixes.
|
|
let dashed = cmd::resolve_alias(&name.trim_start_matches(['_', '.']).to_ascii_lowercase());
|
|
let id = if find_command(&dashed).is_some() { dashed } else { cmd::resolve_alias(&lower) };
|
|
let Some(spec) = find_command(&id) else {
|
|
// A command defined in LISP: (defun c:NAME …).
|
|
if !transparent && let Some(f) = self.lisp_command(&lower) {
|
|
if self.running.is_some() {
|
|
self.cancel();
|
|
}
|
|
self.last_command = Some(lower.clone());
|
|
self.echo(format!("Command: {}", lower.to_ascii_uppercase()));
|
|
return self.lisp_run_command(&lower, f);
|
|
}
|
|
// A bare system variable name (ORTHOMODE, GRIDMODE…) acts as SETVAR on that variable.
|
|
let var = lower.to_ascii_uppercase();
|
|
if !name.starts_with(['_', '.', '-']) && sysvars::get(self, &var).is_some() {
|
|
self.start(if transparent { "'setvar" } else { "setvar" })?;
|
|
return self.input(Input::Text(var));
|
|
}
|
|
return Err(EngineError::UnknownCommand(name.to_string()));
|
|
};
|
|
if transparent && spec.transparent && self.running.is_some() {
|
|
// Transparent commands (zoom, pan…) run without cancelling the active one.
|
|
if spec.interactive.is_none() {
|
|
self.echo(format!(">>{}", spec.label));
|
|
return self.execute(spec.id, &Value::Null).map(|_| ());
|
|
}
|
|
if self.suspended.is_some() {
|
|
self.echo("A transparent command is already in progress.");
|
|
return Ok(());
|
|
}
|
|
}
|
|
let nested = transparent && spec.transparent && self.running.is_some() && spec.interactive.is_some();
|
|
// Ctrl+Z / U / UNDO while a command's prompt offers its own Undo keyword (LINE, PLINE…)
|
|
// takes back the last segment and keeps the command running, as the keyword does.
|
|
if !nested && matches!(spec.id, "u" | "undo") && self.current_prompt().is_some_and(|p| p.keywords.iter().any(|k| k == "Undo")) {
|
|
self.echo(format!("Command: {}", spec.id.to_ascii_uppercase()));
|
|
return self.input(Input::Keyword("Undo".into()));
|
|
}
|
|
if !nested && self.running.is_some() {
|
|
self.cancel();
|
|
}
|
|
(spec.enabled)(self).map_err(|m| EngineError::Disabled(spec.id.into(), m))?;
|
|
if let Some(why) = cmd::blocks::bedit::refused_in_editor(self, spec.id) {
|
|
return Err(EngineError::Disabled(spec.id.into(), why));
|
|
}
|
|
if nested {
|
|
// The outer command stays the one Enter repeats.
|
|
self.echo(format!(">>{}", spec.id.to_ascii_uppercase()));
|
|
} else {
|
|
self.last_command = Some(spec.id.to_string());
|
|
self.echo(format!("Command: {}", spec.id.to_ascii_uppercase()));
|
|
}
|
|
match spec.interactive {
|
|
Some(factory) => {
|
|
let machine = guard::Guarded::create(spec.id, factory, self)?;
|
|
let st = self.state()?;
|
|
let before = st.doc.clone();
|
|
let selection_before = st.selection.clone();
|
|
if nested {
|
|
self.suspended = self.running.take();
|
|
}
|
|
self.running = Some(Running { id: spec.id.to_string(), machine, before, selection_before });
|
|
self.pending_window = None;
|
|
// Some commands complete immediately (e.g. ERASE with a pickfirst selection).
|
|
self.feed(None)?;
|
|
Ok(())
|
|
}
|
|
None => {
|
|
let r = self.execute(spec.id, &Value::Null)?;
|
|
if let Some(s) = r.as_str() {
|
|
self.echo(s.to_string());
|
|
} else if !r.is_null()
|
|
&& let Some(msg) = r.get("message").and_then(Value::as_str)
|
|
{
|
|
for l in msg.lines() {
|
|
self.echo(l.to_string());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Feed an object-snap pick, retaining its viewport origin for measurements. Point modifiers
|
|
/// that combine or constrain coordinates produce ordinary drawing-space points.
|
|
pub fn input_snapped(&mut self, hit: snap::SnapHit) -> Result<()> {
|
|
let source = hit.viewport.filter(|_| self.point_mods.iter().all(|f| matches!(f, pointmod::Frame::Snap { mode } if *mode != 0)));
|
|
let previous = std::mem::replace(&mut self.picked_viewport, source);
|
|
let result = self.input(hit.input());
|
|
self.picked_viewport = previous;
|
|
result
|
|
}
|
|
|
|
/// Give an input to the running command. With no command running, `Enter` repeats the last
|
|
/// command (as in AutoCAD) and points do nothing. A LISP routine waiting in a `get…`
|
|
/// function takes the input instead; one paused in `command` carries on after it.
|
|
pub fn input(&mut self, input: Input) -> Result<()> {
|
|
if self.lisp_input(input.clone()) {
|
|
return Ok(());
|
|
}
|
|
let r = self.input_to_command(input);
|
|
self.lisp_after_command_input();
|
|
r
|
|
}
|
|
|
|
fn input_to_command(&mut self, input: Input) -> Result<()> {
|
|
if self.running.is_none() {
|
|
if input == Input::Enter
|
|
&& let Some(last) = self.last_command.clone()
|
|
{
|
|
return self.start(&last);
|
|
}
|
|
return Ok(());
|
|
}
|
|
if !self.point_mods.is_empty() {
|
|
return self.modifier_input(input);
|
|
}
|
|
// A deferred snap is only meaningful where the prompt resolves it; elsewhere it is the
|
|
// point it was picked at.
|
|
let input = match input {
|
|
Input::Deferred(d) if !self.current_prompt().is_some_and(|p| p.deferred) => Input::Point(d.at),
|
|
other => other,
|
|
};
|
|
// Generic selection handling during "Select objects:" prompts.
|
|
let input = self.preprocess_selection(input)?;
|
|
match input {
|
|
Some(i) => {
|
|
// SIngle: the first selection that finds objects also ends the prompt.
|
|
let single = self.select_single && matches!(&i, Input::Pick(hs) if !hs.is_empty());
|
|
self.feed(Some(i))?;
|
|
if single {
|
|
self.select_single = false;
|
|
if self.current_prompt().is_some_and(|p| p.accept.select) {
|
|
self.feed(Some(Input::Enter))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
None => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn preprocess_selection(&mut self, input: Input) -> Result<Option<Input>> {
|
|
// The command's own prompt: a typed selection mode only applies at its "Select objects".
|
|
let Some(prompt) = self.running.as_ref().map(|r| r.machine.prompt(self)) else { return Ok(Some(input)) };
|
|
if !prompt.accept.select {
|
|
return Ok(Some(input));
|
|
}
|
|
if self.select_mode.is_some() {
|
|
return self.select_mode_input(input);
|
|
}
|
|
// `_L`, `_ALL`, `_W`: the English name of a selection keyword, as scripts write it.
|
|
let input = match input {
|
|
Input::Text(t) => Input::Text(t.trim().strip_prefix('_').map(str::to_string).unwrap_or(t)),
|
|
other => other,
|
|
};
|
|
if let Input::Text(t) | Input::Keyword(t) = &input
|
|
&& let Some(m) = select::SelectMode::from_keyword(t)
|
|
{
|
|
self.pending_window = None;
|
|
self.select_mode = Some(m);
|
|
return Ok(None);
|
|
}
|
|
if let Input::Text(t) | Input::Keyword(t) = &input
|
|
&& !prompt.keywords.iter().any(|k| k.eq_ignore_ascii_case(t.trim()))
|
|
{
|
|
match t.trim().to_ascii_lowercase().as_str() {
|
|
"si" | "single" => {
|
|
self.select_single = true;
|
|
return Ok(None);
|
|
}
|
|
// AUto (a pick, or a window where nothing is picked) is how points always select;
|
|
// Multiple picks the same way until Enter.
|
|
"au" | "auto" | "m" | "multiple" => return Ok(None),
|
|
_ => {}
|
|
}
|
|
}
|
|
let space = self.space();
|
|
match input {
|
|
Input::Point(p) => {
|
|
let ap = self.pixel_size() * self.settings.pickbox.max(1.0) * 1.5;
|
|
if let Some(pw) = self.pending_window.take() {
|
|
let crossing = self.window_is_crossing(pw.corner, p);
|
|
let hs = select::select_window(self.doc()?, &space, Bounds2::new(pw.corner, p), crossing);
|
|
return Ok(Some(Input::Pick(cmd::groups::expand(self, hs))));
|
|
}
|
|
match select::pick(self.doc()?, &space, p, ap) {
|
|
Some(h) => Ok(Some(Input::Pick(cmd::groups::expand(self, vec![h])))),
|
|
None => {
|
|
self.pending_window = Some(PendingWindow { corner: p, during_command: true });
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
Input::Text(ref t) | Input::Keyword(ref t) => {
|
|
let tl = t.trim().to_ascii_lowercase();
|
|
let d = self.doc()?;
|
|
let store = d.space(&space);
|
|
let picked = match tl.as_str() {
|
|
"all" => store.map(|s| s.iter().filter(|e| d.is_visible(e)).map(|e| e.handle).collect()),
|
|
"l" | "last" => store.and_then(|s| s.last()).map(|e| vec![e.handle]),
|
|
"p" | "previous" => Some(self.state()?.previous_selection.clone()),
|
|
_ => None,
|
|
};
|
|
match picked {
|
|
Some(hs) => Ok(Some(Input::Pick(cmd::groups::expand(self, hs)))),
|
|
None => Ok(Some(input)),
|
|
}
|
|
}
|
|
Input::Cancel => {
|
|
self.pending_window = None;
|
|
Ok(Some(Input::Cancel))
|
|
}
|
|
other => Ok(Some(other)),
|
|
}
|
|
}
|
|
|
|
/// Input while a typed selection mode (W, C, F, WP, CP) collects its points: the selected
|
|
/// objects go to the command as a pick once the mode is complete.
|
|
fn select_mode_input(&mut self, input: Input) -> Result<Option<Input>> {
|
|
let Some(mut mode) = self.select_mode.take() else { return Ok(Some(input)) };
|
|
if mode.kind == select::SelectKind::Group {
|
|
return match input {
|
|
Input::Text(name) | Input::Keyword(name) => match select::select_group(self.doc()?, &self.space(), &name) {
|
|
Some(hs) => Ok(Some(Input::Pick(hs))),
|
|
None => {
|
|
self.echo(format!("No group named \"{}\".", name.trim()));
|
|
Ok(None)
|
|
}
|
|
},
|
|
// Enter or Esc leaves the group name prompt.
|
|
Input::Cancel => Ok(Some(input)),
|
|
Input::Enter => Ok(None),
|
|
_ => {
|
|
self.echo("Invalid group name.");
|
|
self.select_mode = Some(mode);
|
|
Ok(None)
|
|
}
|
|
};
|
|
}
|
|
match input {
|
|
Input::Point(p) => {
|
|
if mode.push(p) {
|
|
return Ok(mode.select(self.doc()?, &self.space()).map(Input::Pick));
|
|
}
|
|
}
|
|
Input::Keyword(k) if k == "Undo" => {
|
|
mode.pts.pop();
|
|
}
|
|
// Enter ends a fence or polygon; with too few points the mode is dropped.
|
|
Input::Enter => return Ok(mode.select(self.doc()?, &self.space()).map(Input::Pick)),
|
|
Input::Cancel | Input::Pick(_) => return Ok(Some(input)),
|
|
_ => self.echo("Invalid point."),
|
|
}
|
|
self.select_mode = Some(mode);
|
|
Ok(None)
|
|
}
|
|
|
|
pub(crate) fn feed(&mut self, input: Option<Input>) -> Result<()> {
|
|
let base = if matches!(input, Some(Input::Point(_))) { self.current_prompt().and_then(|p| p.base) } else { None };
|
|
let Some(mut run) = self.running.take() else { return Ok(()) };
|
|
let step = match input {
|
|
Some(Input::Cancel) => {
|
|
run.machine.cancelled(self);
|
|
Ok(Step::Cancel)
|
|
}
|
|
Some(i) => {
|
|
if let Input::Point(p) = &i {
|
|
self.last_point = *p;
|
|
if let Some(b) = base.filter(|b| b.is_finite() && p.is_finite() && b.dist(*p) > 1e-12) {
|
|
self.last_segment = Some((b, *p));
|
|
}
|
|
// A temporary tracking point (TT) serves one point.
|
|
self.tracking.release_temporary();
|
|
}
|
|
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run.machine.input(self, i)));
|
|
match r {
|
|
Ok(r) => r,
|
|
Err(_) => Err(EngineError::Internal(run.id.clone(), "panic".into())),
|
|
}
|
|
}
|
|
// A command starts with no modifiers pending.
|
|
None => {
|
|
self.point_mods.clear();
|
|
run.machine.begin(self)
|
|
}
|
|
};
|
|
// Redraw what the command has added so far (LINE adds a segment per point), not only when it ends.
|
|
if let Ok(st) = self.state_mut()
|
|
&& !Arc::ptr_eq(&run.before, &st.doc)
|
|
{
|
|
st.revision += 1;
|
|
}
|
|
match step {
|
|
Ok(Step::Continue) => {
|
|
self.running = Some(run);
|
|
Ok(())
|
|
}
|
|
Ok(Step::Done) => {
|
|
self.finish(run, false);
|
|
self.resume_suspended();
|
|
Ok(())
|
|
}
|
|
Ok(Step::Cancel) => {
|
|
self.echo("*Cancel*");
|
|
self.finish(run, true);
|
|
self.resume_suspended();
|
|
Ok(())
|
|
}
|
|
Err(EngineError::Internal(id, m)) => {
|
|
// Restore and end the command.
|
|
if let Ok(st) = self.state_mut() {
|
|
st.doc = run.before.clone();
|
|
}
|
|
self.echo(format!("Internal error in {id}: {m}"));
|
|
self.resume_suspended();
|
|
Err(EngineError::Internal(id, m))
|
|
}
|
|
Err(e) => {
|
|
// Invalid input: report and keep prompting (AutoCAD re-prompts).
|
|
self.echo(e.to_string());
|
|
self.running = Some(run);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// End a command: every way out of one comes through here. The passes after a command
|
|
/// (constraints, associativity, groups) can panic like the command itself; a panic restores
|
|
/// the drawing as it was before the command, as a panic in its `input` does.
|
|
fn finish(&mut self, run: Running, cancelled: bool) {
|
|
let (id, before, selection) = (run.id.clone(), run.before.clone(), run.selection_before.clone());
|
|
if let Err(p) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.finish_guarded(run, cancelled))) {
|
|
if let Ok(st) = self.state_mut() {
|
|
st.doc = before;
|
|
st.selection = selection;
|
|
}
|
|
self.echo(format!("Internal error in {id}: {}", crate::guard::message(&*p)));
|
|
}
|
|
}
|
|
|
|
fn finish_guarded(&mut self, run: Running, cancelled: bool) {
|
|
// A typed selection mode or SIngle never outlives its command.
|
|
self.select_mode = None;
|
|
self.select_single = false;
|
|
if find_command(&run.id).is_some_and(|c| c.undoable)
|
|
&& let Err(e) = self.refuse_non_finite(None, &run.before, &run.selection_before, &run.id)
|
|
{
|
|
self.echo(e.to_string());
|
|
}
|
|
// A command outside undo that changed the drawing (UNDO, which swapped it for an earlier
|
|
// one) records no undo step, as in `execute`.
|
|
if find_command(&run.id).is_some_and(|c| !c.undoable) && self.state().is_ok_and(|st| !Arc::ptr_eq(&run.before, &st.doc)) {
|
|
self.pending_window = None;
|
|
return;
|
|
}
|
|
self.pending_window = None;
|
|
self.select_mode = None;
|
|
let label = find_command(&run.id).map(|c| c.label).unwrap_or("Command");
|
|
let _ = cancelled;
|
|
if let Some(m) = cmd::blocks::refedit::after_command(self, &run.before, &run.id) {
|
|
self.echo(m);
|
|
}
|
|
self.after_command(Some(&run.before), None, true);
|
|
// A transparent command's changes belong to the command it ran inside, and a command a
|
|
// LISP routine ran belongs to the routine's single undo step (the routine keeps it for U
|
|
// inside it).
|
|
if self.suspended.is_some() || self.lisp_groups_undo() {
|
|
if let Ok(st) = self.state_mut()
|
|
&& !Arc::ptr_eq(&run.before, &st.doc)
|
|
{
|
|
st.revision += 1;
|
|
if self.suspended.is_none() {
|
|
self.lisp_record_step(Snapshot { label: label.to_string(), doc: run.before, selection: run.selection_before });
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if let Ok(st) = self.state_mut()
|
|
&& !Arc::ptr_eq(&run.before, &st.doc)
|
|
{
|
|
st.push_undo(Snapshot { label: label.to_string(), doc: run.before, selection: run.selection_before });
|
|
st.revision += 1;
|
|
}
|
|
}
|
|
|
|
/// The post-command passes, in order, run after every command — by [`Session::execute`] and
|
|
/// when an interactive command finishes — so their edits land in the command's own undo step:
|
|
/// group membership is pruned, constraints are inferred, purged and re-solved, and
|
|
/// associative dimensions and hatches follow their sources. A new pass is added here, once.
|
|
///
|
|
/// `before` is the drawing the command started from; `uid` (when known) is the document it
|
|
/// started in, so associativity is not applied across a document switch; `ok` is false for a
|
|
/// failed or non-undoable programmatic command, which skips the constraint pass.
|
|
pub(super) fn after_command(&mut self, before: Option<&Arc<Drawing>>, uid: Option<u64>, ok: bool) {
|
|
#[cfg(test)]
|
|
crate::guard::tests::panic_if_armed();
|
|
cmd::groups::after_command(self, before);
|
|
cmd::constraints::after_command(self, before, ok);
|
|
assoc::after_command(self, before, uid);
|
|
}
|
|
|
|
fn resume_suspended(&mut self) {
|
|
if let Some(outer) = self.suspended.take() {
|
|
self.echo(format!("Resuming {} command.", outer.id.to_ascii_uppercase()));
|
|
self.running = Some(outer);
|
|
}
|
|
}
|
|
|
|
/// Cancel the running command (Esc). With no command, clears the selection. Inside a
|
|
/// transparent command, cancels it and the command it interrupted.
|
|
pub fn cancel(&mut self) {
|
|
self.point_mods.clear();
|
|
// Esc stops a LISP routine waiting for input, and the command it paused in.
|
|
if self.lisp_waiting() {
|
|
if self.running.is_some() {
|
|
let _ = self.feed(Some(Input::Cancel));
|
|
}
|
|
self.echo("*Cancel*");
|
|
self.lisp_abort("Function cancelled");
|
|
return;
|
|
}
|
|
if self.running.is_some() {
|
|
if self.suspended.is_some() {
|
|
if let Some(mut nested) = self.running.take() {
|
|
nested.machine.cancelled(self);
|
|
self.finish(nested, true);
|
|
}
|
|
self.running = self.suspended.take();
|
|
}
|
|
let _ = self.feed(Some(Input::Cancel));
|
|
} else {
|
|
self.pending_window = None;
|
|
self.set_selection(Vec::new());
|
|
}
|
|
}
|
|
|
|
/// The multiline text the running command is collecting, for the in-place editor
|
|
/// ([`Interactive::editing_mtext`]).
|
|
pub fn editing_mtext(&self) -> Option<cadcraft_doc::MText> {
|
|
self.running.as_ref().and_then(|r| r.machine.editing_mtext())
|
|
}
|
|
|
|
pub fn current_prompt(&self) -> Option<Prompt> {
|
|
if let Some(p) = self.lisp_prompt() {
|
|
return Some(p);
|
|
}
|
|
self.running.as_ref().map(|r| {
|
|
let p = r.machine.prompt(self);
|
|
match &self.select_mode {
|
|
Some(m) if p.accept.select => m.prompt(),
|
|
_ => p,
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Whether the cursor picks objects right now, so the UI shows the pick box (PICKBOX) at the
|
|
/// crosshair: at prompts that select objects, and with no command running when PICKFIRST is on
|
|
/// (noun-verb selection).
|
|
pub fn picking_objects(&self) -> bool {
|
|
match self.current_prompt() {
|
|
Some(p) => p.picks_objects(),
|
|
None => self.settings.pickfirst,
|
|
}
|
|
}
|
|
|
|
/// Whether the object under the cursor is highlighted (SELECTIONPREVIEW): the cursor picks
|
|
/// objects ([`Self::picking_objects`]) and the bit for a command asking for them (2) or for
|
|
/// no command (1) is on.
|
|
pub fn previews_selection(&self) -> bool {
|
|
let bit = if self.current_prompt().is_some() { 2 } else { 1 };
|
|
self.picking_objects() && self.settings.selectionpreview & bit != 0
|
|
}
|
|
|
|
/// Whether selected objects show grips while `selected` objects are selected (GRIPS,
|
|
/// GRIPOBJLIMIT).
|
|
pub fn shows_grips(&self, selected: usize) -> bool {
|
|
let limit = self.settings.gripobjlimit;
|
|
self.settings.grips > 0 && (limit == 0 || selected <= limit as usize)
|
|
}
|
|
|
|
/// The text shown on the command line: the active prompt, or "Command:".
|
|
pub fn prompt_text(&self) -> String {
|
|
if let Some(p) = self.lisp_prompt() {
|
|
return p.display();
|
|
}
|
|
match &self.running {
|
|
Some(r) if self.suspended.is_some() => format!(">>{} {}", r.id.to_ascii_uppercase(), r.machine.prompt(self).display()),
|
|
Some(r) => format!("{} {}", r.id.to_ascii_uppercase(), r.machine.prompt(self).display()),
|
|
None => "Command:".into(),
|
|
}
|
|
}
|
|
|
|
/// Preview entities for the cursor: objects drawn with their own properties
|
|
/// ([`Interactive::preview_objects`]), then the rubber band with the current properties, in
|
|
/// the grip blue over such objects (so it shows over their fills, whatever their colour).
|
|
pub fn preview(&self, cursor: Vec2) -> Vec<Entity> {
|
|
let mut common = self.current_common();
|
|
let (kinds, mut out) = match (self.lisp_preview(cursor), &self.running) {
|
|
(Some(k), _) => (k, Vec::new()),
|
|
(None, Some(r)) => (r.machine.preview(self, cursor), r.machine.preview_objects(self, cursor)),
|
|
(None, None) => (Vec::new(), Vec::new()),
|
|
};
|
|
if !out.is_empty() {
|
|
common.color = cadcraft_doc::color::Color::True(cadcraft_doc::color::Rgb(0x2f, 0x6b, 0xff));
|
|
}
|
|
out.extend(kinds.into_iter().map(|k| Entity { handle: Handle(0), common: common.clone(), kind: k }));
|
|
out
|
|
}
|
|
|
|
/// What the drawing area shows in place of the drawing while a command runs (DVIEW's preview
|
|
/// set, see [`Interactive::display_only`]), also while a transparent command interrupts it.
|
|
/// `None`: the drawing.
|
|
pub fn display_only(&self) -> Option<DisplayOnly> {
|
|
[&self.running, &self.suspended].into_iter().flatten().find_map(|r| r.machine.display_only(self))
|
|
}
|
|
|
|
/// The active command's preview as a transform of existing objects (see
|
|
/// [`Interactive::drag`]), with its extra rubber-band geometry as entities.
|
|
pub fn drag(&self, cursor: Vec2) -> Option<(Drag, Vec<Entity>)> {
|
|
let r = self.running.as_ref()?;
|
|
let d = r.machine.drag(self, cursor)?;
|
|
let common = self.current_common();
|
|
let extra = d.extra.iter().map(|k| Entity { handle: Handle(0), common: common.clone(), kind: k.clone() }).collect();
|
|
Some((d, extra))
|
|
}
|
|
}
|