mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Guides: drag guide rows to another layer; put older guides on a layer
Guide rows in the Layers panel drag (the selected guides when the row is one of them) onto any row of another layer, which lights up, and go to that layer in one undo step (guide.setLayer takes indexes). Guides on no layer, from files saved before guides had layers, go on the top visible, unlocked layer when a document opens or is reverted. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
This commit is contained in:
1 parent
02e93902b3
commit
2972e2e6bf
7 files changed
+97
-10
No files matched your search
@@ -1004,6 +1004,24 @@ impl Document {
|
||||
pub fn guide_editable(&self, g: &Guide) -> bool {
|
||||
g.layer.is_none_or(|l| self.node(l).is_none() || self.is_editable(l))
|
||||
}
|
||||
/// Put the ruler guides on no layer (from files saved before guides had layers), or on one that
|
||||
/// is gone, on the top visible, unlocked layer ([`Self::default_layer`]). Returns how many.
|
||||
pub fn adopt_guides(&mut self) -> usize {
|
||||
let Some(home) = self.default_layer() else { return 0 };
|
||||
let lost: Vec<usize> = self
|
||||
.guides
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, g)| g.layer.is_none_or(|l| !self.node(l).is_some_and(Node::is_layer)))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
for i in &lost {
|
||||
if let Some(g) = self.guides.get_mut(*i) {
|
||||
g.layer = Some(home);
|
||||
}
|
||||
}
|
||||
lost.len()
|
||||
}
|
||||
/// The ruler guides on layer `layer` (not its sublayers'), by index.
|
||||
pub fn guides_on(&self, layer: NodeId) -> impl Iterator<Item = (usize, &Guide)> + '_ {
|
||||
self.guides.iter().enumerate().filter(move |(_, g)| g.layer == Some(layer))
|
||||
|
||||
@@ -222,6 +222,14 @@ mod tests {
|
||||
assert!(d.guide_shown(&gone) && d.guide_editable(&gone));
|
||||
d.guides.push(g);
|
||||
assert_eq!(d.guides_on(layer).count(), 1);
|
||||
// Guides on no layer, or a gone one, are adopted by the top visible, unlocked layer.
|
||||
d.node_mut(layer).unwrap().visible = true;
|
||||
d.node_mut(layer).unwrap().locked = false;
|
||||
d.guides.push(crate::Guide::new(false, 5.0));
|
||||
d.guides.push(gone);
|
||||
assert_eq!(d.adopt_guides(), 2);
|
||||
assert!(d.guides.iter().all(|g| g.layer == Some(layer)));
|
||||
assert_eq!(d.adopt_guides(), 0, "once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -205,7 +205,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Move Guide to Layer",
|
||||
[],
|
||||
None,
|
||||
"{index?, layer: id} put ruler guide `index`, else the selected guides, on layer `layer` (a layer or sublayer), as for art moved to another layer → {count}",
|
||||
"{index?, indexes?: [index…], layer: id} put ruler guide `index` (or guides `indexes`), else the selected guides, on layer `layer` (a layer or sublayer), as dragging their rows onto that layer's row in the Layers panel does → {count}",
|
||||
guides_unlocked,
|
||||
guide_set_layer
|
||||
),
|
||||
@@ -591,9 +591,10 @@ pub(crate) fn guide_remove(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn guide_set_layer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let layer = id_param(p, "layer").filter(|l| s.doc().is_ok_and(|st| st.doc.node(*l).is_some_and(Node::is_layer)));
|
||||
let layer = layer.ok_or_else(|| bad("guide.setLayer", "layer must name a layer or sublayer"))?;
|
||||
let moving: Vec<usize> = match p.get("index") {
|
||||
Some(v) => vec![guide_index(s, Some(v), "guide.setLayer")?],
|
||||
None => s.doc()?.selection.guides.clone(),
|
||||
let moving: Vec<usize> = match (p.get("index"), p.get("indexes").and_then(Value::as_array)) {
|
||||
(Some(v), _) => vec![guide_index(s, Some(v), "guide.setLayer")?],
|
||||
(None, Some(list)) => list.iter().take(10_000).map(|v| guide_index(s, Some(v), "guide.setLayer")).collect::<Result<Vec<_>>>()?,
|
||||
(None, None) => s.doc()?.selection.guides.clone(),
|
||||
};
|
||||
if moving.is_empty() {
|
||||
return Err(bad("guide.setLayer", "no guide selected (or give an index)"));
|
||||
|
||||
@@ -1041,6 +1041,8 @@ impl Session {
|
||||
/// Add a document and make it active.
|
||||
pub fn add_document(&mut self, mut doc: Document, path: Option<String>) -> usize {
|
||||
Self::refresh_text_bounds(&mut doc);
|
||||
// Guides from files saved before guides had layers go on a layer.
|
||||
doc.adopt_guides();
|
||||
self.reset_tool_for_doc_switch();
|
||||
let mut st = DocState::new(doc, path);
|
||||
st.history.limit = self.prefs.history_states as usize;
|
||||
@@ -1056,6 +1058,7 @@ impl Session {
|
||||
return false;
|
||||
}
|
||||
Self::refresh_text_bounds(&mut doc);
|
||||
doc.adopt_guides();
|
||||
if self.active == Some(index) {
|
||||
// Pending tool work (typing, a drag) belongs to the content being thrown away.
|
||||
self.reset_tool_for_doc_switch();
|
||||
|
||||
@@ -914,7 +914,10 @@ fn artboard_guides_go_with_their_artboard() {
|
||||
let n = undo_len(&s);
|
||||
s.execute("artboard.delete", &json!({"index": 0})).unwrap();
|
||||
assert_eq!((guides(&s).len(), selected_guides(&s)), (3, vec![0, 1]));
|
||||
assert_eq!(list(&mut s)[0], json!({"index": 0, "vertical": false, "pos": 100.0, "selected": true}));
|
||||
assert_eq!(
|
||||
list(&mut s)[0],
|
||||
json!({"index": 0, "vertical": false, "pos": 100.0, "selected": true, "layer": layer, "shown": true, "editable": true})
|
||||
);
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((guides(&s).len(), selected_guides(&s)), (4, vec![1, 2]));
|
||||
@@ -963,6 +966,19 @@ fn guides_dragged_out_of_a_ruler_snap_and_take_the_artboard_tool_s_artboard() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Guides saved before guides had layers are put on a layer when the document opens.
|
||||
#[test]
|
||||
fn guides_from_older_files_are_put_on_a_layer() {
|
||||
let mut s = Session::new();
|
||||
let mut d = vectorcraft_doc::Document::new(400.0, 300.0);
|
||||
d.guides.push(vectorcraft_doc::Guide::new(true, 50.0));
|
||||
d.guides.push(vectorcraft_doc::Guide::new(false, 80.0));
|
||||
s.add_document(d, None);
|
||||
let st = s.doc().unwrap();
|
||||
let top = st.doc.default_layer();
|
||||
assert!(top.is_some() && st.doc.guides.iter().all(|g| g.layer == top), "{:?}", st.doc.guides);
|
||||
}
|
||||
|
||||
/// Ruler guides are layer objects: made on the current layer, hidden, locked and deleted with it,
|
||||
/// and moved to another layer.
|
||||
#[test]
|
||||
@@ -987,6 +1003,11 @@ fn ruler_guides_live_on_layers() {
|
||||
assert_eq!(s.doc().unwrap().doc.guides[1].layer, Some(first));
|
||||
s.execute("guide.setLayer", &json!({"index": 1, "layer": second.0})).unwrap();
|
||||
assert!(s.execute("guide.setLayer", &json!({"index": 1, "layer": 99999})).is_err());
|
||||
// Several at once, as dragging their rows onto a layer's row does.
|
||||
s.execute("guide.setLayer", &json!({"indexes": [0, 1], "layer": first.0})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.layer == Some(first)));
|
||||
s.execute("guide.setLayer", &json!({"indexes": [1], "layer": second.0})).unwrap();
|
||||
assert!(s.execute("guide.setLayer", &json!({"indexes": [7], "layer": first.0})).is_err());
|
||||
// Deleting a layer deletes its guides; undo brings them back.
|
||||
s.execute("layer.delete", &json!({"ids": [second.0]})).unwrap();
|
||||
assert_eq!(guides(&s), [(true, 200.0)]);
|
||||
|
||||
@@ -98,6 +98,9 @@ enum LayersDrag {
|
||||
Rows(Vec<NodeId>),
|
||||
/// The selected-art square: the selected objects go to the row it is dropped on.
|
||||
Art,
|
||||
/// Ruler guide rows (indexes into the document's guides): they go to the layer of the row
|
||||
/// they are dropped on.
|
||||
Guides(Vec<usize>),
|
||||
}
|
||||
|
||||
/// A drag down the eye or lock column: every row it passes takes `value`.
|
||||
@@ -549,6 +552,7 @@ fn takes(n: &Node, drag: &LayersDrag, doc: &Document) -> bool {
|
||||
NodeKind::Group { .. } => match drag {
|
||||
LayersDrag::Rows(ids) => !ids.iter().any(|i| doc.node(*i).is_some_and(Node::is_layer)),
|
||||
LayersDrag::Art => true,
|
||||
LayersDrag::Guides(_) => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
@@ -559,7 +563,7 @@ fn takes(n: &Node, drag: &LayersDrag, doc: &Document) -> bool {
|
||||
/// from) the selected guides.
|
||||
fn guide_row(ui: &mut Ui, view: &View, item: &Row, i: usize, out: &mut Out) {
|
||||
let (t, h) = (&view.t, view.h);
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click_and_drag());
|
||||
let Some(g) = view.doc.guides.get(i) else { return };
|
||||
let selected = view.guides.contains(&i);
|
||||
if resp.hovered() {
|
||||
@@ -588,6 +592,29 @@ fn guide_row(ui: &mut Ui, view: &View, item: &Row, i: usize, out: &mut Out) {
|
||||
let m = ui.input(|i| i.modifiers);
|
||||
out.actions.push(("guide.select".into(), json!({"indexes": [i], "toggle": m.shift || m.command})));
|
||||
}
|
||||
// Dragged: the selected guides when this is one of them, else this one, to another layer's row.
|
||||
if resp.drag_started() {
|
||||
let guides = if selected { view.guides.clone() } else { vec![i] };
|
||||
egui::DragAndDrop::set_payload(ui.ctx(), LayersDrag::Guides(guides));
|
||||
}
|
||||
if resp.dragged() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
}
|
||||
// Guides dropped here go to this guide's layer.
|
||||
if let Some(drag) = resp.dnd_hover_payload::<LayersDrag>()
|
||||
&& let LayersDrag::Guides(guides) = &*drag
|
||||
{
|
||||
guide_drop(ui, view, guides, item.node.id, r, &resp, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Guide rows `guides` dragged over row `r` (`resp`) of layer `layer`: the row lights up, and the
|
||||
/// release puts them on that layer (`guide.setLayer`, one undo step).
|
||||
fn guide_drop(ui: &Ui, view: &View, guides: &[usize], layer: NodeId, r: egui::Rect, resp: &egui::Response, out: &mut Out) {
|
||||
ui.painter().rect_stroke(r.shrink(1.0), 0.0, Stroke::new(2.0, view.t.accent), StrokeKind::Inside);
|
||||
if resp.dnd_release_payload::<LayersDrag>().is_some() {
|
||||
out.actions.push(("guide.setLayer".into(), json!({"indexes": guides, "layer": layer.0})));
|
||||
}
|
||||
}
|
||||
|
||||
/// The widgets of row `item`.
|
||||
@@ -828,6 +855,13 @@ fn drop_target(ui: &Ui, view: &View, n: &Node, r: egui::Rect, indent: f32, open:
|
||||
let moving: Vec<NodeId> = match &*drag {
|
||||
LayersDrag::Rows(ids) => ids.clone(),
|
||||
LayersDrag::Art => view.sel.iter().copied().collect(),
|
||||
LayersDrag::Guides(guides) => {
|
||||
// Guides go to the layer of the row (the row's own, for a layer).
|
||||
if let Some(layer) = if n.is_layer() { Some(n.id) } else { doc.layer_containing(n.id) } {
|
||||
guide_drop(ui, view, guides, layer, r, resp, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Never into itself or a row inside it.
|
||||
let around = doc.ancestry(n.id).unwrap_or_default();
|
||||
@@ -842,7 +876,7 @@ fn drop_target(ui: &Ui, view: &View, n: &Node, r: egui::Rect, indent: f32, open:
|
||||
LayersDrag::Art => Place::Above,
|
||||
LayersDrag::Rows(_) if inside && rel >= 0.25 && (rel <= 0.75 || open) => Place::Inside,
|
||||
LayersDrag::Rows(_) if rel < 0.5 => Place::Above,
|
||||
LayersDrag::Rows(_) => Place::Below,
|
||||
LayersDrag::Rows(_) | LayersDrag::Guides(_) => Place::Below,
|
||||
};
|
||||
let stroke = Stroke::new(2.0, view.t.accent);
|
||||
match place {
|
||||
@@ -862,6 +896,7 @@ fn drop_target(ui: &Ui, view: &View, n: &Node, r: egui::Rect, indent: f32, open:
|
||||
// The selection's top objects, as Arrange and Group take them.
|
||||
LayersDrag::Art => doc.paint_order(moving.iter().copied()).iter().map(|i| i.0).collect(),
|
||||
LayersDrag::Rows(ids) => ids.iter().map(|i| i.0).collect(),
|
||||
LayersDrag::Guides(_) => return,
|
||||
};
|
||||
out.actions.push(("layer.move".into(), json!({"ids": ids, "target": n.id.0, "place": place.id(), "copy": copy})));
|
||||
}
|
||||
|
||||
@@ -257,9 +257,10 @@ where it is greyed. The modifier keys still work while dragging (Cmd on a corner
|
||||
perspective, Cmd on a side shears).
|
||||
|
||||
Ruler guides are layer objects: `guide.add` puts a guide on the current layer (or `layer`), where it shows, hides,
|
||||
locks and is deleted with its layer and is listed in the Layers panel (a click on its row selects it);
|
||||
`guide.setLayer {index?, layer}` moves guides to another layer, and `guide.list` gives each
|
||||
guide's `layer`, `shown` and `editable`.
|
||||
locks and is deleted with its layer and is listed in the Layers panel (a click on its row selects it, a drag onto
|
||||
another layer's rows moves it there); `guide.setLayer {index?, indexes?, layer}` moves guides to another layer, and
|
||||
`guide.list` gives each guide's `layer`, `shown` and `editable`. Guides in files saved before guides had layers go on
|
||||
the top visible, unlocked layer when the document opens.
|
||||
|
||||
Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title out of the dock floats that panel
|
||||
inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A
|
||||
|
||||
Reference in new issue
Block a user