//! Layers panel, artboards. use serde_json::{Value, json}; use vectorcraft_doc::{Artboard, LayerColor, NodeId, NodeKind}; use vectorcraft_geom::Rect; use super::*; use crate::EngineError; pub fn specs() -> Vec { vec![ cmd!("layer.new", "New Layer…", ["Window", "Layers"], Some("Cmd+L"), "{name?} → {id} (above the current layer)", has_doc, new_layer), cmd!("layer.newSublayer", "New Sublayer…", ["Window", "Layers"], None, "{parent?: id, name?} → {id}", has_doc, new_sublayer), cmd!("layer.delete", "Delete Layer", ["Window", "Layers"], None, "{id?} (default: current layer)", has_doc, delete_layer), cmd!("layer.duplicate", "Duplicate Layer", ["Window", "Layers"], None, "{id?}", has_doc, duplicate_layer), cmd!("layer.setCurrent", "Set Current Layer", [], None, "{id}", has_doc, set_current), cmd!( "layer.setProps", "Layer Options…", ["Window", "Layers"], None, "{id, name?, visible?, locked?, template?, printable?, color?: index 0..26}", has_doc, set_props ), cmd!("layer.collectInNew", "Collect in New Layer", ["Window", "Layers"], None, "{}", has_selection, collect), cmd!("layer.selectAll", "Select All Art on Layer", [], None, "{id}", has_doc, select_all_on), cmd!("node.move", "Reorder", [], None, "{id, parent?: id (null = top level), index} drag-reorder in the Layers panel", has_doc, move_node), cmd!( "artboard.new", "New Artboard", ["Window", "Artboards"], None, "{x?, y?, width?, height?, name?} (default: right of the last)", has_doc, artboard_new ), cmd!("artboard.delete", "Delete Artboard", ["Window", "Artboards"], None, "{index}", has_doc, artboard_delete), cmd!( "artboard.setProps", "Artboard Options…", ["Window", "Artboards"], None, "{index, name?, x?, y?, width?, height?}", has_doc, artboard_set ), cmd!("artboard.fitToArt", "Fit to Artwork Bounds", ["Object", "Artboards"], None, "{index?}", has_doc, artboard_fit_art), cmd!("artboard.fitToSelection", "Fit to Selected Art", ["Object", "Artboards"], None, "{index?}", has_selection, artboard_fit_sel), ] } fn new_layer(s: &mut Session, p: &Value) -> Result { let name = str_param(p, "name").map(str::to_string); let st = s.doc()?; let cur = st.active_layer.and_then(|l| st.doc.position(l)).filter(|(par, _, _)| par.is_none()).map(|(_, i, _)| i); let id = s.edit("New Layer", |d, _| { let id = d.add_layer(name.as_deref()); if let Some(i) = cur { d.move_node(id, None, i + 1)?; } Ok(id) })?; s.doc_mut()?.active_layer = Some(id); Ok(json!({ "id": id.0 })) } fn new_sublayer(s: &mut Session, p: &Value) -> Result { let parent = id_param(p, "parent").or(s.doc()?.current_layer()).ok_or_else(|| bad("layer.newSublayer", "no parent layer"))?; let name = str_param(p, "name").map(str::to_string); let id = s.edit("New Sublayer", |d, _| { let n = d.node(parent).and_then(|n| n.children()).map(|c| c.iter().filter(|c| c.is_layer()).count()).unwrap_or(0); let id = d.alloc_id(); let color = LayerColor::Preset(((d.layers.len() + n + 1) % 27) as u8); let nm = name.unwrap_or_else(|| format!("Layer {}", d.node_count())); d.insert(Some(parent), usize::MAX, vectorcraft_doc::Node::layer(id, &nm, color))?; Ok(id) })?; Ok(json!({ "id": id.0 })) } fn delete_layer(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").or(s.doc()?.current_layer()).ok_or_else(|| bad("layer.delete", "no layer"))?; let st = s.doc()?; if st.doc.layers.len() == 1 && st.doc.layers[0].id == id { return Err(EngineError::Other("a document needs at least one layer".into())); } s.edit("Delete Layer", |d, sel| { d.remove(id)?; sel.prune(d); Ok(()) })?; let st = s.doc_mut()?; if st.active_layer == Some(id) || st.active_layer.is_some_and(|l| st.doc.node(l).is_none()) { st.active_layer = st.doc.default_layer(); } ok() } fn duplicate_layer(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").or(s.doc()?.current_layer()).ok_or_else(|| bad("layer.duplicate", "no layer"))?; let nid = s.edit("Duplicate Layer", |d, _| { let (par, idx, _) = d.position(id).ok_or(EngineError::NoNode(id))?; let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?; let mut c = d.reid(&n); c.name = Some(format!("{} copy", n.display_name())); Ok(d.insert(par, idx + 1, c)?) })?; Ok(json!({ "id": nid.0 })) } fn set_current(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").ok_or_else(|| bad("layer.setCurrent", "missing id"))?; let st = s.doc_mut()?; if !st.doc.node(id).is_some_and(|n| n.is_layer()) { return Err(bad("layer.setCurrent", "not a layer")); } st.active_layer = Some(id); st.revision += 1; ok() } fn set_props(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").ok_or_else(|| bad("layer.setProps", "missing id"))?; s.edit("Layer Options", |d, sel| { let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?; if let Some(v) = str_param(p, "name") { n.name = Some(v.to_string()); } if let Some(v) = p.get("visible").and_then(Value::as_bool) { n.visible = v; } if let Some(v) = p.get("locked").and_then(Value::as_bool) { n.locked = v; } if let NodeKind::Layer { template, printable, color, .. } = &mut n.kind { if let Some(v) = p.get("template").and_then(Value::as_bool) { *template = v; } if let Some(v) = p.get("printable").and_then(Value::as_bool) { *printable = v; } if let Some(v) = p.get("color").and_then(Value::as_u64) { *color = LayerColor::Preset((v % 27) as u8); } } // Hidden or locked content can't stay selected. let hidden: Vec = sel.objects.iter().copied().filter(|o| !d.is_editable(*o)).collect(); for h in hidden { sel.remove(h); } Ok(()) })?; ok() } fn collect(s: &mut Session, _: &Value) -> Result { let ids = super::edit::selected_roots(s)?; let lid = s.edit("Collect in New Layer", |d, _| { let lid = d.add_layer(None); for id in &ids { d.move_node(*id, Some(lid), usize::MAX)?; } Ok(lid) })?; Ok(json!({ "id": lid.0 })) } fn select_all_on(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").ok_or_else(|| bad("layer.selectAll", "missing id"))?; let ids: Vec = s .doc()? .doc .node(id) .and_then(|n| n.children()) .map(|c| c.iter().filter(|n| n.visible && !n.locked).map(|n| n.id).collect()) .unwrap_or_default(); s.select(|_, sel| sel.set(ids.iter().copied()))?; ok() } fn move_node(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").ok_or_else(|| bad("node.move", "missing id"))?; let parent = id_param(p, "parent"); let index = p.get("index").and_then(Value::as_u64).unwrap_or(u64::MAX) as usize; if parent.is_none() && !s.doc()?.doc.node(id).is_some_and(|n| n.is_layer()) { return Err(bad("node.move", "only layers can be at the top level")); } s.edit("Reorder", |d, _| Ok(d.move_node(id, parent, index)?))?; ok() } fn rect_from(p: &Value, default: Rect) -> Rect { let x = f64_or(p, "x", default.x0); let y = f64_or(p, "y", default.y0); let w = f64_or(p, "width", default.width()).max(1.0); let h = f64_or(p, "height", default.height()).max(1.0); Rect::new(x, y, x + w, y + h) } fn artboard_new(s: &mut Session, p: &Value) -> Result { let d0 = &s.doc()?.doc; let last = d0.artboards.last().map(|a| a.rect).unwrap_or(Rect::new(0.0, 0.0, 612.0, 792.0)); let def = Rect::new(last.x1 + 20.0, last.y0, last.x1 + 20.0 + last.width(), last.y1); let r = rect_from(p, def); let idx = s.edit("New Artboard", |d, _| { let id = d.next_artboard_id(); let name = str_param(p, "name").map(str::to_string).unwrap_or_else(|| format!("Artboard {}", d.artboards.len() + 1)); d.artboards.push(Artboard { id, name, rect: r, show_center_mark: false, show_cross_hairs: false }); Ok(d.artboards.len() - 1) })?; Ok(json!({ "index": idx })) } fn artboard_delete(s: &mut Session, p: &Value) -> Result { let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("artboard.delete", "missing index"))? as usize; s.edit("Delete Artboard", |d, _| { if d.artboards.len() <= 1 { return Err(EngineError::Other("a document needs at least one artboard".into())); } if i >= d.artboards.len() { return Err(EngineError::Other("no such artboard".into())); } d.artboards.remove(i); Ok(()) })?; ok() } fn artboard_set(s: &mut Session, p: &Value) -> Result { let i = p.get("index").and_then(Value::as_u64).unwrap_or(0) as usize; s.edit("Artboard Options", |d, _| { let a = d.artboards.get_mut(i).ok_or_else(|| EngineError::Other("no such artboard".into()))?; a.rect = rect_from(p, a.rect); if let Some(n) = str_param(p, "name") { a.name = n.to_string(); } Ok(()) })?; ok() } fn artboard_fit_art(s: &mut Session, p: &Value) -> Result { let i = p.get("index").and_then(Value::as_u64).unwrap_or(0) as usize; let b = s.doc()?.doc.art_bounds().ok_or_else(|| EngineError::Other("no artwork".into()))?; s.execute("artboard.setProps", &json!({"index": i, "x": b.x0, "y": b.y0, "width": b.width(), "height": b.height()})) } fn artboard_fit_sel(s: &mut Session, p: &Value) -> Result { let i = p.get("index").and_then(Value::as_u64).unwrap_or(0) as usize; let st = s.doc()?; let b = st.doc.bounds_of(&st.selection.objects, true).ok_or_else(|| EngineError::Other("no selection bounds".into()))?; s.execute("artboard.setProps", &json!({"index": i, "x": b.x0, "y": b.y0, "width": b.width(), "height": b.height()})) }