vectorcraft/crates/engine/src/cmd/layer.rs

321 lines
13 KiB
Rust

//! 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<CommandSpec> {
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),
cmd!(
"layer.clippingMask.toggle",
"Make/Release Clipping Mask",
["Window", "Layers"],
None,
"{id?: layer or group (default: the one selected group, else the current layer)} make: its top object (a path, compound path or text, which loses its paint) clips the rest and moves to the bottom (new art added on top is clipped); release: it stops clipping, the clipping path stays unpainted → {clip}",
has_doc,
clip_toggle
),
cmd!(
"layer.target",
"Target",
[],
None,
"{id} target a layer, group or object as clicking its target circle in the Layers panel does: a layer gets its visible, unlocked art selected and becomes the current layer, and appearance.*, effect.*, transparency.* and the opacity-mask commands without `ids` then act on the layer itself; anything else is selected. Any other selection change ends the targeting (`document.inspect` → target) → {id, selected: [..]}",
has_doc,
target
),
]
}
/// The Layers panel's target circle.
fn target(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "layer.target";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
let st = s.doc()?;
let n = st.doc.node(id).ok_or(EngineError::NoNode(id))?;
if !st.doc.is_editable(id) {
return Err(bad(C, "the object is hidden or locked"));
}
let layer = n.is_layer();
s.select(|d, sel| sel.set_target(d, id))?;
let st = s.doc_mut()?;
if layer {
st.active_layer = Some(id);
}
Ok(json!({ "id": id.0, "selected": st.selection.objects.iter().map(|i| i.0).collect::<Vec<_>>() }))
}
/// The Layers panel's clipping mask button: the top object of a layer (or group) clips the rest,
/// or the clipping mask is released.
fn clip_toggle(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let group = match st.selection.objects.as_slice() {
[id] if matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::Group { .. })) => Some(*id),
_ => None,
};
let id = id_param(p, "id").or(group).or(st.current_layer()).ok_or_else(|| bad("layer.clippingMask.toggle", "no layer"))?;
let clip = s.edit("Make/Release Clipping Mask", |d, _| {
let n = d.node(id).ok_or(EngineError::NoNode(id))?;
if !matches!(n.kind, NodeKind::Layer { .. } | NodeKind::Group { .. }) {
return Err(bad("layer.clippingMask.toggle", "not a layer or group"));
}
if n.clips() {
super::object::release_clip(d, id);
return Ok(false);
}
let top =
n.children().and_then(|c| c.last()).map(|c| c.id).ok_or_else(|| EngineError::Other("the layer has no objects to clip by".into()))?;
super::object::make_clipping_path(d, top)?;
d.move_node(top, Some(id), 0)?;
if let Some(n) = d.node_mut(id) {
n.set_clips(true);
}
Ok(true)
})?;
Ok(json!({ "clip": clip }))
}
fn new_layer(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<NodeId> = 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<Value> {
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<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("layer.selectAll", "missing id"))?;
let ids: Vec<NodeId> = 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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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()}))
}