Files
figcraft/crates/engine/src/cmd/component/instance.rs
T

410 lines
18 KiB
Rust

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<Value> {
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<Value> {
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<Value> {
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::<String>().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<Vec<NodeId>> {
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<Value> {
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<Value> {
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<NodeId> {
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<Value> {
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<Value> {
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<Value> {
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<String> = 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<String, Value>) -> BTreeMap<String, PropDef> {
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<NodeId, NodeId> {
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
let mut used: BTreeSet<NodeId> = 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<Value> {
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<Value> {
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<Value> {
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}))
}