use super::props::{Owner, add_prop_for, delete_prop, prop_defs}; use super::{ BTreeMap, BTreeSet, Document, EngineError, NodeId, NodeKind, OVERRIDE_GROUPS, PROP_BINDING, PropDef, Result, SLOT, Session, Value, bad, boolean, copy_group, create_instance_at, default_props, get, id_of, inside_instance, is_slot, json, keep_text_overrides, main_of, ok, owning_component, owning_instance, rebase_instance, req_id, req_num, req_targets, root_instance, slot_edited, slot_name, slots_of, string, targets, unlink_layers, variant_options, }; pub(super) fn detach(s: &mut Session, p: &Value) -> Result { let cmd = "component.detach"; let ids = req_targets(cmd, s, p)?; let doc = s.doc_mut()?; let mut out = Vec::new(); for i in ids { if !matches!(doc.get(i)?.kind, NodeKind::Instance { .. }) { continue; } if inside_instance(doc, i) { return Err(EngineError::Disabled(cmd.into(), "a nested instance can't be detached on its own: detach the outer instance first".into())); } // Own layers lose their link (slot content stays as it is); nested instances stay // instances of their own mains. let nested = unlink_layers(doc, i, Some(i)); keep_main_modes(doc, i)?; let n = doc.get_mut(i)?; n.kind = NodeKind::Frame; n.source = None; n.overrides.clear(); n.prop_refs.remove("mainComponent"); // No properties left to drive. n.bound.retain(|k, _| !k.starts_with(PROP_BINDING)); for nn in nested { rebase_instance(doc, nn); } out.push(i); } if out.is_empty() { return Err(EngineError::Disabled(cmd.into(), "select an instance".into())); } s.set_selection(out.clone()); Ok(json!({"ids": out})) } /// Make the variable modes instance `i` takes from its main its own (it's being detached). fn keep_main_modes(doc: &mut Document, i: NodeId) -> Result<()> { let modes = match doc.get(i)?.kind { NodeKind::Instance { component, .. } => doc.node(component).map(|m| m.var_modes.clone()).unwrap_or_default(), _ => return Ok(()), }; let own = &mut doc.get_mut(i)?.var_modes; for (c, m) in modes { own.entry(c).or_insert(m); } Ok(()) } pub(super) fn reset_instance(s: &mut Session, p: &Value) -> Result { let cmd = "component.resetInstance"; let ids = req_targets(cmd, s, p)?; let doc = s.doc_mut()?; let mut done = Vec::new(); for i in ids { let Some(inst) = owning_instance(doc, i) else { continue }; let top = !inside_instance(doc, inst); for d in doc.subtree(inst) { let keep: Vec<&str> = if d == inst && top { vec!["size"] } else { vec![] }; if let Some(n) = doc.node(d) && n.overrides.iter().any(|g| !keep.contains(&g.as_str())) && let Some(n) = doc.node_mut(d) { n.overrides.retain(|g| keep.contains(&g.as_str())); } } // Property values go back to the defaults. if top && let Some(NodeKind::Instance { component, props }) = doc.node(inst).map(|n| n.kind.clone()) && let Some(defaults) = doc.node(component).map(default_props) && props != defaults && let NodeKind::Instance { props, .. } = &mut doc.get_mut(inst)?.kind { *props = defaults; } // ...and stop following variables. if top && doc.node(inst).is_some_and(|n| n.bound.keys().any(|k| k.starts_with(PROP_BINDING))) { doc.get_mut(inst)?.bound.retain(|k, _| !k.starts_with(PROP_BINDING)); } done.push(inst); } if done.is_empty() { return Err(EngineError::Disabled(cmd.into(), "select an instance".into())); } Ok(json!({"ids": done})) } pub(super) fn reset_override(s: &mut Session, p: &Value) -> Result { let cmd = "component.resetOverride"; let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a layer inside an instance"))?; let group = string(p, "group"); if let Some(g) = &group && !OVERRIDE_GROUPS.contains(&g.as_str()) { return Err(bad(cmd, format!("unknown group `{g}` (known: {})", OVERRIDE_GROUPS.join(", ")))); } let doc = s.doc_mut()?; if owning_instance(doc, i).is_none() || doc.get(i)?.source.is_none() { return Err(bad(cmd, "not a layer of an instance")); } let top_root = matches!(doc.get(i)?.kind, NodeKind::Instance { .. }) && !inside_instance(doc, i); let had = doc.get(i)?.overrides.clone(); let n = doc.get_mut(i)?; match &group { Some(g) => { n.overrides.remove(g); } None => n.overrides.clear(), } let mut changed = had != doc.get(i)?.overrides; // The instance's own property values reset with `props`. if top_root && group.as_deref().is_none_or(|g| g == "props") && let Some(NodeKind::Instance { component, props }) = doc.node(i).map(|n| n.kind.clone()) && let Some(defaults) = doc.node(component).map(default_props) && props != defaults && let NodeKind::Instance { props, .. } = &mut doc.get_mut(i)?.kind { *props = defaults; changed = true; } Ok(json!({"changed": changed})) } /// A property name for `owner` based on `base` (a layer name): valid and not taken yet. fn fresh_prop_name(doc: &Document, owner: &Owner, base: &str) -> String { let base: String = base.chars().filter(|c| !matches!(c, ',' | '=')).take(100).collect::().trim().to_string(); let base = if base.is_empty() { "Slot".to_string() } else { base }; let taken = |n: &str| { owner.components(doc).iter().any(|c| prop_defs(doc, *c).contains_key(n)) || matches!(owner, Owner::Set(s) if variant_options(doc, *s).iter().any(|(k, _)| k == n)) }; (1..10_000).map(|k| if k == 1 { base.clone() } else { format!("{base} {k}") }).find(|n| !taken(n)).unwrap_or(base) } /// The `preferred` components of a slot property: component or component set ids. pub(super) fn preferred_arg(cmd: &str, doc: &Document, p: &Value) -> Result> { let Some(v) = get(p, "preferred").filter(|v| !v.is_null()) else { return Ok(Vec::new()) }; let list = v.as_array().ok_or_else(|| bad(cmd, "`preferred`: an array of component ids"))?; let mut out = Vec::new(); for x in list.iter().take(500) { let c = id_of(x) .filter(|c| doc.node(*c).is_some_and(|n| matches!(n.kind, NodeKind::Component { .. } | NodeKind::ComponentSet { .. }))) .ok_or_else(|| bad(cmd, format!("`preferred`: {x} is not a component or component set")))?; if !out.contains(&c) { out.push(c); } } Ok(out) } pub(super) fn make_slot(s: &mut Session, p: &Value) -> Result { let cmd = "component.makeSlot"; let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a frame inside a main component"))?; let doc = s.doc()?; let n = doc.get(id)?; let inside_main = owning_component(doc, id).is_some_and(|c| c != id) && owning_instance(doc, id).is_none(); if !matches!(n.kind, NodeKind::Frame) || !inside_main { return Err(bad(cmd, "only a frame inside a main component (not inside an instance) can become a slot")); } if is_slot(doc, id) { return Err(EngineError::Disabled(cmd.into(), format!("\"{}\" is already a slot", n.name))); } let owner = Owner::resolve(doc, id).ok_or_else(|| bad(cmd, "the frame isn't inside a component"))?; let name = string(p, "name").unwrap_or_else(|| fresh_prop_name(doc, &owner, &n.name)); let preferred = get(p, "preferred").cloned().unwrap_or(Value::Null); add_prop_for(cmd, s, &json!({"id": id, "layer": id, "name": name, "type": "slot", "preferred": preferred})) } pub(super) fn remove_slot(s: &mut Session, p: &Value) -> Result { let cmd = "component.removeSlot"; let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a slot frame of a main component"))?; let doc = s.doc()?; if owning_instance(doc, id).is_some() { return Err(EngineError::Disabled(cmd.into(), "slots are removed on the main component".into())); } let name = slot_name(doc, id).ok_or_else(|| bad(cmd, "not a slot frame of a main component"))?; delete_prop(s, &json!({"id": id, "name": name}))?; Ok(json!({"id": id, "name": name})) } /// The slot inside an instance a command acts on: `id` itself, or instance `id`'s slot `name`. fn instance_slot(cmd: &str, doc: &Document, id: NodeId, name: Option<&str>) -> Result { let slot = match name { Some(nm) => slots_of(doc, id).into_iter().find(|(_, n)| n == nm).map(|(l, _)| l).ok_or_else(|| bad(cmd, format!("no slot named `{nm}`")))?, None => id, }; if !is_slot(doc, slot) || owning_instance(doc, slot).is_none() { return Err(bad(cmd, "not a slot of an instance (a main component's slot takes layers like any frame)")); } Ok(slot) } pub(super) fn slot_insert(s: &mut Session, p: &Value) -> Result { let cmd = "instance.slotInsert"; let c = req_id(cmd, p, "component")?; let target = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a slot inside an instance"))?; let index = match get(p, "index").filter(|v| !v.is_null()) { None => None, Some(v) => Some(v.as_u64().and_then(|i| usize::try_from(i).ok()).ok_or_else(|| bad(cmd, "`index`: a child index (0 = bottom)"))?), }; let doc = s.doc_mut()?; let slot = instance_slot(cmd, doc, target, string(p, "slot").as_deref())?; let c = main_of(doc, c).ok_or_else(|| bad(cmd, "`component` must be a component or component set"))?; if boolean(p, "replace").unwrap_or(false) { for k in doc.get(slot)?.children.clone() { doc.remove(k)?; } } let i = create_instance_at(doc, c, slot, 0.0, 0.0, index)?; s.set_selection(vec![i]); Ok(json!({"id": i, "slot": slot})) } pub(super) fn reset_slot(s: &mut Session, p: &Value) -> Result { let cmd = "instance.resetSlot"; let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a slot inside an instance"))?; let doc = s.doc_mut()?; let slot = instance_slot(cmd, doc, id, string(p, "slot").as_deref())?; let changed = slot_edited(doc, slot); if changed { doc.get_mut(slot)?.overrides.remove(SLOT); } Ok(json!({"id": slot, "changed": changed})) } pub(super) fn push_changes(s: &mut Session, p: &Value) -> Result { let cmd = "component.pushChanges"; let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance"))?; let doc = s.doc_mut()?; // Changes go to the outer instance's main (never into a nested instance's own main). let inst = root_instance(doc, i).ok_or_else(|| bad(cmd, "not an instance"))?; let Some(NodeKind::Instance { component, .. }) = doc.node(inst).map(|n| n.kind.clone()) else { return ok() }; if !doc.contains(component) { return Err(EngineError::Disabled(cmd.into(), "the main component was deleted (use component.restoreMain)".into())); } // Copy every overridden layer's values to its source in the main (geometry, resizing, swaps, // property values and slot content stay with the instance). let mut pushed = 0; for d in doc.subtree(inst) { let Some(n) = doc.node(d).cloned() else { continue }; let Some(src) = (if d == inst { Some(component) } else { n.source }) else { continue }; if n.overrides.is_empty() || !doc.contains(src) || !doc.is_ancestor(component, src) { continue; } let Some(mut target) = doc.node(src).cloned() else { continue }; let groups: BTreeSet = n .overrides .iter() .filter(|g| !(matches!(g.as_str(), "size" | "resizing" | "position" | "component" | "props" | SLOT) || d == inst && g.as_str() == "name")) .cloned() .collect(); for g in &groups { copy_group(&n, &mut target, g); } if groups.is_empty() { continue; } pushed += 1; if let Some(slot) = doc.node_mut(src) { *slot = target; } if let Some(m) = doc.node_mut(d) { m.overrides.retain(|g| !groups.contains(g)); } } Ok(json!({"layers": pushed})) } fn restore_prop_defs(doc: &Document, props: &BTreeMap) -> BTreeMap { let mut defs = BTreeMap::new(); for (k, v) in props { let def = match v { Value::Bool(b) => PropDef::Boolean { default: *b }, Value::String(t) => PropDef::Text { default: t.clone() }, other => match id_of(other) { Some(c) if doc.contains(c) => PropDef::InstanceSwap { default: c, preferred: Vec::new() }, _ => continue, }, }; defs.insert(k.clone(), def); } defs } fn restore_id_map(doc: &mut Document, i: NodeId, component: NodeId, sub: &[NodeId]) -> BTreeMap { let mut map: BTreeMap = BTreeMap::new(); let mut used: BTreeSet = BTreeSet::new(); map.insert(i, component); used.insert(component); for d in sub.iter().filter(|d| **d != i) { let src = doc.node(*d).and_then(|n| n.source); let nid = match src { Some(s) if !doc.contains(s) && used.insert(s) => s, _ => doc.alloc_id(), }; map.insert(*d, nid); } map } pub(super) fn restore_main(s: &mut Session, p: &Value) -> Result { let cmd = "component.restoreMain"; let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance"))?; let doc = s.doc_mut()?; let Some(NodeKind::Instance { component, props }) = doc.node(i).map(|n| n.kind.clone()) else { return Err(bad(cmd, "select an instance")); }; if doc.contains(component) { return Err(EngineError::Disabled(cmd.into(), "the main component still exists".into())); } if inside_instance(doc, i) { return Err(EngineError::Disabled(cmd.into(), "select the outer instance".into())); } let parent = doc.get(i)?.parent.ok_or_else(|| bad(cmd, "no parent"))?; let index = doc.index_in_parent(i); // The main comes back with its old id (other instances point to it) and its layers with the // ids their copies mirror, so every instance re-links by source. let sub = doc.subtree(i); let map = restore_id_map(doc, i, component, &sub); let mut nodes = Vec::new(); let mut nested = Vec::new(); for d in &sub { let Some(mut n) = doc.node(*d).cloned() else { continue }; if *d == i { let defs = restore_prop_defs(doc, &props); n.kind = NodeKind::Component { description: String::new(), props: defs }; n.source = None; n.overrides.clear(); n.prop_refs.clear(); } else { let parent_inst = n.parent.and_then(|p| owning_instance(doc, p)); if parent_inst != Some(i) { nodes.push(n); continue; } if matches!(n.kind, NodeKind::Instance { .. }) { nested.push(*d); } else { n.source = None; n.overrides.clear(); } } nodes.push(n); } doc.remove(i)?; doc.paste_nodes(&nodes, &map, i, parent, index)?; for nn in nested { if let Some(m) = map.get(&nn).copied() { rebase_instance(doc, m); } } s.set_selection(vec![component]); Ok(json!({"id": component})) } pub(super) fn swap(s: &mut Session, p: &Value) -> Result { let cmd = "instance.swap"; let c = req_id(cmd, p, "component")?; let ids = req_targets(cmd, s, p)?; let doc = s.doc_mut()?; let c = main_of(doc, c).ok_or_else(|| bad(cmd, "`component` must be a component or component set"))?; for i in ids { swap_instance(doc, i, c).map_err(|m| bad(cmd, m))?; } ok() } /// Make instance `i` show main component `c`, keeping the values of properties with the same names /// (the layers' overrides follow by name when the instance is rebuilt). pub(crate) fn swap_instance(doc: &mut Document, i: NodeId, c: NodeId) -> std::result::Result<(), String> { if doc.is_ancestor(c, i) { return Err("an instance can't show the component it is inside".into()); } let defaults = doc.node(c).map(default_props).unwrap_or_default(); match doc.get_mut(i).map(|n| &mut n.kind) { Ok(NodeKind::Instance { component, props }) => { *component = c; let old = std::mem::take(props); *props = defaults.iter().map(|(k, d)| (k.clone(), old.get(k).cloned().unwrap_or_else(|| d.clone()))).collect(); Ok(()) } _ => Err(format!("{i} is not an instance")), } } /// Swap the instance under a page point to a component (`instance.swapAt`). pub(crate) fn swap_at(s: &mut Session, p: &Value) -> Result { let cmd = "instance.swapAt"; let c = req_id(cmd, p, "component")?; let pt = figcraft_geom::Point::new(req_num(cmd, p, "x")?, req_num(cmd, p, "y")?); let page = s.page()?; let doc = s.doc()?; let c = main_of(doc, c).ok_or_else(|| bad(cmd, "`component` must be a component or component set"))?; let family = crate::drop::swap_family(doc, c); let target = crate::drop::swap_target(doc, page, pt, family.as_deref(), Some(c)) .ok_or_else(|| EngineError::Disabled(cmd.into(), "there is no instance at that point to swap".into()))?; keep_text_overrides(s.doc_mut()?, target); swap(s, &json!({"ids": [target], "component": c}))?; s.set_selection(vec![target]); Ok(json!({"id": target, "component": c})) }