mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3805 from Taha-Mahmoodi/fix/3705-double-click-clip-group
Fix #3705: art drawn or pasted inside an isolated object in Draw Inside stays in isolation mode
This commit is contained in:
@@ -73,7 +73,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Drawing Mode",
|
||||
[],
|
||||
Some("Shift+D"),
|
||||
"{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)",
|
||||
"{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; when the Draw Inside target is isolated, drawing or pasting into it moves isolation to its clip group)",
|
||||
has_doc,
|
||||
draw_mode
|
||||
),
|
||||
@@ -149,16 +149,18 @@ fn insert_look_turned(
|
||||
let mode = s.draw_mode;
|
||||
let inside = s.draw_inside;
|
||||
let behind_of = s.doc()?.selection.in_paint_order(&s.doc()?.doc).first().copied();
|
||||
let id = s.edit(label, |d, sel| {
|
||||
let (id, into) = s.edit(label, |d, sel| {
|
||||
let id = d.alloc_id();
|
||||
let mut n = Node::new(id, kind);
|
||||
look.apply(d, &mut n);
|
||||
n.name = name;
|
||||
n.bbox_angle = bbox_angle;
|
||||
let mut into = None;
|
||||
match (mode, inside) {
|
||||
(crate::DrawMode::Inside, Some(target)) if d.node(target).is_some() => {
|
||||
let group = draw_inside_group(d, target)?;
|
||||
d.insert(Some(group), usize::MAX, n)?;
|
||||
into = Some((target, group));
|
||||
}
|
||||
(crate::DrawMode::Behind, _) => match behind_of.and_then(|b| d.position(b)) {
|
||||
Some((par, idx, _)) => {
|
||||
@@ -175,8 +177,11 @@ fn insert_look_turned(
|
||||
if select {
|
||||
sel.set([id]);
|
||||
}
|
||||
Ok(id)
|
||||
Ok((id, into))
|
||||
})?;
|
||||
if let Some((target, group)) = into {
|
||||
isolate_draw_inside_group(s, target, group)?;
|
||||
}
|
||||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
@@ -204,6 +209,21 @@ pub(crate) fn draw_inside_group(d: &mut vectorcraft_doc::Document, target: NodeI
|
||||
Ok(gid)
|
||||
}
|
||||
|
||||
/// Draw Inside into the isolated object (#3705): isolation mode moves to the clip group the art
|
||||
/// went into ([`draw_inside_group`]), so what is drawn or pasted inside the object is in isolation
|
||||
/// mode with it, shown undimmed and picked by clicks, instead of locked beside it. Art added in
|
||||
/// isolation mode belongs with what is isolated: it takes the isolated art's place when the mode
|
||||
/// ends (<https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/isolate-objects.html>).
|
||||
/// A drag's next preview, replayed on the drag's snapshot, makes the group again under the same
|
||||
/// id, so isolation stays on it; cancelling the drag restores the isolation it began with.
|
||||
pub(crate) fn isolate_draw_inside_group(s: &mut Session, target: NodeId, group: NodeId) -> Result<()> {
|
||||
let st = s.doc_mut()?;
|
||||
if st.isolation == Some(target) {
|
||||
st.isolation = Some(group);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
|
||||
@@ -411,6 +411,7 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
st.sync_new_art();
|
||||
super::maskedit::follow_history(st, editing);
|
||||
leave_isolation_if_gone(st);
|
||||
Ok(json!({ "undone": label }))
|
||||
}
|
||||
|
||||
@@ -436,9 +437,19 @@ fn redo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
st.sync_new_art();
|
||||
super::maskedit::follow_history(st, editing);
|
||||
leave_isolation_if_gone(st);
|
||||
Ok(json!({ "redone": label }))
|
||||
}
|
||||
|
||||
/// After undo or redo, isolation mode ends when the isolated object is no longer in the document,
|
||||
/// such as the clip group Draw Inside made around the isolated object (#3705), rather than naming
|
||||
/// an id a later object may take.
|
||||
fn leave_isolation_if_gone(st: &mut crate::DocState) {
|
||||
if st.isolation.is_some_and(|i| st.doc.node(i).is_none()) {
|
||||
st.isolation = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// History panel › Clear History: removes the steps after the current state and the steps before
|
||||
/// it, and leaves the document as it is. Typing or a drag still in progress is first kept as a
|
||||
/// step, as Undo keeps it, so it is removed with the others and its change stays in the document.
|
||||
@@ -724,7 +735,7 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
// In place, in front, in back: onto the active artboard when the UI names it.
|
||||
_ => vec![onto_artboard(&st.doc, p, clip)],
|
||||
};
|
||||
let (ids, imported, artboards) = s.edit(mode.label(), |d, sel| {
|
||||
let (ids, imported, artboards, into) = 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()?;
|
||||
@@ -771,8 +782,11 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
// Pasted type arrives without the layout bounds cache: fill it so boxes and alignment are
|
||||
// right away (opening the file later would, but paste must not depend on a reopen).
|
||||
super::typecmd::refresh_bounds_of(d, &new_ids);
|
||||
Ok((new_ids, imported, artboards))
|
||||
Ok((new_ids, imported, artboards, into))
|
||||
})?;
|
||||
if let (Some(target), Some(group)) = (inside, into) {
|
||||
super::create::isolate_draw_inside_group(s, target, group)?;
|
||||
}
|
||||
// The Artboard tool selects the pasted artboards, the first one active.
|
||||
if !artboards.is_empty() && s.tool_id() == "artboard" {
|
||||
s.set_tool_option("selected", &json!(artboards));
|
||||
|
||||
@@ -242,6 +242,64 @@ fn pasting_in_draw_inside_puts_the_copies_inside_the_object() {
|
||||
assert_eq!((s.doc().unwrap().doc.node(g).unwrap().kind_label(), parent(&s, ids[0])), ("Clip Group", Some(g)));
|
||||
}
|
||||
|
||||
/// #3705: in Draw Inside, with the object isolated by a double-click with the Selection tool, what
|
||||
/// is drawn or pasted inside it stays in isolation mode: isolation moves to the clip group Draw
|
||||
/// Inside makes around the object, so a click picks the pasted copy and it draws undimmed. It used
|
||||
/// to stay on the bare path, the new art locked and dimmed beside it, and double-clicks never
|
||||
/// reached the clip group. Undoing the step that made the clip group leaves isolation mode, as the
|
||||
/// group is gone.
|
||||
#[test]
|
||||
fn drawing_inside_an_isolated_object_keeps_the_new_art_in_isolation_mode() {
|
||||
use vectorcraft_geom::Affine;
|
||||
use vectorcraft_render::{RenderOptions, Renderer};
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
let mut s = session();
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
let red = s.execute("shape.ellipse", &json!({"x": 20, "y": 20, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("paint.setFill", &json!({"color": "#ff0000", "ids": [red]})).unwrap();
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "inside"})).unwrap();
|
||||
let ev = |s: &mut Session, kinds: &[PointerKind], x: f64, y: f64| {
|
||||
for k in kinds {
|
||||
s.pointer(&PointerEvent::new(*k, x, y), ViewInfo::default()).unwrap();
|
||||
}
|
||||
};
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
ev(&mut s, &[PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick], 150.0, 150.0);
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(NodeId(a)), "the double-click isolates the object");
|
||||
// An ellipse dragged out with the Ellipse tool (several previews) goes inside the object, and
|
||||
// isolation mode with it.
|
||||
s.select_tool("ellipse", ViewInfo::default()).unwrap();
|
||||
ev(&mut s, &[PointerKind::Down], 110.0, 110.0);
|
||||
for d in [5.0, 10.0, 15.0, 20.0] {
|
||||
ev(&mut s, &[PointerKind::Drag], 110.0 + d, 110.0 + d);
|
||||
}
|
||||
ev(&mut s, &[PointerKind::Up], 130.0, 130.0);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let g = d.parent_of(NodeId(a)).unwrap();
|
||||
assert_eq!(d.node(g).unwrap().kind_label(), "Clip Group");
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(g), "isolation mode follows the object into its clip group");
|
||||
// A paste goes into it too, in isolation mode, and a click with the Selection tool picks it.
|
||||
let pasted = s.execute("edit.paste", &json!({"center": [170, 170]})).unwrap()["ids"][0].as_u64().unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!((st.doc.parent_of(NodeId(pasted)), st.isolation), (Some(g), Some(g)));
|
||||
// Isolation mode draws the art around what is isolated dimmed: the red copy is not.
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), isolated: st.isolation, ..Default::default() };
|
||||
let p = Renderer::new().render(&st.doc, 400, 300, Affine::IDENTITY, &opts).pixel(170, 170);
|
||||
assert_eq!([p[0], p[1], p[2]], [255, 0, 0], "the pasted copy draws undimmed");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
ev(&mut s, &[PointerKind::Down, PointerKind::Up], 170.0, 170.0);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![NodeId(pasted)], "a click picks the pasted copy");
|
||||
// Undoing the paste keeps isolation mode; undoing the ellipse, which made the clip group, ends it.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(g));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.node(g).is_none() && st.isolation.is_none(), "{:?}", st.isolation);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
+4
-1
@@ -2966,7 +2966,10 @@ pastes objects back into the layers they came from, by name, making missing ones
|
||||
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.
|
||||
`edit.pasteInBack` ignore the drawing mode. When the object drawn into is the one isolation mode isolates (`object.isolate`,
|
||||
or a double-click on it with the Selection tool), art drawn or pasted inside it takes isolation mode into its clip group,
|
||||
so the new art can be picked and isn't dimmed; `edit.undo` or `edit.redo` that removes the isolated object ends
|
||||
isolation 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
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user