mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3752 from Taha-Mahmoodi/fix/3705-paste-draw-inside
Fix #3705: pastes follow Draw Inside and Draw Behind, and Shift+D no longer sticks in Draw Behind
This commit is contained in:
@@ -73,7 +73,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Drawing Mode",
|
||||
[],
|
||||
Some("Shift+D"),
|
||||
"{mode?: normal|behind|inside} (no param cycles; inside needs one selected path)",
|
||||
"{mode?: normal|behind|inside} (inside needs one selected path, compound path or type; no param cycles normal → behind → inside → normal, going from behind back to normal when Draw Inside can't draw into the selection)",
|
||||
has_doc,
|
||||
draw_mode
|
||||
),
|
||||
@@ -157,23 +157,7 @@ fn insert_look_turned(
|
||||
n.bbox_angle = bbox_angle;
|
||||
match (mode, inside) {
|
||||
(crate::DrawMode::Inside, Some(target)) if d.node(target).is_some() => {
|
||||
// Wrap the target once in a clip group [mask copy, target, …new art].
|
||||
let group = match d.parent_of(target).and_then(|p| d.node(p).map(|pn| (p, pn.kind.clone()))) {
|
||||
Some((p, NodeKind::Group { clip: true, children })) if children.get(1).is_some_and(|c| c.id == target) => p,
|
||||
_ => {
|
||||
let (par, idx, _) = d.position(target).ok_or(crate::EngineError::NoNode(target))?;
|
||||
let mut mask = d.node(target).cloned().ok_or(crate::EngineError::NoNode(target))?;
|
||||
mask.id = d.alloc_id();
|
||||
mask.appearance = Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0).into();
|
||||
if let NodeKind::Path { clipping, .. } = &mut mask.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
let gid = d.alloc_id();
|
||||
d.insert(par, idx + 1, Node::new(gid, NodeKind::Group { children: vec![std::sync::Arc::new(mask)], clip: true }))?;
|
||||
d.move_node(target, Some(gid), usize::MAX)?;
|
||||
gid
|
||||
}
|
||||
};
|
||||
let group = draw_inside_group(d, target)?;
|
||||
d.insert(Some(group), usize::MAX, n)?;
|
||||
}
|
||||
(crate::DrawMode::Behind, _) => match behind_of.and_then(|b| d.position(b)) {
|
||||
@@ -196,27 +180,57 @@ fn insert_look_turned(
|
||||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
/// Draw Inside's clip group for `target`, the object new art goes inside: the clip group
|
||||
/// [mask copy, target, …] `target` already is the clipped object of, else one made where `target`
|
||||
/// was, holding an unpainted clipping copy of it and then `target`. New art and pastes go on top
|
||||
/// of its children.
|
||||
pub(crate) fn draw_inside_group(d: &mut vectorcraft_doc::Document, target: NodeId) -> Result<NodeId> {
|
||||
if let Some(p) = d.parent_of(target)
|
||||
&& d.node(p).is_some_and(|pn| matches!(&pn.kind, NodeKind::Group { clip: true, children } if children.get(1).is_some_and(|c| c.id == target)))
|
||||
{
|
||||
return Ok(p);
|
||||
}
|
||||
let (par, idx, _) = d.position(target).ok_or(crate::EngineError::NoNode(target))?;
|
||||
// The mask is a copy with ids of its own, its subpaths' too (a compound path's).
|
||||
let original = d.node(target).cloned().ok_or(crate::EngineError::NoNode(target))?;
|
||||
let mut mask = d.reid(&original);
|
||||
mask.appearance = Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0).into();
|
||||
if let NodeKind::Path { clipping, .. } = &mut mask.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
let gid = d.alloc_id();
|
||||
d.insert(par, idx + 1, Node::new(gid, NodeKind::Group { children: vec![std::sync::Arc::new(mask)], clip: true }))?;
|
||||
d.move_node(target, Some(gid), usize::MAX)?;
|
||||
Ok(gid)
|
||||
}
|
||||
|
||||
/// The object Draw Inside would draw into: the one selected object, when it is a path, a compound
|
||||
/// path or type.
|
||||
fn inside_target(st: &crate::DocState) -> Option<NodeId> {
|
||||
let first = st.selection.objects.first().copied()?;
|
||||
let fits = matches!(st.doc.node(first).map(|n| &n.kind), Some(NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_)));
|
||||
(fits && st.selection.len() == 1).then_some(first)
|
||||
}
|
||||
|
||||
fn draw_mode(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mode = match str_param(p, "mode") {
|
||||
Some("behind") => crate::DrawMode::Behind,
|
||||
Some("inside") => crate::DrawMode::Inside,
|
||||
Some("normal") => crate::DrawMode::Normal,
|
||||
// Cycling (Shift+D): Draw Behind goes on to Draw Inside only when it can draw inside the
|
||||
// selection, else back to Draw Normal (#3705: with one group selected it stayed in Draw
|
||||
// Behind).
|
||||
None => match s.draw_mode {
|
||||
crate::DrawMode::Normal => crate::DrawMode::Behind,
|
||||
crate::DrawMode::Behind if s.doc()?.selection.len() == 1 => crate::DrawMode::Inside,
|
||||
crate::DrawMode::Behind if inside_target(s.doc()?).is_some() => crate::DrawMode::Inside,
|
||||
_ => crate::DrawMode::Normal,
|
||||
},
|
||||
Some(o) => return Err(bad("view.drawMode", format!("unknown mode `{o}`"))),
|
||||
};
|
||||
if mode == crate::DrawMode::Inside {
|
||||
let st = s.doc()?;
|
||||
let target =
|
||||
st.selection.objects.first().copied().filter(|id| {
|
||||
matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_)))
|
||||
});
|
||||
match target {
|
||||
Some(t) if st.selection.len() == 1 => s.draw_inside = Some(t),
|
||||
_ => return Err(bad("view.drawMode", "Draw Inside needs exactly one selected path, compound path or text")),
|
||||
match inside_target(s.doc()?) {
|
||||
Some(t) => s.draw_inside = Some(t),
|
||||
None => return Err(bad("view.drawMode", "Draw Inside needs exactly one selected path, compound path or text")),
|
||||
}
|
||||
} else {
|
||||
s.draw_inside = None;
|
||||
|
||||
@@ -119,7 +119,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Paste",
|
||||
["Edit"],
|
||||
Some("Cmd+V"),
|
||||
"{center?: [x, y], dx?, dy?, swatchConflict?} paste centred on `center` (the app passes the view centre), else offset by dx/dy (default: the Paste Offset preference). Pasting brings the image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used), gradient swatches, graphic styles, character and paragraph styles and brushes the objects use; one of the same name that differs comes in renamed. swatchConflict, for a swatch whose name the document gives another colour (clipboard.conflicts): \"merge\" (default: the objects take the document's swatch) | \"add\" (the pasted swatch comes in renamed) | {name: \"merge\"|\"add\"}. With Paste Remembers Layers on (layer.pasteRemembersLayers), objects go back into the layers they came from (by name; made when missing). Artboards copied with artboard.copy come with their art, together right of the last artboard as they lay (the other paste commands but Paste on All Artboards: where they were), their art back in the layers it came from, and the Artboard tool, when chosen, selects them → {ids, artboard: the first new artboard's index or null, artboards: the new artboards' indices, added: resources added, merged: conflicts merged, renamed: [{kind, from, to}]}",
|
||||
"{center?: [x, y], dx?, dy?, swatchConflict?} paste centred on `center` (the app passes the view centre), else offset by dx/dy (default: the Paste Offset preference). Pasting brings the image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used), gradient swatches, graphic styles, character and paragraph styles and brushes the objects use; one of the same name that differs comes in renamed. swatchConflict, for a swatch whose name the document gives another colour (clipboard.conflicts): \"merge\" (default: the objects take the document's swatch) | \"add\" (the pasted swatch comes in renamed) | {name: \"merge\"|\"add\"}. With Paste Remembers Layers on (layer.pasteRemembersLayers), objects go back into the layers they came from (by name; made when missing). Artboards copied with artboard.copy come with their art, together right of the last artboard as they lay (the other paste commands but Paste on All Artboards: where they were), their art back in the layers it came from, and the Artboard tool, when chosen, selects them. In Draw Inside (view.drawMode) the objects go inside the object drawn into, as new art does, and Paste Remembers Layers doesn't apply; edit.pasteInPlace and edit.pasteOnAllArtboards do the same, and in Draw Behind they go behind the selection as edit.pasteInBack puts them; edit.pasteInFront and edit.pasteInBack ignore the drawing mode → {ids, artboard: the first new artboard's index or null, artboards: the new artboards' indices, added: resources added, merged: conflicts merged, renamed: [{kind, from, to}]}",
|
||||
has_clipboard,
|
||||
|s, p| paste(s, p, PasteMode::Offset)
|
||||
),
|
||||
@@ -146,7 +146,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Paste in Place",
|
||||
["Edit"],
|
||||
Some("Cmd+Shift+V"),
|
||||
"{swatchConflict?, artboard?: index (as edit.pasteInFront)} paste where the objects were copied (resources and layers as edit.paste) → {ids, added, merged, renamed}",
|
||||
"{swatchConflict?, artboard?: index (as edit.pasteInFront)} paste where the objects were copied (resources, layers and drawing modes as edit.paste) → {ids, added, merged, renamed}",
|
||||
has_clipboard,
|
||||
|s, p| paste(s, p, PasteMode::InPlace)
|
||||
),
|
||||
@@ -155,7 +155,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Paste on All Artboards",
|
||||
["Edit"],
|
||||
Some("Cmd+Alt+Shift+V"),
|
||||
"{swatchConflict?} paste a copy on every artboard at the offset the objects had to the artboard they were copied from (resources and layers as edit.paste) → {ids, added, merged, renamed}",
|
||||
"{swatchConflict?} paste a copy on every artboard at the offset the objects had to the artboard they were copied from (resources, layers and drawing modes as edit.paste) → {ids, added, merged, renamed}",
|
||||
has_clipboard,
|
||||
|s, p| paste(s, p, PasteMode::AllArtboards)
|
||||
),
|
||||
@@ -686,21 +686,30 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
.reduce(|a, b| a.union(b))
|
||||
.filter(|_| mode != PasteMode::AllArtboards)
|
||||
.map(|all| (&clip.artboards, if mode == PasteMode::Offset { super::panelcmds::beside_artboards(&st.doc, all) } else { Vec2::ZERO }));
|
||||
// Paste Remembers Layers (not while isolating a group: pastes stay in it); an artboard's art
|
||||
// always goes back into its layers.
|
||||
// Drawing modes (#3705): Draw Inside puts what Paste, Paste in Place and Paste on All
|
||||
// Artboards bring inside the object it draws into; Draw Behind puts what Paste in Place and
|
||||
// Paste on All Artboards bring behind the selection, as Paste in Back does. Paste in Front and
|
||||
// Paste in Back keep their own places, and copied artboards' art goes back into its layers.
|
||||
let modal = boards.is_none() && matches!(mode, PasteMode::Offset | PasteMode::InPlace | PasteMode::AllArtboards);
|
||||
// (Only with objects to paste: an artboard copied without art makes no clip group.)
|
||||
let inside = s.draw_inside.filter(|t| modal && !clip.nodes.is_empty() && s.draw_mode == crate::DrawMode::Inside && st.doc.node(*t).is_some());
|
||||
let behind = modal && mode != PasteMode::Offset && s.draw_mode == crate::DrawMode::Behind;
|
||||
let (front, back) = (mode == PasteMode::Front, mode == PasteMode::Back || behind);
|
||||
// Paste Remembers Layers (not while isolating a group: pastes stay in it, nor in Draw
|
||||
// Inside); an artboard's art always goes back into its layers.
|
||||
// A drop goes into the current editable insertion context, not a same-named source layer.
|
||||
let remember = mode != PasteMode::Drag
|
||||
&& inside.is_none()
|
||||
&& (st.doc.paste_remembers_layers || boards.is_some())
|
||||
&& parent.is_some_and(|p| st.doc.node(p).is_some_and(Node::is_layer));
|
||||
// Front/back: relative to the selection (top-most / bottom-most selected object); with
|
||||
// nothing selected, the top / bottom of the current layer.
|
||||
let anchor = match mode {
|
||||
PasteMode::Front | PasteMode::Back => {
|
||||
let order = st.selection.in_paint_order(&st.doc);
|
||||
let a = if mode == PasteMode::Front { order.last() } else { order.first() };
|
||||
a.and_then(|id| st.doc.position(*id))
|
||||
}
|
||||
_ => None,
|
||||
let anchor = if front || back {
|
||||
let order = st.selection.in_paint_order(&st.doc);
|
||||
let a = if front { order.last() } else { order.first() };
|
||||
a.and_then(|id| st.doc.position(*id))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let placements: Vec<Affine> = match (mode, boards) {
|
||||
(_, Some((_, dv))) => vec![Affine::translate(dv)],
|
||||
@@ -717,6 +726,8 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
};
|
||||
let (ids, imported, artboards) = s.edit(mode.label(), |d, sel| {
|
||||
let imported = clip.import_into(d, choices, same_doc);
|
||||
// Draw Inside: the clip group of the object drawn into, made on the first paste.
|
||||
let into = inside.map(|t| super::create::draw_inside_group(d, t)).transpose()?;
|
||||
let artboards: Vec<usize> = boards
|
||||
.into_iter()
|
||||
.flat_map(|(all, dv)| all.iter().map(move |a| (a, dv)))
|
||||
@@ -739,15 +750,17 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
Some(Some(name)) if remember => Some(*layers.entry(name).or_insert_with(|| layer_named(d, name))),
|
||||
_ => None,
|
||||
};
|
||||
let (par, idx) = match (anchor, layer) {
|
||||
let (par, idx) = match (into, anchor, layer) {
|
||||
// Inside the object Draw Inside draws into, on top of what is in it already.
|
||||
(Some(group), ..) => (Some(group), usize::MAX),
|
||||
// Next to the anchor, unless the object goes back to another layer.
|
||||
(Some((par, i, _)), l) if l.is_none() || l == par => {
|
||||
(None, Some((par, i, _)), l) if l.is_none() || l == par => {
|
||||
let k = placed.get(&par).copied().unwrap_or(0);
|
||||
(par, if mode == PasteMode::Front { i + 1 + k } else { i + k })
|
||||
(par, if front { i + 1 + k } else { i + k })
|
||||
}
|
||||
(_, l) => {
|
||||
(None, _, l) => {
|
||||
let par = l.or(parent);
|
||||
(par, if mode == PasteMode::Back { placed.get(&par).copied().unwrap_or(0) } else { usize::MAX })
|
||||
(par, if back { placed.get(&par).copied().unwrap_or(0) } else { usize::MAX })
|
||||
}
|
||||
};
|
||||
*placed.entry(par).or_default() += 1;
|
||||
|
||||
@@ -148,6 +148,154 @@ fn double_clicking_outside_the_draw_inside_object_returns_to_draw_normal() {
|
||||
assert_eq!(double(&mut s, 150.0, 150.0), (DrawMode::Normal, None));
|
||||
}
|
||||
|
||||
/// #3705: in Draw Inside, Paste, Paste in Place and Paste on All Artboards put the copies inside
|
||||
/// the object drawn into, as drawn art goes, each paste one undo step; the first one makes the
|
||||
/// clip group and the next ones add to it, with Paste Remembers Layers on and with the object
|
||||
/// isolated by a double-click too. Paste in Front and Paste in Back keep their places.
|
||||
/// Draw Inside a compound path: the clip group's mask is a copy whose subpaths have ids of their
|
||||
/// own, so no two objects share an id. An artboard copied without art, pasted on all artboards in
|
||||
/// Draw Inside, makes no clip group.
|
||||
#[test]
|
||||
fn draw_inside_a_compound_path_gives_its_mask_ids_of_its_own() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300, "artboards": 2})).unwrap();
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("shape.rectangle", &json!({"x": 130, "y": 130, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a, b]})).unwrap();
|
||||
let c = s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [c]})).unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "inside"})).unwrap();
|
||||
s.execute("shape.ellipse", &json!({"x": 120, "y": 120, "width": 30, "height": 30})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let mut ids = Vec::new();
|
||||
d.walk(|n| ids.push(n.id.0));
|
||||
let all = ids.len();
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), all, "an id is used twice");
|
||||
assert_eq!(d.node(d.parent_of(NodeId(c)).unwrap()).unwrap().kind_label(), "Clip Group");
|
||||
// An artboard alone on the clipboard: Paste on All Artboards adds nothing inside the object.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300, "artboards": 2})).unwrap();
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("artboard.copy", &json!({"index": 1, "art": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "inside"})).unwrap();
|
||||
let _ = s.execute("edit.pasteOnAllArtboards", &json!({}));
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(d.parent_of(NodeId(a)).and_then(|p| d.node(p)).is_some_and(|p| p.is_layer()), "no clip group around the object");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pasting_in_draw_inside_puts_the_copies_inside_the_object() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300, "artboards": 2})).unwrap();
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("shape.ellipse", &json!({"x": 20, "y": 20, "width": 50, "height": 50})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "inside"})).unwrap();
|
||||
let parent = |s: &Session, id: u64| s.doc().unwrap().doc.parent_of(NodeId(id));
|
||||
let pasted = |r: Value| r["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect::<Vec<_>>();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let ids = pasted(s.execute("edit.paste", &json!({"center": [150, 150]})).unwrap());
|
||||
let g = parent(&s, a).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(g).unwrap().kind_label(), "Clip Group");
|
||||
assert_eq!(parent(&s, ids[0]), Some(g), "the copy is inside the object");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(*s.doc().unwrap().doc, *before, "one undo step takes the copy and the clip group away");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
// Paste in Place and Paste on All Artboards (one copy per artboard) go into the same group,
|
||||
// on top of what is in it.
|
||||
let in_place = pasted(s.execute("edit.pasteInPlace", &json!({})).unwrap());
|
||||
let all = pasted(s.execute("edit.pasteOnAllArtboards", &json!({})).unwrap());
|
||||
assert_eq!(all.len(), 2);
|
||||
let children: Vec<u64> = s.doc().unwrap().doc.node(g).unwrap().children().unwrap().iter().map(|n| n.id.0).collect();
|
||||
assert_eq!(children.len(), 6, "{children:?}");
|
||||
assert_eq!(children[1..], [a, ids[0], in_place[0], all[0], all[1]]);
|
||||
// Paste Remembers Layers doesn't take the copy out of the object.
|
||||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||||
let remembered = pasted(s.execute("edit.paste", &json!({})).unwrap());
|
||||
assert_eq!(parent(&s, remembered[0]), Some(g));
|
||||
// Paste in Front and Paste in Back ignore the drawing mode: next to the selection.
|
||||
s.execute("layer.pasteRemembersLayers", &json!({"on": false})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
let front = pasted(s.execute("edit.pasteInFront", &json!({})).unwrap());
|
||||
assert_eq!(parent(&s, front[0]), Some(g));
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let at = |id: u64| d.position(NodeId(id)).unwrap().1;
|
||||
assert_eq!(at(front[0]), at(a) + 1, "just above the selected object");
|
||||
// The object isolated (a double-click with the Selection tool) before anything went inside.
|
||||
let mut s = session();
|
||||
let b = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("shape.ellipse", &json!({"x": 20, "y": 20, "width": 50, "height": 50})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [b]})).unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "inside"})).unwrap();
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
s.pointer(&vectorcraft_tools::PointerEvent::new(vectorcraft_tools::PointerKind::DoubleClick, 150.0, 150.0), ViewInfo::default()).unwrap();
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(NodeId(b)));
|
||||
let ids = pasted(s.execute("edit.paste", &json!({})).unwrap());
|
||||
let g = parent(&s, b).unwrap();
|
||||
assert_eq!((s.doc().unwrap().doc.node(g).unwrap().kind_label(), parent(&s, ids[0])), ("Clip Group", Some(g)));
|
||||
}
|
||||
|
||||
/// In Draw Behind, Paste in Place and Paste on All Artboards put the copies just below the
|
||||
/// selection, as Paste in Back does; Paste puts them on top, as in Draw Normal.
|
||||
#[test]
|
||||
fn paste_in_place_in_draw_behind_goes_behind_the_selection() {
|
||||
let mut s = session();
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("shape.ellipse", &json!({"x": 20, "y": 20, "width": 50, "height": 50})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "behind"})).unwrap();
|
||||
let order = |s: &Session| s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id.0).collect::<Vec<_>>();
|
||||
let first = |r: Value| r["ids"][0].as_u64().unwrap();
|
||||
let placed = first(s.execute("edit.pasteInPlace", &json!({})).unwrap());
|
||||
let o = order(&s);
|
||||
let at = |id: u64| o.iter().position(|x| *x == id).unwrap();
|
||||
assert_eq!(at(placed) + 1, at(a), "just below the selected rectangle: {o:?}");
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
let all = first(s.execute("edit.pasteOnAllArtboards", &json!({})).unwrap());
|
||||
let o = order(&s);
|
||||
let at = |id: u64| o.iter().position(|x| *x == id).unwrap();
|
||||
assert_eq!(at(all) + 1, at(a), "{o:?}");
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
let pasted = first(s.execute("edit.paste", &json!({})).unwrap());
|
||||
assert_eq!(order(&s).last(), Some(&pasted), "Paste: on top");
|
||||
}
|
||||
|
||||
/// #3705: Shift+D (`view.drawMode {}`) goes Draw Normal → Draw Behind → Draw Inside → Draw Normal
|
||||
/// with one path selected, and from Draw Behind back to Draw Normal when Draw Inside can't draw
|
||||
/// into the selection: one group (such as the clip group Draw Inside made), one image, several
|
||||
/// objects or none. It used to stop at Draw Behind with one object selected that isn't a path,
|
||||
/// compound path or type.
|
||||
#[test]
|
||||
fn shift_d_goes_past_draw_behind_whatever_is_selected() {
|
||||
let mut s = session();
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("shape.ellipse", &json!({"x": 120, "y": 0, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
let cycle = |s: &mut Session| (0..3).map(|_| s.execute("view.drawMode", &json!({})).map(|r| r["mode"].clone()).unwrap()).collect::<Vec<_>>();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
assert_eq!(cycle(&mut s), [json!("behind"), json!("inside"), json!("normal")]);
|
||||
s.execute("select.set", &json!({"ids": [a, b]})).unwrap();
|
||||
assert_eq!(cycle(&mut s), [json!("behind"), json!("normal"), json!("behind")]);
|
||||
s.execute("view.drawMode", &json!({"mode": "normal"})).unwrap();
|
||||
let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [g]})).unwrap();
|
||||
assert_eq!(cycle(&mut s), [json!("behind"), json!("normal"), json!("behind")], "one group selected");
|
||||
assert!(s.draw_inside.is_none());
|
||||
s.execute("view.drawMode", &json!({"mode": "normal"})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
assert_eq!(cycle(&mut s), [json!("behind"), json!("normal"), json!("behind")]);
|
||||
// Asked for by name, Draw Inside is still refused without a path, compound path or type.
|
||||
s.execute("select.set", &json!({"ids": [g]})).unwrap();
|
||||
assert!(s.execute("view.drawMode", &json!({"mode": "inside"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_for_screens_writes_every_combination() {
|
||||
let mut s = Session::new();
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
+5
-1
@@ -2961,7 +2961,11 @@ Placement: `edit.paste {center}` centres the objects on a point (the app passes
|
||||
by `dx`/`dy` (the Paste Offset preference). With nothing selected, `edit.pasteInFront` / `edit.pasteInBack` put them
|
||||
on top / at the bottom of the current layer. `edit.pasteOnAllArtboards` keeps their offset to the artboard they were
|
||||
copied from. `layer.pasteRemembersLayers {on?}` (a document option, `document.inspect` → `pasteRemembersLayers`)
|
||||
pastes objects back into the layers they came from, by name, making missing ones.
|
||||
pastes objects back into the layers they came from, by name, making missing ones. Drawing modes (`view.drawMode`) apply
|
||||
too: in Draw Inside, `edit.paste`, `edit.pasteInPlace` and `edit.pasteOnAllArtboards` put the objects inside the
|
||||
object drawn into (its clip group, made on the first paste), whatever Paste Remembers Layers says; in Draw Behind,
|
||||
`edit.pasteInPlace` and `edit.pasteOnAllArtboards` put them just below the selection. `edit.pasteInFront` and
|
||||
`edit.pasteInBack` ignore the drawing mode.
|
||||
|
||||
Artboards copy with their art. `artboard.copy {index? | indices?, art?}` puts artboards on the clipboard together
|
||||
with the art fully inside each (`index` and `indices` default to the artboards the Artboard tool has selected; `art`
|
||||
|
||||
File diff suppressed because one or more lines are too long
+4
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user