mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #1902 from Messhias/feat/g12-layers-collect-export
G12: Collect for Export on Layers (≡ and row menu)
This commit is contained in:
@@ -20,7 +20,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Collect for Export",
|
||||
["Object", "Collect for Export"],
|
||||
None,
|
||||
"{ids?: [id…] (default: the selected objects), multiple?: true (an asset per object; false: one asset of them all), name?: (the asset's name, when it makes one)} collect art as assets for Window › Asset Export and Export for Screens' Assets tab, named after the object (else Asset 1, Asset 2…); art already collected as such an asset keeps it → {assets: [asset id…] (in order), added: n}. Assets follow their art: moving it moves the crop, deleting it removes it from the asset",
|
||||
"{ids?: [id…] (default: the selected objects; a layer id collects the art under it, including in its sublayers), multiple?: true (an asset per object; false: one asset of them all), name?: (the asset's name, when it makes one)} collect art as assets for Window › Asset Export and Export for Screens' Assets tab, named after the object (else Asset 1, Asset 2…); art already collected as such an asset keeps it → {assets: [asset id…] (in order), added: n}. Assets follow their art: moving it moves the crop, deleting it removes it from the asset",
|
||||
has_doc,
|
||||
add
|
||||
),
|
||||
@@ -78,13 +78,32 @@ fn clean_name(name: &str) -> Option<String> {
|
||||
(!n.is_empty()).then_some(n)
|
||||
}
|
||||
|
||||
/// Ids of art under `id`: the id itself when it is not a layer, else every non-layer under it
|
||||
/// (sublayers unfolded). Layers alone have nothing to collect.
|
||||
fn art_under(doc: &vectorcraft_doc::Document, id: NodeId, out: &mut Vec<NodeId>) {
|
||||
let Some(n) = doc.node(id) else { return };
|
||||
if n.is_layer() {
|
||||
if let Some(kids) = n.children() {
|
||||
for c in kids {
|
||||
art_under(doc, c.id, out);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
out.push(id);
|
||||
}
|
||||
|
||||
fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "assets.add";
|
||||
let ids = if p.get("ids").is_some_and(|v| !v.is_null()) {
|
||||
// An explicit list is authoritative: never silently drop malformed or stale ids.
|
||||
let ids = checked_ids_param(s, p, "ids", C)?;
|
||||
let d = &s.doc()?.doc;
|
||||
roots_of(d, d.paint_order(ids))
|
||||
let mut art = Vec::new();
|
||||
for id in &ids {
|
||||
art_under(d, *id, &mut art);
|
||||
}
|
||||
roots_of(d, d.paint_order(art))
|
||||
} else {
|
||||
selected_roots(s)?
|
||||
};
|
||||
|
||||
@@ -68,14 +68,37 @@ fn multiple_gives_an_asset_per_object_and_single_one_of_them_all() {
|
||||
let again = run(&mut s, "assets.add", json!({"ids": [b, a]}));
|
||||
assert_eq!((again["added"].clone(), again["assets"].clone()), (json!(0), json!([list[0]["id"], list[1]["id"]])));
|
||||
assert_eq!(undo_len(&s), steps);
|
||||
// Layers aren't art; nothing to collect is an error.
|
||||
// A layer id collects the art under it (Layers panel Collect for Export / MCP).
|
||||
let layer = s.doc().unwrap().doc.layers[0].id.0;
|
||||
assert!(s.execute("assets.add", &json!({"ids": [layer]})).is_err());
|
||||
let from_layer = run(&mut s, "assets.add", json!({"ids": [layer]}));
|
||||
assert_eq!(from_layer["added"], 0, "those objects are assets already");
|
||||
assert_eq!(from_layer["assets"], json!([list[0]["id"], list[1]["id"]]));
|
||||
// An empty layer has nothing to collect.
|
||||
let empty = run(&mut s, "layer.new", json!({}))["id"].as_u64().unwrap();
|
||||
assert!(s.execute("assets.add", &json!({"ids": [empty]})).is_err());
|
||||
run(&mut s, "select.set", json!({"ids": []}));
|
||||
assert!(s.execute("assets.add", &json!({})).is_err());
|
||||
assert!(s.execute("assets.add", &json!({"ids": [a, b], "name": "Two"})).is_err(), "a name names one asset");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layer_id_collects_the_art_under_it_including_sublayers() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0);
|
||||
let sub = run(&mut s, "layer.newSublayer", json!({}))["id"].as_u64().unwrap();
|
||||
run(&mut s, "layer.setCurrent", json!({"id": sub}));
|
||||
let b = rect(&mut s, 50.0, 10.0, 30.0, 20.0);
|
||||
let layer = s.doc().unwrap().doc.layers[0].id.0;
|
||||
let r = run(&mut s, "assets.add", json!({"ids": [layer]}));
|
||||
assert_eq!(r["added"], 2);
|
||||
let list = assets(&mut s);
|
||||
assert_eq!(list.len(), 2);
|
||||
assert_eq!((list[0]["nodes"].clone(), list[1]["nodes"].clone()), (json!([a]), json!([b])));
|
||||
let one = run(&mut s, "assets.add", json!({"ids": [layer], "multiple": false}));
|
||||
assert_eq!(one["added"], 1);
|
||||
assert_eq!(assets(&mut s).last().unwrap()["nodes"], json!([a, b]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_the_object_prunes_it_and_undo_brings_it_back() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -994,6 +994,13 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
||||
out.actions.push(("ui.layerOptions".into(), json!({"ids": [n.id.0]})));
|
||||
}
|
||||
}
|
||||
// Row menu: Collect for Export on this row (or every highlighted row when this is one of them).
|
||||
let collect_ids: Vec<u64> = if view.rows.contains(&n.id) { view.rows.iter().map(|id| id.0).collect() } else { vec![n.id.0] };
|
||||
resp.context_menu(|ui| {
|
||||
collect_for_export_menu(ui, true, |multiple| {
|
||||
out.actions.push(("assets.add".into(), json!({ "ids": collect_ids.clone(), "multiple": multiple })));
|
||||
});
|
||||
});
|
||||
// Dragging rows: the highlighted rows when this is one of them, else this row.
|
||||
if resp.drag_started() && view.column.is_none() {
|
||||
let ids = if view.rows.contains(&n.id) { view.rows.clone() } else { vec![n.id] };
|
||||
@@ -1486,6 +1493,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
Some(("Merge Selected", "layer.merge", json!({}), rows.len() >= 2, false)),
|
||||
Some(("Flatten Artwork", "layer.flatten", json!({}), true, false)),
|
||||
Some(("Collect in New Layer", "layer.collectInNew", json!({}), !rows.is_empty() || has_sel, false)),
|
||||
// Sentinel: rendered as Collect for Export › As Single / As Multiple (assets.add).
|
||||
Some(("Collect for Export", "", json!({}), !targets.is_empty() || has_sel, false)),
|
||||
Some(("Release to Layers (Sequence)", "layer.releaseToLayers", json!({}), one_container, false)),
|
||||
Some(("Release to Layers (Build)", "layer.releaseToLayersBuild", json!({}), one_container, false)),
|
||||
Some(("Reverse Order", "layer.reverse", json!({}), rows.len() >= 2, false)),
|
||||
@@ -1525,6 +1534,15 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
None => {
|
||||
ui.separator();
|
||||
}
|
||||
Some(("Collect for Export", _, _, enabled, _)) => {
|
||||
collect_for_export_menu(ui, enabled, |multiple| {
|
||||
let mut p = json!({ "multiple": multiple });
|
||||
if !targets.is_empty() {
|
||||
p["ids"] = json!(targets.iter().map(|id| id.0).collect::<Vec<_>>());
|
||||
}
|
||||
app.run("assets.add", p).ok();
|
||||
});
|
||||
}
|
||||
Some((label, cmd, p, enabled, checked)) => {
|
||||
if widgets::menu_item(ui, label, enabled, checked) {
|
||||
app.run(cmd, p).ok();
|
||||
@@ -1534,6 +1552,23 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect for Export › As Single Asset / As Multiple Assets (`assets.add`).
|
||||
fn collect_for_export_menu(ui: &mut Ui, enabled: bool, mut run: impl FnMut(bool)) {
|
||||
ui.add_enabled_ui(enabled, |ui| {
|
||||
// English labels match the rest of this panel menu (and Object › Collect for Export).
|
||||
ui.menu_button("Collect for Export", |ui| {
|
||||
if widgets::menu_item(ui, "As Single Asset", true, false) {
|
||||
run(false);
|
||||
ui.close();
|
||||
}
|
||||
if widgets::menu_item(ui, "As Multiple Assets", true, false) {
|
||||
run(true);
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// `ui.layersExpand {ids?, open?}`: open (or close) rows of the active document's Layers panel;
|
||||
/// without `ids`, every row that holds others.
|
||||
pub fn expand(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
@@ -2500,6 +2535,7 @@ mod tests {
|
||||
"Merge Selected",
|
||||
"Flatten Artwork",
|
||||
"Collect in New Layer",
|
||||
"Collect for Export",
|
||||
"Release to Layers (Sequence)",
|
||||
"Release to Layers (Build)",
|
||||
"Reverse Order",
|
||||
|
||||
+10
-6
@@ -2766,13 +2766,14 @@ defaults); presets and remembered settings are preferences.
|
||||
|
||||
## Asset Export
|
||||
|
||||
Assets are pieces of art collected for export on their own (Window › Asset Export, Object › Collect for Export, File ›
|
||||
Export Selection…).
|
||||
Assets are pieces of art collected for export on their own (Window › Asset Export, Object › Collect for Export, the
|
||||
Layers panel ≡ menu and row menu, File › Export Selection…).
|
||||
`assets.add {ids?, multiple?}` collects the selected objects (or `ids`): one asset per object, or with
|
||||
`multiple: false` one asset of them all, named after the object or `Asset 1`, `Asset 2`…; art already collected keeps
|
||||
its asset. Assets name their objects, so they follow edits: moving the art moves the crop, and deleting it
|
||||
removes it from its asset (an asset left with nothing goes). `assets.list` gives each asset's id, name, objects and
|
||||
crop (`bounds`); `assets.rename {asset, name}` and `assets.remove {assets}` are undo steps.
|
||||
`multiple: false` one asset of them all, named after the object or `Asset 1`, `Asset 2`…; a layer id collects the art
|
||||
under it (sublayers included). Art already collected keeps its asset. Assets name their objects, so they follow edits:
|
||||
moving the art moves the crop, and deleting it removes it from its asset (an asset left with nothing goes).
|
||||
`assets.list` gives each asset's id, name, objects and crop (`bounds`); `assets.rename {asset, name}` and
|
||||
`assets.remove {assets}` are undo steps.
|
||||
|
||||
`assets.export {assets?, folder?, zip?}` writes each asset's art alone, cropped to it and named after it, in every
|
||||
format row of the document's export settings, the ones Export for Screens remembers (`document.exportSettings`); any
|
||||
@@ -2783,9 +2784,12 @@ renderer measures for what the art draws, its effects included.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"assets.add","params":{"multiple":true}}}
|
||||
{"name":"run_command","arguments":{"command":"assets.add","params":{"ids":[12],"multiple":true}}}
|
||||
{"name":"run_command","arguments":{"command":"assets.settings.set","params":{"formats":[{"format":"png","scale":"1x"},{"format":"png","scale":"2x"},{"format":"svg"}]}}}
|
||||
{"name":"run_command","arguments":{"command":"assets.export","params":{"zip":true}}}
|
||||
```
|
||||
A layer id in `ids` (12 above) is the same as Collect for Export on that Layers row: every object under the layer
|
||||
becomes an asset (or one asset with `multiple: false`).
|
||||
|
||||
## CSS Properties
|
||||
|
||||
|
||||
Reference in New Issue
Block a user