mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
319 lines
16 KiB
Rust
319 lines
16 KiB
Rust
use super::{
|
|
Arc, Document, EngineError, Interaction, LastTransform, NamedStep, Result, Selection, Session, TOO_DEEP, Value, cmd, deselect_hidden, doc_sane,
|
|
objects_in_range,
|
|
};
|
|
|
|
impl Session {
|
|
/// Run `f` on a mutable copy of the active document. Outside an interaction this records one
|
|
/// undo step labelled `label`; inside one it just mutates (the interaction commits later).
|
|
pub fn edit<T>(&mut self, label: &str, f: impl FnOnce(&mut Document, &mut Selection) -> Result<T>) -> Result<T> {
|
|
// An edit made while typing (the Transform panel) is a step of its own: inside the typing
|
|
// session, the next keystroke's preview would start again from before it (#1881).
|
|
if !self.previewing && cmd::typing_in_progress(self) {
|
|
cmd::settle_interaction(self)?;
|
|
}
|
|
let menu = self.menu.clone();
|
|
let st = self.doc_mut()?;
|
|
let before = st.doc.clone();
|
|
let before_sel = st.selection.clone();
|
|
let doc = Arc::make_mut(&mut st.doc);
|
|
// The Global Edit members the edit reached besides the selection.
|
|
let mut spread = vec![];
|
|
// Objects attached to a path follow it, and dimensions follow the art they measure.
|
|
let result = match f(doc, &mut st.selection).and_then(|v| {
|
|
// Global Edit: the other members take what the edit changed about the one it edited.
|
|
if let Some(ge) = &st.global_edit {
|
|
spread = cmd::globaledit::propagate(&before, doc, ge, [&before_sel.objects, &st.selection.objects]);
|
|
}
|
|
cmd::objects_on_path::refresh(&before, doc);
|
|
cmd::dimension::refresh(&before, doc)?;
|
|
Ok(v)
|
|
}) {
|
|
Ok(v) => match cmd::shaper::refresh(&before, Arc::make_mut(&mut st.doc)) {
|
|
Err(e) => Err(e),
|
|
Ok(()) => {
|
|
// Attached Shaper groups whose art was rebuilt go back on their path.
|
|
cmd::objects_on_path::refresh(&before, Arc::make_mut(&mut st.doc));
|
|
// Inline graphics in text follow their symbols.
|
|
cmd::inline::refresh(&before, Arc::make_mut(&mut st.doc));
|
|
// Text Wrap: area type follows its wrap objects; then threads re-flow.
|
|
cmd::textwrap::refresh(Arc::make_mut(&mut st.doc));
|
|
// Type in no thread numbers its list paragraphs from 1 (before the mask copies
|
|
// its editing layer's art).
|
|
cmd::threads::restart_unthreaded_counts(&mut st.doc);
|
|
// Opacity-mask editing: the mask follows its art on the editing layer.
|
|
if st.doc.mask_edit.is_some() {
|
|
cmd::maskedit::sync(Arc::make_mut(&mut st.doc));
|
|
}
|
|
// Threaded text re-flows when any of its frames changed.
|
|
if !st.doc.text_threads.is_empty() {
|
|
cmd::threads::reflow(&before, Arc::make_mut(&mut st.doc));
|
|
}
|
|
// Asset Export: assets let go of deleted art.
|
|
if !st.doc.assets.is_empty() {
|
|
Arc::make_mut(&mut st.doc).prune_assets();
|
|
}
|
|
// Variables: bindings let go of deleted art.
|
|
if !st.doc.variables.bindings.is_empty() {
|
|
Arc::make_mut(&mut st.doc).prune_variable_bindings();
|
|
}
|
|
if st.doc.too_deep(Some(&before)) {
|
|
// Deeper trees overflow the stack of what walks them.
|
|
Err(EngineError::Other(TOO_DEEP.into()))
|
|
} else if doc_sane(&st.doc, &st.selection) && objects_in_range(&st.doc, &spread) {
|
|
Ok(v)
|
|
} else {
|
|
Err(EngineError::Other("result would exceed the canvas (coordinates out of range)".into()))
|
|
}
|
|
}
|
|
},
|
|
Err(e) => Err(e),
|
|
};
|
|
match result {
|
|
Ok(v) => {
|
|
st.selection.prune(&st.doc);
|
|
// An edit that selects what it unlocks, shows or pastes leaves locked guides out,
|
|
// and one that selects the slices it makes leaves them out while they are locked
|
|
// or hidden.
|
|
deselect_hidden(&menu, &st.doc, &mut st.selection);
|
|
// The art on a locked artboard can't stay selected, however it got there, except
|
|
// the new art this edit made and new art still selected.
|
|
st.protect_locked_artboards(&before_sel.objects, &before_sel.new_art, Some(&*before));
|
|
st.revision += 1;
|
|
if st.interaction.is_none() {
|
|
let selection = match st.history.undo.last() {
|
|
Some(l) if *l.selection == before_sel => l.selection.clone(),
|
|
_ => Arc::new(before_sel),
|
|
};
|
|
st.push_undo(label.to_string(), before, selection);
|
|
}
|
|
Ok(v)
|
|
}
|
|
Err(e) => {
|
|
st.doc = before;
|
|
st.selection = before_sel;
|
|
cmd::globaledit::follow_history(st);
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// [`Session::edit`] with the step named `template`, its `{name}` placeholder filled with `name`
|
|
/// (a style's name). The undo step it records keeps the template and the name
|
|
/// ([`HistoryEntry::named`]), and a frontend shows the template's translation with the name in
|
|
/// it. An edit that records no step of its own (inside an interaction, or a later edit of an
|
|
/// undo group) keeps neither, and neither does a step an undo group names.
|
|
pub fn edit_named<T>(
|
|
&mut self,
|
|
template: &'static str,
|
|
name: &str,
|
|
built_in: bool,
|
|
f: impl FnOnce(&mut Document, &mut Selection) -> Result<T>,
|
|
) -> Result<T> {
|
|
let named = NamedStep { template, name: name.to_string(), built_in };
|
|
let step = named.label();
|
|
// The step this edit records keeps the document as it is now.
|
|
let now = Arc::as_ptr(&self.doc()?.doc);
|
|
let v = self.edit(&step, f)?;
|
|
if let Ok(st) = self.doc_mut()
|
|
&& let Some(e) = st.history.undo.last_mut()
|
|
&& Arc::as_ptr(&e.doc) == now
|
|
&& e.label == step
|
|
{
|
|
e.named = Some(named);
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
/// Change only the selection (not an undo step).
|
|
pub fn select(&mut self, f: impl FnOnce(&Document, &mut Selection)) -> Result<()> {
|
|
self.active_appearance_item = None;
|
|
let menu = self.menu.clone();
|
|
let st = self.doc_mut()?;
|
|
let before = st.selection.objects.clone();
|
|
let new_art = st.selection.new_art.clone();
|
|
f(&st.doc, &mut st.selection);
|
|
st.selection.prune(&st.doc);
|
|
// Lock Guides locks the paths made into guides too (#1534), and Lock Slices and Hide
|
|
// Slices keep the slices out, whatever selects them.
|
|
deselect_hidden(&menu, &st.doc, &mut st.selection);
|
|
// The art on a locked artboard can't be selected, whatever selects it, except new art
|
|
// that was selected and still is.
|
|
st.protect_locked_artboards(&before, &new_art, None);
|
|
// Selecting art makes its layer (or sublayer) the current one, as in the Layers panel of
|
|
// the reference app: new art then goes beside it.
|
|
if st.selection.objects != before
|
|
&& let Some(layer) = st.selection.objects.last().and_then(|id| st.doc.layer_containing(*id)).filter(|l| st.doc.is_editable(*l))
|
|
&& st.active_layer != Some(layer)
|
|
{
|
|
st.active_layer = Some(layer);
|
|
st.layer_rows.clear();
|
|
}
|
|
st.revision += 1;
|
|
// Puppet Warp pins belong to the art they were placed on: another selection starts afresh.
|
|
if st.doc.puppet.as_ref().is_some_and(|p| p.ids != st.selection.objects) {
|
|
cmd::distortcmds::drop_puppet_pins(st);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// ---------- interactions (live drags) ----------
|
|
|
|
pub(crate) fn clear_deferred_overlay_cache(&mut self) {
|
|
self.deferred_overlay_cache = None;
|
|
}
|
|
|
|
/// The interaction's latest preview command when the document is still its snapshot (deferred
|
|
/// Real-time Drawing), regardless of the preference at commit or overlay time.
|
|
pub(crate) fn unapplied_interaction_preview(&self) -> Option<(String, Value)> {
|
|
self.active().and_then(|d| d.interaction.as_ref().filter(|it| std::sync::Arc::ptr_eq(&it.doc, &d.doc)).and_then(|it| it.preview.clone()))
|
|
}
|
|
|
|
pub fn begin_interaction(&mut self, label: &str) -> Result<()> {
|
|
// Typing in progress is kept first: a drag or a dialog's preview would otherwise start
|
|
// from the snapshot taken before the typing, and drop it (#1881).
|
|
if label != "Typing" && cmd::typing_in_progress(self) {
|
|
cmd::settle_interaction(self)?;
|
|
}
|
|
self.clear_deferred_overlay_cache();
|
|
let st = self.doc_mut()?;
|
|
if st.interaction.is_some() {
|
|
return Ok(());
|
|
}
|
|
st.interaction = Some(Interaction {
|
|
label: label.to_string(),
|
|
doc: st.doc.clone(),
|
|
selection: Arc::new(st.selection.clone()),
|
|
preview: None,
|
|
active_layer: st.active_layer,
|
|
layer_rows: st.layer_rows.clone(),
|
|
variables_highlight: st.variables_highlight.clone(),
|
|
isolation: st.isolation,
|
|
perspective_again: None,
|
|
copies: None,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
/// Re-apply `cmd` on top of the interaction snapshot (replacing the previous preview).
|
|
pub fn preview(&mut self, cmd: &str, params: &Value) -> Result<Value> {
|
|
{
|
|
let menu = self.menu.clone();
|
|
let st = self.doc_mut()?;
|
|
let Some(it) = &st.interaction else { return Err(EngineError::Other("no interaction in progress".into())) };
|
|
// Undoing the previous preview counts as a change, so views redraw even when the command
|
|
// below returns without an edit.
|
|
let selection_changed = st.selection != *it.selection;
|
|
if !Arc::ptr_eq(&st.doc, &it.doc) || selection_changed {
|
|
st.revision += 1;
|
|
}
|
|
st.doc = it.doc.clone();
|
|
if selection_changed {
|
|
st.selection.clone_from(&it.selection);
|
|
}
|
|
// Guides locked and slices locked or hidden during the interaction stay deselected,
|
|
// even when the preview fails.
|
|
deselect_hidden(&menu, &st.doc, &mut st.selection);
|
|
cmd::globaledit::follow_history(st);
|
|
}
|
|
// The previous preview's paint is undone with it.
|
|
self.pending_paint = None;
|
|
let rw = cmd::distortcmds::perspective_rewrite(self, cmd, params).or_else(|| cmd::bounds_rewrite(self, cmd, params));
|
|
let (cmd, params) = rw.as_ref().map_or((cmd, params), |(c, p)| (c.as_str(), p));
|
|
let outer = std::mem::replace(&mut self.previewing, true);
|
|
let r = self.execute(cmd, params);
|
|
self.previewing = outer;
|
|
let params = self.noted(params);
|
|
let st = self.doc_mut()?;
|
|
if let Some(it) = &mut st.interaction {
|
|
// A failed preview leaves the document at the snapshot, so nothing is left to commit
|
|
// (not even an earlier successful preview, which the reset above already undid).
|
|
it.preview = r.is_ok().then(|| (cmd.to_string(), params));
|
|
}
|
|
r
|
|
}
|
|
|
|
pub fn commit_interaction(&mut self) -> Result<()> {
|
|
self.clear_deferred_overlay_cache();
|
|
self.remember_pending_paint();
|
|
let st = self.doc_mut()?;
|
|
let Some(mut it) = st.interaction.take() else { return Ok(()) };
|
|
let Some(preview) = it.preview.take() else { return Ok(()) };
|
|
let perspective_again = it.perspective_again.take();
|
|
let copy = preview.0 == "object.transform" && preview.1.get("copy").and_then(Value::as_bool).unwrap_or(false);
|
|
// Alt pressed or released during a move drag turns it into a copy or back: name it for
|
|
// what it did in the end.
|
|
match (it.label.as_str(), copy) {
|
|
("Move", true) => it.label = "Copy".into(),
|
|
("Copy", false) if preview.0 == "object.transform" => it.label = "Move".into(),
|
|
_ => {}
|
|
}
|
|
// A preview that left the document as it was records no Transform Again either (as
|
|
// `keep_interaction` records no undo step): the last real transformation stands.
|
|
let changed = !Arc::ptr_eq(&it.doc, &st.doc);
|
|
// How far a drag of direct-selected anchors moved them (`path.moveAnchors`).
|
|
let moved = vectorcraft_geom::Vec2::new(
|
|
preview.1.get("dx").and_then(Value::as_f64).unwrap_or(0.0),
|
|
preview.1.get("dy").and_then(Value::as_f64).unwrap_or(0.0),
|
|
);
|
|
st.keep_interaction(it);
|
|
if preview.0 == "object.transform" {
|
|
let m = cmd::matrix_param(&preview.1, "matrix");
|
|
if let Some(m) = m {
|
|
// The options are the last preview's params with the values it noted, without the
|
|
// matrix and the ids.
|
|
let mut options = preview.1.clone();
|
|
if let Some(o) = options.as_object_mut() {
|
|
o.remove("matrix");
|
|
o.remove("ids");
|
|
}
|
|
st.last_transform = Some(LastTransform::Matrix(m, options));
|
|
st.last_perspective = None;
|
|
}
|
|
} else if preview.0 == "path.moveAnchors" && (moved.x != 0.0 || moved.y != 0.0) {
|
|
// A drag of direct-selected anchors (from an anchor, or from a fill with anchors
|
|
// selected): Transform Again moves the selected anchors and the objects selected whole
|
|
// by the same distance. A drag that ends where it began leaves the last transform.
|
|
let mut options = serde_json::json!({ "copy": false });
|
|
if let Some(patterns) = preview.1.get("patterns") {
|
|
options["patterns"] = patterns.clone();
|
|
}
|
|
st.last_transform = Some(LastTransform::Matrix(vectorcraft_geom::Affine::translate(moved), options));
|
|
st.last_perspective = None;
|
|
} else if preview.0 == "object.transformEach" && changed {
|
|
// Transform Again repeats it as it stood when OK was pressed.
|
|
st.last_transform = Some(LastTransform::Each(preview.1.clone()));
|
|
st.last_perspective = None;
|
|
}
|
|
if perspective_again.is_some() {
|
|
st.last_perspective = perspective_again;
|
|
}
|
|
let (doc, journal, grouped) = (self.active().map(|d| d.uid), self.journal.len(), self.undo_group_docs());
|
|
self.journal.push(preview);
|
|
self.journal_in_undo_groups(doc, &grouped, journal);
|
|
Ok(())
|
|
}
|
|
|
|
/// Remember the paint a live preview applied (as its interaction is kept).
|
|
pub(crate) fn remember_pending_paint(&mut self) {
|
|
if let Some(p) = self.pending_paint.take() {
|
|
self.remember_paint_now(&p);
|
|
}
|
|
}
|
|
|
|
pub fn cancel_interaction(&mut self) -> Result<()> {
|
|
self.clear_deferred_overlay_cache();
|
|
self.pending_paint = None;
|
|
let menu = self.menu.clone();
|
|
let st = self.doc_mut()?;
|
|
st.undo_interaction();
|
|
// Guides locked and slices locked or hidden during the interaction stay deselected.
|
|
deselect_hidden(&menu, &st.doc, &mut st.selection);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn in_interaction(&self) -> bool {
|
|
self.active().is_some_and(|d| d.interaction.is_some())
|
|
}
|
|
}
|