From 0bc49705bdd0ba17f4e60bdf5c3dc731ceb5cd4e Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 19:20:45 +0000 Subject: [PATCH] refactor(engine): split assets/components.rs into components/ --- crates/engine/src/assets/components.rs | 2078 +---------------- crates/engine/src/assets/components/diff.rs | 218 ++ crates/engine/src/assets/components/guard.rs | 135 ++ .../engine/src/assets/components/instances.rs | 342 +++ crates/engine/src/assets/components/patch.rs | 528 +++++ crates/engine/src/assets/components/rebase.rs | 249 ++ crates/engine/src/assets/components/sync.rs | 459 ++++ .../engine/src/assets/components/variants.rs | 147 ++ 8 files changed, 2099 insertions(+), 2057 deletions(-) create mode 100644 crates/engine/src/assets/components/diff.rs create mode 100644 crates/engine/src/assets/components/guard.rs create mode 100644 crates/engine/src/assets/components/instances.rs create mode 100644 crates/engine/src/assets/components/patch.rs create mode 100644 crates/engine/src/assets/components/rebase.rs create mode 100644 crates/engine/src/assets/components/sync.rs create mode 100644 crates/engine/src/assets/components/variants.rs diff --git a/crates/engine/src/assets/components.rs b/crates/engine/src/assets/components.rs index c85c6b8b..07389419 100644 --- a/crates/engine/src/assets/components.rs +++ b/crates/engine/src/assets/components.rs @@ -37,6 +37,27 @@ use figcraft_doc::*; use figcraft_geom::Affine; use serde_json::Value; +mod diff; +mod guard; +mod instances; +mod patch; +mod rebase; +mod sync; +mod variants; + +pub use diff::{diff_groups, property_owners, set_boolean_default, variable_groups}; +pub use guard::{check_override_edit, check_structure_edit, refused_override, unlink_layers}; +pub(crate) use instances::copy_group; +pub use instances::{PROP_BINDING, copy_in_file, create_instance, create_instance_at, sync_instance}; +pub use patch::sync_after_edit; +#[cfg(test)] +pub(crate) use patch::{FULL_REBUILD, PATCHED}; +#[cfg(test)] +use rebase::RebaseSources; +pub use rebase::rebase_instance; +pub use sync::keep_text_overrides; +pub use variants::{default_variant, exposed_copies, find_variant, mirror_issues, variant_conflicts, variant_name, variant_options, variant_props}; + /// Nesting limit for instances inside instances. const MAX_DEPTH: usize = 16; /// The override group (and `prop_refs` field) of slots. @@ -273,2067 +294,10 @@ pub fn prop_default_value(d: &PropDef) -> Value { } } -/// Create an instance of `component` under `parent` at `(x, y)` (parent space). -pub fn create_instance(doc: &mut Document, component: NodeId, parent: NodeId, x: f64, y: f64) -> Result { - create_instance_at(doc, component, parent, x, y, None) -} - -/// [`create_instance`] at child `index` of `parent` (None: on top). -pub fn create_instance_at(doc: &mut Document, component: NodeId, parent: NodeId, x: f64, y: f64, index: Option) -> Result { - let comp = doc.get(component)?.clone(); - if !matches!(comp.kind, NodeKind::Component { .. }) { - return Err(DocError::Invalid(format!("{component} is not a component"))); - } - if doc.is_ancestor(component, parent) { - return Err(DocError::Invalid("an instance can't be placed inside its own main component".into())); - } - let id = doc.alloc_id(); - let mut root = comp.clone(); - root.id = id; - root.name = default_instance_name(doc, component); - root.kind = NodeKind::Instance { component, props: default_props(&comp) }; - root.children.clear(); - root.parent = None; - root.source = Some(component); - root.overrides.clear(); - root.transform = Affine::translate((x, y)); - root.prop_refs.clear(); - root.annotations.clear(); - root.code_links.clear(); - // The main's dev resources show on its instances without being copied. - root.dev_resources.clear(); - // Measurements belong to the layer they were placed on. - root.measurements.clear(); - // The main's interactions play on its instances in prototypes; the instance's own start empty. - root.interactions.clear(); - // So do the variable modes the main sets (see `Document::inherited_modes`). - root.var_modes.clear(); - doc.insert(root, parent, index)?; - sync_instance(doc, id); - Ok(id) -} - -/// Copy `id` within its own file under `parent` (at `index`), keeping its local transform. A file -/// has one main per component, so a main component's copy is an instance of it; a variant copied -/// into its set stays a new variant, and every other layer is deep-copied. -pub fn copy_in_file(doc: &mut Document, id: NodeId, parent: NodeId, index: Option) -> Result { - let n = doc.get(id)?; - let in_set = doc.node(parent).is_some_and(|p| matches!(p.kind, NodeKind::ComponentSet { .. })); - if !matches!(n.kind, NodeKind::Component { .. }) || in_set { - return doc.duplicate_into(id, parent, index); - } - let transform = n.transform; - let instance = create_instance_at(doc, id, parent, 0.0, 0.0, index)?; - doc.get_mut(instance)?.transform = transform; - Ok(instance) -} - -/// Which part of the carried overrides to apply. -#[derive(Clone, Copy, PartialEq)] -enum Carry { - /// Paint, text, visibility, … (everything but geometry). - Values, - /// Values plus size (instance roots: their size decides how children follow constraints). - Root, - /// Size and position (children, after constraints ran). - Geometry, -} - -/// Copy the overridden property groups of `from` onto `to`. -fn carry_overrides(from: &Node, to: &mut Node, what: Carry) { - for g in &from.overrides { - let geo = matches!(g.as_str(), "size" | "position"); - match what { - Carry::Values if geo => continue, - Carry::Root if g == "position" => continue, - Carry::Geometry if !geo => continue, - _ => {} - } - copy_group(from, to, g); - } - if what != Carry::Geometry { - to.overrides = from.overrides.clone(); - } -} - -/// An instance switched from variant `was` to `new`: its resizing (sizing and min / max per axis) -/// that is still `was`'s follows `new`; what the instance set itself stays in `own`. -fn follow_variant_resizing(own: &mut ChildLayout, was: &ChildLayout, new: &ChildLayout) { - if own.sizing_h == was.sizing_h { - own.sizing_h = new.sizing_h; - } - if own.sizing_v == was.sizing_v { - own.sizing_v = new.sizing_v; - } - for (o, w, n) in [ - (&mut own.min_w, was.min_w, new.min_w), - (&mut own.max_w, was.max_w, new.max_w), - (&mut own.min_h, was.min_h, new.min_h), - (&mut own.max_h, was.max_h, new.max_h), - ] { - if *o == w { - *o = n; - } - } -} - -/// Copy the values of property group `g` of `from` onto `to`, with the variables bound to them. -pub(crate) fn copy_group(from: &Node, to: &mut Node, g: &str) { - if from.bound.iter().chain(to.bound.iter()).any(|(k, _)| binding_group(k) == Some(g)) { - to.bound.retain(|k, _| binding_group(k) != Some(g)); - to.bound.extend(bindings_of(from, g).map(|(k, v)| (k.clone(), v.clone()))); - } - match g { - "fills" => { - to.fills = from.fills.clone(); - to.styles.fill = from.styles.fill.clone(); - } - "strokes" => { - to.strokes = from.strokes.clone(); - to.stroke = from.stroke.clone(); - to.styles.stroke = from.styles.stroke.clone(); - } - "effects" => { - to.effects = from.effects.clone(); - to.styles.effect = from.styles.effect.clone(); - } - "opacity" => { - to.opacity = from.opacity; - to.blend = from.blend; - } - "visible" => to.visible = from.visible, - "name" => to.name = from.name.clone(), - "corners" => to.corners = from.corners, - "characters" => { - if let (NodeKind::Text(a), NodeKind::Text(b)) = (&from.kind, &mut to.kind) { - b.characters = a.characters.clone(); - b.runs = a.runs.clone(); - b.paragraphs = a.paragraphs.clone(); - } - } - "textStyle" => { - if let (NodeKind::Text(a), NodeKind::Text(b)) = (&from.kind, &mut to.kind) { - b.style = a.style.clone(); - b.runs = a.runs.clone(); - b.auto_resize = a.auto_resize; - b.align_h = a.align_h; - b.align_v = a.align_v; - b.paragraph_spacing = a.paragraph_spacing; - b.paragraph_indent = a.paragraph_indent; - b.list = a.list; - b.paragraphs = a.paragraphs.clone(); - b.list_spacing = a.list_spacing; - b.hanging_punctuation = a.hanging_punctuation; - b.hanging_list = a.hanging_list; - b.max_lines = a.max_lines; - b.features = a.features.clone(); - } - to.styles.text = from.styles.text.clone(); - } - "padding" | "gap" | "alignment" => { - if let (Some(a), Some(b)) = (&from.layout, &mut to.layout) { - match g { - "padding" => b.padding = a.padding, - "gap" => (b.gap, b.cross_gap) = (a.gap, a.cross_gap), - _ => (b.main_align, b.cross_align, b.content_align) = (a.main_align, a.cross_align, a.content_align), - } - } - } - "grids" => { - to.grids = from.grids.clone(); - to.styles.grid = from.styles.grid.clone(); - } - "exports" => to.exports = from.exports.clone(), - "interactions" => to.interactions = from.interactions.clone(), - "size" => { - to.width = from.width; - to.height = from.height; - if let (NodeKind::Text(a), NodeKind::Text(b)) = (&from.kind, &mut to.kind) { - b.auto_resize = a.auto_resize; - } - } - "resizing" => { - let (a, b) = (&from.child_layout, &mut to.child_layout); - (b.sizing_h, b.sizing_v, b.min_w, b.max_w, b.min_h, b.max_h) = (a.sizing_h, a.sizing_v, a.min_w, a.max_w, a.min_h, a.max_h); - } - "position" => to.transform = from.transform, - "modes" => to.var_modes = from.var_modes.clone(), - "component" => { - if let (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) = (&from.kind, &mut to.kind) { - *b = *a; - } - } - "props" => { - if let (NodeKind::Instance { props: a, .. }, NodeKind::Instance { props: b, .. }) = (&from.kind, &mut to.kind) { - *b = a.clone(); - } - carry_prop_bindings(from, to); - } - _ => {} - } -} - -/// Layer name paths below `root` ("Frame/Label", with `#n` for same-named siblings). With -/// `src_names` (an instance's layers), layers are named after the main layer they mirror (renames -/// don't break matching), and the content of slots holding the instance's own is left out. -fn path_map(doc: &Document, root: NodeId, src_names: bool) -> BTreeMap { - let mut out = BTreeMap::new(); - let mut stack = vec![(root, String::new())]; - while let Some((id, prefix)) = stack.pop() { - let Some(n) = doc.node(id) else { continue }; - let mut seen: BTreeMap<&str, usize> = BTreeMap::new(); - for c in &n.children { - let Some(cn) = doc.node(*c) else { continue }; - // A nested instance linked only to its own component so far (an imported one) mirrors no - // layer yet: it goes by its own name, like the main's layer it will be matched with. - let unplaced = matches!(&cn.kind, NodeKind::Instance { component, .. } if cn.source == Some(*component)); - let mirrored = cn.source.filter(|_| src_names && !unplaced).and_then(|s| doc.node(s)); - let name = mirrored.map_or(cn.name.as_str(), |s| s.name.as_str()); - let k = seen.entry(name).or_insert(0); - let seg = if *k == 0 { name.to_string() } else { format!("{name}#{k}") }; - *k += 1; - let path = if prefix.is_empty() { seg } else { format!("{prefix}/{seg}") }; - out.insert(*c, path.clone()); - if out.len() > 1_000_000 { - return out; - } - if !(src_names && holds_content(cn)) { - stack.push((*c, path)); - } - } - } - out -} - -/// The binding key prefix of an instance's component property value -/// (`componentProperties/`): a boolean property takes a boolean variable, a text property a -/// string variable. -pub const PROP_BINDING: &str = "componentProperties/"; - -/// Give `to` the component property bindings of instance `from` (they go with its property -/// values, which an instance keeps across rebuilds). -fn carry_prop_bindings(from: &Node, to: &mut Node) { - to.bound.retain(|k, _| !k.starts_with(PROP_BINDING)); - to.bound.extend(from.bound.iter().filter(|(k, _)| k.starts_with(PROP_BINDING)).map(|(k, v)| (k.clone(), v.clone()))); -} - -/// The override group a property-bound field belongs to. -fn field_group(field: &str) -> &'static str { - match field { - "visible" => "visible", - "characters" => "characters", - "mainComponent" => "component", - _ => "", - } -} - -/// Apply one component-property value to a layer. -fn apply_prop(doc: &Document, n: &mut Node, field: &str, val: &Value) { - match field { - "visible" => { - if let Some(b) = val.as_bool() { - n.visible = b; - } - } - "characters" => { - if let (NodeKind::Text(t), Some(sv)) = (&mut n.kind, val.as_str().map(crate::cmd::props::normalize_line_endings)) - && t.characters != sv - { - let len = t.char_count(); - t.replace_range(0, len, &sv); - } - } - "mainComponent" => { - if let (NodeKind::Instance { component: c, .. }, Some(v)) = (&mut n.kind, crate::params::id_of(val)) - && doc.node(v).is_some_and(|m| matches!(m.kind, NodeKind::Component { .. })) - { - *c = v; - } - } - _ => {} - } -} - -/// Rebuild instance `inst` from its main component, keeping ids, overrides and the content of its -/// slots. An instance whose main component was deleted keeps its layers as they are. -pub fn sync_instance(doc: &mut Document, inst: NodeId) { - // Layers the rebuild leaves as they were keep their shared snapshot (and aren't edits). - let old: Vec<(std::sync::Arc, bool)> = - doc.subtree(inst).into_iter().filter_map(|i| doc.node_arc(i).map(|a| (a.clone(), doc.dirty().contains(&i)))).collect(); - let kept = slot_content(doc, inst); - sync_instance_depth(doc, inst, 0, BTreeMap::new()); - restore_slot_content(doc, kept); - for (a, was_dirty) in old { - doc.keep_if_unchanged(a, was_dirty); - } -} - -/// The slots in instance `inst` (nested instances included) holding the instance's own content: -/// each with its content (the subtrees of its children, pre-order). -fn slot_content(doc: &Document, inst: NodeId) -> Vec<(NodeId, Vec)> { - let mut out = Vec::new(); - let mut stack: Vec = doc.node(inst).map(|n| n.children.clone()).unwrap_or_default(); - while let Some(c) = stack.pop() { - let Some(n) = doc.node(c) else { continue }; - if holds_content(n) { - let nodes = n.children.iter().flat_map(|k| doc.subtree(*k)).filter_map(|d| doc.node(d).cloned()).collect(); - out.push((c, nodes)); - } else { - stack.extend(n.children.iter().copied()); - } - } - out -} - -/// Put slot content back after a rebuild, into the slots that still hold the instance's own -/// content (a slot the main no longer has, or no longer marks as a slot, loses it). Constraints -/// and geometry overrides ran before, so the content keeps its place. -fn restore_slot_content(doc: &mut Document, kept: Vec<(NodeId, Vec)>) { - for (slot, nodes) in kept { - let keep = slot_edited(doc, slot) && is_slot(doc, slot) && nodes.iter().all(|n| !doc.contains(n.id)); - if !keep { - continue; - } - let ids: Vec = nodes.iter().map(|n| n.id).collect(); - for n in nodes { - // Pre-order: every parent is back before its children. - let _ = insert_linked(doc, n); - } - // Never leave links to a layer that couldn't come back. - for id in ids.into_iter().chain([slot]) { - let dangling = doc.node(id).is_some_and(|n| n.children.iter().any(|c| !doc.contains(*c))); - if dangling { - let live: Vec = doc.node(id).map(|n| n.children.iter().copied().filter(|c| doc.contains(*c)).collect()).unwrap_or_default(); - if let Some(n) = doc.node_mut(id) { - n.children = live; - } - } - } - } -} - -/// `spare`: layers the outer rebuild didn't reuse that were below `inst` (a swapped nested -/// instance): its layers the outer main doesn't have, matched here so they keep ids and overrides. -fn sync_instance_depth(doc: &mut Document, inst: NodeId, depth: usize, spare: BTreeMap>) { - if depth > MAX_DEPTH { - return; - } - let Some(mut root) = doc.node(inst).cloned() else { return }; - let NodeKind::Instance { component, props: root_props } = &root.kind else { return }; - let component = *component; - let Some(comp) = doc.node(component).cloned() else { return }; - if !matches!(comp.kind, NodeKind::Component { .. }) || doc.is_ancestor(inst, component) || doc.is_ancestor(component, inst) { - // A missing main, or an instance inside its own main (would recurse forever). - return; - } - let mut eff_props = default_props(&comp); - for (k, v) in root_props { - if eff_props.contains_key(k) { - eff_props.insert(k.clone(), v.clone()); - } - } - // Old layers (kept alive for their overrides), by (parent, source), by source alone (when only - // one layer mirrors it) and, only when those don't match, by name path. - let mut old_ids = old_layers(doc, inst); - old_ids.extend(spare.keys().copied()); - old_ids.sort(); - old_ids.dedup(); - let mut old: BTreeMap> = spare; - let mut by_source: BTreeMap<(NodeId, NodeId), NodeId> = BTreeMap::new(); - let mut by_src: BTreeMap> = BTreeMap::new(); - for i in &old_ids { - let Some(n) = doc.node_arc(*i).or_else(|| old.get(i)).cloned() else { continue }; - if let Some(s) = n.source { - if let Some(p) = n.parent { - by_source.entry((p, s)).or_insert(*i); - } - by_src.entry(s).and_modify(|e| *e = None).or_insert(Some(*i)); - } - old.insert(*i, n); - } - let mut by_path: Option> = None; - let mut main_paths: Option> = None; - // Ids for the copies: reuse the matching old layer's id (a layer the main moved to another - // parent keeps its id and overrides). - let src: Vec = doc.subtree(component).into_iter().filter(|i| *i != component).collect(); - let mut map: BTreeMap = BTreeMap::new(); - map.insert(component, inst); - let mut used: BTreeSet = BTreeSet::new(); - // Slots holding the instance's own content (new ids), and main layers not copied: the - // default content of those slots. - let mut edited: BTreeSet = BTreeSet::new(); - let mut skipped: BTreeSet = BTreeSet::new(); - let mut fresh_ids = Vec::new(); - for s in &src { - let Some(sn) = doc.node(*s) else { continue }; - if sn.parent.is_some_and(|p| skipped.contains(&p) || map.get(&p).is_some_and(|pn| edited.contains(pn))) { - skipped.insert(*s); - continue; - } - let parent_new = sn.parent.and_then(|p| map.get(&p).copied()); - let mut found = parent_new.and_then(|pn| by_source.get(&(pn, *s)).copied()).or_else(|| by_src.get(s).copied().flatten()); - if found.is_none() { - let paths = by_path.get_or_insert_with(|| path_map(doc, inst, true).into_iter().map(|(k, v)| (v, k)).collect()); - let mains = main_paths.get_or_insert_with(|| path_map(doc, component, false)); - found = mains.get(s).and_then(|p| paths.get(p)).copied(); - } - let nid = match found { - Some(o) if used.insert(o) => o, - _ => { - fresh_ids.push(*s); - continue; - } - }; - if let Some(m) = old.get(&nid).and_then(|o| variant_kept(doc, o, *s)) { - old.insert(nid, std::sync::Arc::new(m)); - } - if old.get(&nid).is_some_and(|o| holds_content(o)) && is_slot(doc, *s) { - edited.insert(nid); - } - map.insert(*s, nid); - } - for c in root.children.clone() { - let _ = doc.remove(c); - } - for s in fresh_ids { - let nid = doc.alloc_id(); - map.insert(s, nid); - } - // Build the copies (pre-order: parents first). - let mut enclosing: BTreeMap> = BTreeMap::new(); - enclosing.insert(inst, vec![inst]); - let mut inst_props: BTreeMap> = BTreeMap::new(); - inst_props.insert(inst, eff_props); - let mut nested = Vec::new(); - let mut new_nodes = Vec::new(); - // Text layers showing other text or style than their main layer (to re-measure). - let mut remeasure = Vec::new(); - for s in &src { - let Some(orig) = doc.node(*s) else { continue }; - let Some(&nid) = map.get(s) else { continue }; - let Some(parent) = orig.parent.and_then(|p| map.get(&p).copied()) else { continue }; - let mut n = orig.clone(); - n.id = nid; - n.parent = Some(parent); - n.children = orig.children.iter().filter_map(|c| map.get(c).copied()).collect(); - n.source = Some(*s); - n.overrides.clear(); - n.prop_refs.clear(); - let prev = old.get(&nid); - // Annotations belong to the layer they were added to, not to its copies. - n.annotations = prev.map(|o| o.annotations.clone()).unwrap_or_default(); - n.measurements = prev.map(|o| o.measurements.clone()).unwrap_or_default(); - if let Some(o) = prev { - carry_overrides(o, &mut n, Carry::Values); - keep_laid_out(doc, orig, o, &mut n); - } - if !edited.contains(&nid) { - // No longer a slot in the main: it mirrors the main again. - n.overrides.remove(SLOT); - } - // Component properties: the main layer's bindings use this instance's values; layers of - // nested instances use the nested instance's values (one level per source hop). - let enc = enclosing.get(&parent).cloned().unwrap_or_default(); - let mut chain: Vec<&Node> = vec![orig]; - while chain.len() < enc.len() { - let Some(next) = chain.last().and_then(|c| c.source).and_then(|x| doc.node(x)) else { break }; - chain.push(next); - } - for (j, level) in chain.iter().enumerate().rev() { - let Some(props) = enc.get(j).and_then(|e| inst_props.get(e)) else { continue }; - for (field, prop) in &level.prop_refs { - let g = field_group(field); - if n.overrides.contains(g) || (j > 0 && orig.overrides.contains(g)) { - continue; - } - if let Some(val) = props.get(prop) { - apply_prop(doc, &mut n, field, val); - } - } - } - if let NodeKind::Instance { component: c, props } = &n.kind { - inst_props.insert(nid, props.clone()); - let mut e = enc; - e.push(nid); - enclosing.insert(nid, e); - if !matches!(orig.kind, NodeKind::Instance { component: oc, .. } if oc == *c) { - nested.push(nid); - } - } else { - enclosing.insert(nid, enc); - } - if let (NodeKind::Text(a), NodeKind::Text(b)) = (&orig.kind, &n.kind) - && a != b - { - remeasure.push(nid); - } - new_nodes.push(n); - } - for n in new_nodes { - let (id, parent) = (n.id, n.parent); - if insert_linked(doc, n).is_err() - && let Some(p) = parent.and_then(|p| doc.node_mut(p)) - { - p.children.retain(|c| *c != id); - } - } - // Root: follows the main except its own placement, name, size and overrides. - let order: Vec = comp.children.iter().filter_map(|c| map.get(c).copied()).filter(|c| doc.contains(*c)).collect(); - let mut fresh = comp.clone(); - fresh.id = inst; - fresh.parent = root.parent; - fresh.children = order; - fresh.transform = root.transform; - fresh.kind = root.kind.clone(); - carry_prop_bindings(&root, &mut fresh); - // A nested instance keeps mirroring its layer of the outer main. - let nested_copy = inside_instance(doc, inst); - fresh.source = if nested_copy { root.source } else { Some(component) }; - fresh.constraints = root.constraints; - fresh.child_layout = root.child_layout.clone(); - fresh.interactions = root.interactions.clone(); - fresh.exports = root.exports.clone(); - fresh.prop_refs = root.prop_refs.clone(); - fresh.plugin_data = root.plugin_data.clone(); - fresh.annotations = root.annotations.clone(); - // Code links belong to the main component; instances read them from it. - fresh.code_links.clear(); - // An instance's own dev resources (its main's are listed with them, not copied). - fresh.dev_resources = root.dev_resources.clone(); - fresh.measurements = root.measurements.clone(); - fresh.locked = root.locked; - fresh.visible = root.visible; - fresh.var_modes = root.var_modes.clone(); - fresh.is_mask = root.is_mask; - fresh.exposed = root.exposed; - fresh.guides = root.guides.clone(); - fresh.dev_status = root.dev_status; - fresh.dev_changed = root.dev_changed; - fresh.dev_note = root.dev_note.clone(); - fresh.constrain_proportions = root.constrain_proportions; - // How the instance combines in a boolean group it sits in is its own. - fresh.bool_op = root.bool_op; - // A swapped nested instance keeps the name its layer was given in the outer main. - let renamed = nested_copy - && root.source.and_then(|s| doc.node(s)).is_some_and(|s| match &s.kind { - NodeKind::Instance { component: c, .. } => s.name != default_instance_name(doc, *c), - _ => false, - }); - fresh.name = if renamed { root.name.clone() } else { default_instance_name(doc, component) }; - // Switched to another variant: its own overrides stay where both variants agreed. - if let Some(prev) = root.source.and_then(|p| doc.node(p)).filter(|p| sibling_variants(doc, p.id, component)) { - let differs = diff_groups(prev, &comp); - root.overrides.retain(|g| g == "name" || !differs.contains(&g.as_str())); - follow_variant_resizing(&mut fresh.child_layout, &prev.child_layout, &comp.child_layout); - } - carry_overrides(&root, &mut fresh, Carry::Root); - if let Some(r) = doc.node_mut(inst) { - *r = fresh; - } - // A resized instance: children follow their constraints (auto layout re-flows later). - let (iw, ih) = doc.node(inst).map(|n| (n.width, n.height)).unwrap_or((comp.width, comp.height)); - if (iw - comp.width).abs() > 1e-9 || (ih - comp.height).abs() > 1e-9 { - figcraft_layout::apply_constraints(doc, inst, comp.width, comp.height); - remeasure = map.values().copied().collect(); - } - // Geometry overrides of children win over constraints. - for s in &src { - let Some(nid) = map.get(s).copied() else { continue }; - let Some(o) = old.get(&nid) else { continue }; - // A frame that kept the size its own resizing gave it: its layers (copied at their main - // layers' places) follow their constraints from its main layer's size. - if o.overrides.contains("resizing") - && !o.overrides.contains("size") - && doc.node(nid).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none()) - && let Some((mw, mh)) = doc.node(*s).map(|m| (m.width, m.height)) - { - figcraft_layout::apply_constraints(doc, nid, mw, mh); - } - if !o.overrides.contains("size") && !o.overrides.contains("position") { - continue; - } - remeasure.push(nid); - let Some((ow, oh)) = doc.node(nid).map(|n| (n.width, n.height)) else { continue }; - if o.overrides.contains("size") { - // Through set_size so vector networks scale with the box. - figcraft_layout::set_size(doc, nid, o.width, o.height); - if doc.node(nid).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none()) { - figcraft_layout::apply_constraints(doc, nid, ow, oh); - } - } - if let Some(n) = doc.node_mut(nid) { - carry_overrides(o, n, Carry::Geometry); - } - } - // Text whose characters or style differ from the main re-measures. - for nid in remeasure { - if let Some(n) = doc.node(nid) - && let Some((w, h)) = figcraft_text::auto_size(n) - && ((w - n.width).abs() > 1e-6 || (h - n.height).abs() > 1e-6) - { - figcraft_layout::set_size(doc, nid, w, h); - } - } - // Nested instances showing another main than the main's copy (swapped): rebuild from it, - // with the old layers below them this rebuild didn't reuse. - let unused: BTreeMap> = - old.iter().filter(|(i, _)| !used.contains(*i) && !doc.contains(**i)).map(|(i, x)| (*i, x.clone())).collect(); - for n in nested { - let mut below: BTreeMap> = unused - .iter() - .filter(|(_, x)| { - let mut cur = x.parent; - for _ in 0..=MAX_DEPTH * 4 { - match cur { - Some(p) if p == n => return true, - Some(p) => cur = unused.get(&p).and_then(|a| a.parent), - None => return false, - } - } - false - }) - .map(|(i, x)| (*i, x.clone())) - .collect(); - take_placing_overrides(doc, n, &old, &mut below); - sync_instance_depth(doc, n, depth + 1, below); - } -} - -/// Before swapped nested instance `n` is rebuilt from its new main: each of its old layers (the -/// copies just made of the outer main's, and `below`) takes the overrides held along the source -/// chain it had before this rebuild (`before`), through layers inside instances in the mains that -/// place `n`. Matched by name, they carry onto the new main's layers as the instance's own, so a -/// swap takes what the outer main set once: later rebuilds find layers mirroring the new main. -/// Geometry and slot content follow the new main; groups the layer overrides itself keep its value. -fn take_placing_overrides( - doc: &mut Document, - n: NodeId, - before: &BTreeMap>, - below: &mut BTreeMap>, -) { - let placing = placing_mains(doc, n); - for id in doc.subtree(n).into_iter().filter(|i| *i != n) { - let held = before.get(&id).map(|b| held_along(doc, b, &placing)).unwrap_or_default(); - if let Some(layer) = doc.node_mut(id) { - take(layer, &held); - } - } - for layer in below.values_mut() { - let held = held_along(doc, layer, &placing); - if !held.is_empty() { - take(std::sync::Arc::make_mut(layer), &held); - } - } -} - -/// The mains placing nested instance `n`: those of the layers it mirrors, outward. -fn placing_mains(doc: &Document, n: NodeId) -> BTreeSet { - let mut out = BTreeSet::new(); - let mut cur = doc.node(n).and_then(|x| x.source); - for _ in 0..=MAX_DEPTH { - let Some(s) = cur.and_then(|c| doc.node(c)).filter(|s| !matches!(s.kind, NodeKind::Component { .. })) else { break }; - out.extend(owning_component(doc, s.id)); - cur = s.source; - } - out -} - -/// The overrides but geometry and slot content held along `layer`'s source chain while it runs -/// through layers inside instances in `placing` mains, each with the nearest layer holding it. -fn held_along(doc: &Document, layer: &Node, placing: &BTreeSet) -> BTreeMap> { - let mut out = BTreeMap::new(); - let mut cur = layer.source; - for _ in 0..=MAX_DEPTH { - let Some(s) = cur.and_then(|c| doc.node_arc(c)) else { break }; - if !inside_instance(doc, s.id) || !owning_component(doc, s.id).is_some_and(|m| placing.contains(&m)) { - break; - } - for g in s.overrides.iter().filter(|g| !matches!(g.as_str(), "size" | "position" | SLOT)) { - out.entry(g.clone()).or_insert_with(|| s.clone()); - } - cur = s.source; - } - out -} - -/// Give `layer` the `held` groups it has no override of its own for. -fn take(layer: &mut Node, held: &BTreeMap>) { - for (g, from) in held { - if layer.overrides.insert(g.clone()) { - copy_group(from, layer, g); - } - } -} - -/// Whether main components `a` and `b` are two variants of one set. -fn sibling_variants(doc: &Document, a: NodeId, b: NodeId) -> bool { - a != b && variant_set(doc, a).is_some_and(|s| variant_set(doc, b) == Some(s)) -} - -/// Old copy `o` matched by name to main layer `s` of another variant (the instance switched -/// variants): `o` without the overrides of properties the two variants' layers differ in (`None` -/// when it keeps them all). Text layers keep every override. -fn variant_kept(doc: &Document, o: &Node, s: NodeId) -> Option { - let from = o.source.filter(|f| *f != s).and_then(|f| doc.node(f))?; - if o.overrides.is_empty() || matches!(o.kind, NodeKind::Text(_)) { - return None; - } - if !sibling_variants(doc, owning_component(doc, from.id)?, owning_component(doc, s)?) { - return None; - } - let differs = diff_groups(from, doc.node(s)?); - let mut m = o.clone(); - m.overrides.retain(|g| !differs.contains(&g.as_str())); - (m.overrides.len() != o.overrides.len()).then_some(m) -} - -/// Drop the overrides of instance `inst` but its text layers' (and its own size and the content of -/// its slots): what swapping it from the Assets panel keeps. -pub fn keep_text_overrides(doc: &mut Document, inst: NodeId) { - for d in old_layers(doc, inst).into_iter().chain([inst]) { - let Some(n) = doc.node(d) else { continue }; - let text = matches!(n.kind, NodeKind::Text(_)); - let keep = |g: &String| text || g == SLOT || (d == inst && g == "size"); - if !n.overrides.iter().all(keep) - && let Some(m) = doc.node_mut(d) - { - m.overrides.retain(keep); - } - } -} - -/// Where auto layout places and sizes a rebuilt copy `n` of main layer `orig`, keep the old -/// copy's `o` values (an instance's auto layout re-flows from its own content, so they usually -/// differ from the main's): unchanged layers then stay unchanged, and auto layout re-flows wherever -/// something that sizes or places layers did change. -fn keep_laid_out(doc: &Document, orig: &Node, o: &Node, n: &mut Node) { - if o.parent != n.parent || o.overrides.contains("size") || o.overrides.contains("position") { - return; - } - let flows = !n.child_layout.absolute && orig.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.layout.is_some()); - if flows { - // The main's rotation and skew, the old place. - let [a, b, c, d, _, _] = n.transform.as_coeffs(); - let [.., e, f] = o.transform.as_coeffs(); - n.transform = Affine::new([a, b, c, d, e, f]); - } - // Only layers whose shape is their box (a vector's points scale with it). - let boxed = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Text(_) | NodeKind::Rectangle | NodeKind::Ellipse { .. }); - let derived = |s: Sizing| boxed && ((s == Sizing::Hug && n.layout.is_some()) || (s == Sizing::Fill && flows)); - if derived(n.child_layout.sizing_h) { - n.width = o.width; - } - if derived(n.child_layout.sizing_v) { - n.height = o.height; - } -} - -/// The layers of instance `inst` a rebuild matches against (every layer below it but the content -/// of slots holding its own), ascending. -fn old_layers(doc: &Document, inst: NodeId) -> Vec { - let mut out = Vec::new(); - let mut stack = vec![inst]; - while let Some(id) = stack.pop() { - let Some(n) = doc.node(id) else { continue }; - for c in &n.children { - out.push(*c); - if out.len() > 1_000_000 { - break; - } - if doc.node(*c).is_some_and(|cn| !holds_content(cn)) { - stack.push(*c); - } - } - } - out.sort(); - out.dedup(); - out -} - -/// Insert a node whose parent/children links are already set (used when rebuilding instances). -fn insert_linked(doc: &mut Document, n: Node) -> Result<(), DocError> { - let parent = n.parent.ok_or_else(|| DocError::Invalid("no parent".into()))?; - let id = n.id; - let mut n = n; - let kids = std::mem::take(&mut n.children); - doc.insert(n, parent, None)?; - if let Some(m) = doc.node_mut(id) { - m.children = kids; - } - // The parent may already list us (its children were copied with ids): keep the first. - if let Some(p) = doc.node_mut(parent) { - let mut seen = BTreeSet::new(); - p.children.retain(|c| seen.insert(*c)); - } - Ok(()) -} - -/// The property groups whose value layer `n` takes from the same variables as its main layer -/// `src` (they differ only where the two resolve in different variable modes). -pub fn variable_groups(n: &Node, src: &Node) -> Vec<&'static str> { - let mut out = Vec::new(); - for (k, v) in &n.bound { - let Some(g) = binding_group(k) else { continue }; - if src.bound.get(k) == Some(v) && !out.contains(&g) { - out.push(g); - } - } - out -} - -/// The override group a variable binding key feeds (`None` for component property and layout -/// grid bindings, which follow other rules). -fn binding_group(key: &str) -> Option<&'static str> { - use crate::cmd::lists::{BoundList, list_binding}; - if key.starts_with(PROP_BINDING) { - return None; - } - match list_binding(key).map(|(l, _, _)| l) { - Some(BoundList::Fills) => Some("fills"), - Some(BoundList::Strokes) => Some("strokes"), - Some(BoundList::Effects) => Some("effects"), - Some(BoundList::Grids) => None, - None => Some(crate::cmd::props::override_group(key)).filter(|g| *g != "other"), - } -} - -/// The variable bindings of `n` that feed group `g`. -fn bindings_of<'a>(n: &'a Node, g: &'a str) -> impl Iterator + 'a { - n.bound.iter().filter(move |(k, _)| binding_group(k) == Some(g)) -} - -/// Property groups that differ between two versions of a node. -pub fn diff_groups(a: &Node, b: &Node) -> Vec<&'static str> { - let mut out = Vec::new(); - if a.fills != b.fills { - out.push("fills"); - } - if a.strokes != b.strokes || a.stroke != b.stroke { - out.push("strokes"); - } - if a.effects != b.effects { - out.push("effects"); - } - if a.opacity != b.opacity || a.blend != b.blend { - out.push("opacity"); - } - if a.visible != b.visible { - out.push("visible"); - } - if a.name != b.name { - out.push("name"); - } - if a.corners != b.corners { - out.push("corners"); - } - if (a.width - b.width).abs() > 1e-6 || (a.height - b.height).abs() > 1e-6 { - out.push("size"); - } - if resizing(a) != resizing(b) { - out.push("resizing"); - } - if a.transform != b.transform { - out.push("position"); - } - if let (NodeKind::Text(x), NodeKind::Text(y)) = (&a.kind, &b.kind) { - if x.characters != y.characters { - out.push("characters"); - } - if text_style_differs(x, y) { - out.push("textStyle"); - } - } - if let (Some(x), Some(y)) = (&a.layout, &b.layout) { - if x.padding != y.padding { - out.push("padding"); - } - if x.gap != y.gap || x.cross_gap != y.cross_gap { - out.push("gap"); - } - if x.main_align != y.main_align || x.cross_align != y.cross_align || x.content_align != y.content_align { - out.push("alignment"); - } - } - if a.grids != b.grids || a.styles.grid != b.styles.grid { - out.push("grids"); - } - if a.exports != b.exports { - out.push("exports"); - } - if a.interactions != b.interactions { - out.push("interactions"); - } - if a.var_modes != b.var_modes { - out.push("modes"); - } - if let (NodeKind::Instance { component: x, props: px }, NodeKind::Instance { component: y, props: py }) = (&a.kind, &b.kind) { - if x != y { - out.push("component"); - } - if px != py { - out.push("props"); - } - } - out -} - -/// The `resizing` group of a layer: how it sizes on each axis, and its min and max sizes. -fn resizing(n: &Node) -> (Sizing, Sizing, [Option; 4]) { - let c = &n.child_layout; - (c.sizing_h, c.sizing_v, [c.min_w, c.max_w, c.min_h, c.max_h]) -} - -/// Whether two texts differ in anything but their characters (the `textStyle` group). -fn text_style_differs(x: &TextData, y: &TextData) -> bool { - x.style != y.style - || x.runs != y.runs - || x.auto_resize != y.auto_resize - || x.align_h != y.align_h - || x.align_v != y.align_v - || x.paragraph_spacing != y.paragraph_spacing - || x.paragraph_indent != y.paragraph_indent - || x.list != y.list - || x.paragraphs != y.paragraphs - || x.list_spacing != y.list_spacing - || x.hanging_punctuation != y.hanging_punctuation - || x.hanging_list != y.hanging_list - || x.max_lines != y.max_lines - || x.features != y.features -} - -/// Whether a geometry change of `id` comes from layout rather than the user: an ancestor was -/// resized in the same edit (constraints), the parent is an auto layout frame, or auto-sized text. -fn geometry_derived(doc: &Document, before: &Document, id: NodeId, group: &str) -> bool { - for a in doc.ancestors(id) { - let (Some(x), Some(y)) = (doc.node(a), before.node(a)) else { continue }; - if (x.width - y.width).abs() > 1e-6 || (x.height - y.height).abs() > 1e-6 { - return true; - } - } - let Some(n) = doc.node(id) else { return true }; - if group == "position" && n.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.layout.is_some()) && !n.child_layout.absolute { - return true; - } - if group == "size" - && let NodeKind::Text(t) = &n.kind - { - let wchanged = before.node(id).is_some_and(|b| (b.width - n.width).abs() > 1e-6); - return match t.auto_resize { - TextAutoResize::WidthAndHeight => true, - TextAutoResize::Height => !wchanged, - _ => false, - }; - } - false -} - -/// The components sharing the properties of component `c`: the variants of its set, or `c` alone. -pub fn property_owners(doc: &Document, c: NodeId) -> Vec { - match variant_set(doc, c).and_then(|s| doc.node(s)) { - Some(set) => set.children.iter().copied().filter(|v| doc.node(*v).is_some_and(|n| matches!(n.kind, NodeKind::Component { .. }))).collect(), - None => vec![c], - } -} - -/// Make `value` the default of boolean property `name` on components `comps`, showing or hiding -/// their layers bound to it: on a main component, a boolean property's value is the visibility of -/// its layers. Returns the components and layers it changed. -pub fn set_boolean_default(doc: &mut Document, comps: &[NodeId], name: &str, value: bool) -> Vec { - let mut out = Vec::new(); - for &c in comps { - let Some(NodeKind::Component { props, .. }) = doc.node(c).map(|n| &n.kind) else { continue }; - let Some(PropDef::Boolean { default }) = props.get(name) else { continue }; - if *default != value - && let Some(NodeKind::Component { props, .. }) = doc.node_mut(c).map(|n| &mut n.kind) - { - props.insert(name.to_string(), PropDef::Boolean { default: value }); - out.push(c); - } - // The main's own layers (not those of instances nested in it, which follow their mains). - let layers: Vec = doc - .subtree(c) - .into_iter() - .filter(|d| *d != c && !inside_instance(doc, *d)) - .filter(|d| doc.node(*d).is_some_and(|n| n.visible != value && n.prop_refs.get("visible").map(String::as_str) == Some(name))) - .collect(); - for d in layers { - if let Some(n) = doc.node_mut(d) { - n.visible = value; - out.push(d); - } - } - } - out -} - -/// A main component's layer bound to a boolean property was shown or hidden: that is the -/// property's new default, which its other bound layers (in every variant) take too. Returns the -/// components and layers that changed as a result. -fn follow_boolean_defaults(doc: &mut Document, before: &Document, changed: &[NodeId]) -> Vec { - let mut out = Vec::new(); - for &id in changed { - let (Some(n), Some(b)) = (doc.node(id), before.node(id)) else { continue }; - if n.visible == b.visible || inside_instance(doc, id) { - continue; - } - let Some(name) = n.prop_refs.get("visible").cloned() else { continue }; - let visible = n.visible; - let Some(c) = owning_component(doc, id).filter(|c| *c != id) else { continue }; - out.extend(set_boolean_default(doc, &property_owners(doc, c), &name, visible)); - } - out -} - -/// After an edit: record overrides inside instances, then rebuild instances whose main changed, -/// whose properties or main were switched, or whose overrides were reset. -pub fn sync_after_edit(doc: &mut Document, before: &Document, changed: &[NodeId], touched: &BTreeSet) { - let mut changed = changed.to_vec(); - for id in follow_boolean_defaults(doc, before, &changed) { - if !changed.contains(&id) { - changed.push(id); - } - } - let changed = changed.as_slice(); - // 0. A slot whose layers were added, removed or reordered holds the instance's own content - // from now on; copies of instance layers placed elsewhere stop mirroring their main. - for &id in changed { - let (Some(n), Some(b)) = (doc.node(id), before.node(id)) else { continue }; - if n.children == b.children || owning_instance(doc, id).is_none() { - continue; - } - if let Some(slot) = slot_scope(doc, id) - && !slot_edited(doc, slot) - { - own_slot_content(doc, slot); - } - } - for &id in changed { - if !before.contains(id) { - relink_copy(doc, id); - } - } - let mut resync: BTreeSet = BTreeSet::new(); - // 1. Overrides for layers edited inside instances. - for &id in changed { - let (Some(n), Some(b)) = (doc.node(id), before.node(id)) else { continue }; - let Some(inst) = owning_instance(doc, id) else { continue }; - let top_root = inst == id && !inside_instance(doc, id); - if top_root - && let (NodeKind::Instance { component: c1, props: p1 }, NodeKind::Instance { component: c2, props: p2 }) = (&n.kind, &b.kind) - && (c1 != c2 || p1 != p2) - { - // Switching the main or a property value of an instance isn't an override. - resync.insert(id); - } - let raw = diff_groups(b, n); - let mut groups = raw.clone(); - // An instance's own place, resizing, export settings, interactions and variable modes are - // its own, not overrides. - if top_root { - groups.retain(|g| !matches!(*g, "component" | "props" | "position" | "resizing" | "exports" | "interactions" | "modes")); - } - if groups.iter().any(|g| matches!(*g, "size" | "position")) { - groups.retain(|g| !matches!(*g, "size" | "position") || !geometry_derived(doc, before, id, g)); - } - // A value equal to the main's isn't an override (setting it back clears the override). - let mut cleared = Vec::new(); - if let Some(src) = n.source.and_then(|s| doc.node(s)) { - let mut differs = diff_groups(src, n); - if inst == id { - differs.retain(|g| *g != "name"); - if n.name != default_instance_name(doc, src.id) { - differs.push("name"); - } - } - // A value from the same variable as the main's (resolved in this layer's modes) isn't an - // override either. - let from_variables = variable_groups(n, src); - groups.retain(|g| differs.contains(g) && !from_variables.contains(g)); - // Binding another variable (or none) where the main binds one is an override. - for (k, _) in b.bound.iter().chain(n.bound.iter()) { - let Some(g) = binding_group(k) else { continue }; - let rebound = b.bound.get(k) != n.bound.get(k) && !bindings_of(n, g).eq(bindings_of(src, g)); - if rebound && !groups.contains(&g) { - groups.push(g); - } - } - cleared = - raw.iter().filter(|g| !matches!(**g, "size" | "position") && !differs.contains(g) && n.overrides.contains(**g)).copied().collect(); - if top_root { - cleared.retain(|g| !matches!(*g, "component" | "props")); - } - } - let lost = b.overrides.iter().any(|g| !n.overrides.contains(g)); - let nested_switch = !top_root && raw.iter().any(|g| matches!(*g, "component" | "props")); - if (!groups.is_empty() || !cleared.is_empty()) - && let Some(m) = doc.node_mut(id) - { - for g in groups { - m.overrides.insert(g.to_string()); - } - for g in cleared { - m.overrides.remove(g); - } - } - if (lost || nested_switch) - && let Some(r) = root_instance(doc, id) - { - resync.insert(r); - } - } - // 2. Components whose layers changed, were added or removed. Value-only edits (paint, text, - // visibility, …) are carried to the copies directly; anything else rebuilds the instances. - let mut comps: BTreeSet = BTreeSet::new(); - let mut patches: Vec<(NodeId, Vec<&'static str>)> = Vec::new(); - // Main layers that only gained new layers or lost some: their copies take the same change. - let mut restructured: Vec = Vec::new(); - let mut covered: BTreeSet = BTreeSet::new(); - for &id in changed { - if let (Some(b), Some(n)) = (before.node(id), doc.node(id)) - && b.children != n.children - && owning_instance(doc, id).is_none() - && owning_component(doc, id).is_some() - && let Some(layers) = restructurable(doc, before, b, n) - { - restructured.push(id); - covered.extend(layers); - } - } - for &id in changed { - if covered.contains(&id) || restructured.contains(&id) { - continue; - } - let Some(c) = owning_component(doc, id) else { continue }; - let (Some(b), Some(n)) = (before.node(id), doc.node(id)) else { - comps.insert(c); - continue; - }; - if b == n { - continue; - } - match patchable(doc, b, n) { - Some(groups) => patches.push((id, groups)), - None => { - comps.insert(c); - } - } - } - for &id in touched { - if !doc.contains(id) - && !covered.contains(&id) - && let Some(c) = owning_component(before, id) - && doc.contains(c) - { - comps.insert(c); - } - } - for r in resync { - if doc.contains(r) { - sync_instance(doc, r); - if let Some(c) = owning_component(doc, r) { - comps.insert(c); - } - } - } - // A copy the patch can't follow exactly rebuilds instead (as does any other change). - let patched = comps.is_empty() - && !full_rebuild() - && (restructured.is_empty() || patch_structure(doc, before, &restructured)) - && (patches.is_empty() || patch_copies(doc, &patches)); - #[cfg(test)] - if patched && (!restructured.is_empty() || !patches.is_empty()) { - PATCHED.with(|c| c.set(c.get() + 1)); - } - if !patched { - comps.extend(patches.iter().map(|(id, _)| *id).chain(restructured.iter().copied()).filter_map(|id| owning_component(doc, id))); - } - // 3. Instances of changed components follow; an instance inside another component makes that - // one change too. - rebuild_instances(doc, &comps); -} - -#[cfg(test)] -thread_local! { - /// Tests: rebuild instances even for edits that could be patched (to compare both ways). - pub(crate) static FULL_REBUILD: std::cell::Cell = const { std::cell::Cell::new(false) }; - /// Tests: edits carried to the copies without rebuilding instances. - pub(crate) static PATCHED: std::cell::Cell = const { std::cell::Cell::new(0) }; -} - -#[cfg(test)] -fn full_rebuild() -> bool { - FULL_REBUILD.with(std::cell::Cell::get) -} - -#[cfg(not(test))] -fn full_rebuild() -> bool { - false -} - -/// The property groups of a main-component layer edited from `b` to `n` when the edit changed -/// values only (paint, text, visibility, size, position, …) that copies take as they are: carried to them without -/// rebuilding the instances ([`patch_copies`]). `None` when anything else changed. -fn patchable(doc: &Document, b: &Node, n: &Node) -> Option> { - const VALUES: [&str; 17] = [ - "fills", - "strokes", - "effects", - "opacity", - "visible", - "name", - "corners", - "characters", - "textStyle", - "padding", - "gap", - "alignment", - "grids", - "exports", - "interactions", - "size", - "position", - ]; - if b.children != n.children || b.parent != n.parent || b.kind.type_name() != n.kind.type_name() { - return None; - } - let groups = diff_groups(b, n); - if groups.is_empty() || groups.iter().any(|g| !VALUES.contains(g)) { - return None; - } - // Exactly those groups (and the layer's own overrides) changed. - let mut probe = n.clone(); - for g in &groups { - copy_group(b, &mut probe, g); - } - probe.overrides = b.overrides.clone(); - if probe != *b { - return None; - } - // Values a component property sets (on this layer or a main it mirrors) come from instances. - let mut cur = Some(n); - for _ in 0..=MAX_DEPTH { - let Some(x) = cur else { break }; - if x.prop_refs.keys().any(|f| groups.contains(&field_group(f))) { - return None; - } - cur = x.source.and_then(|s| doc.node(s)); - } - Some(groups) -} - -/// Copies by the layer they mirror; nested instances swapped to a main also follow it. -fn copies_index(doc: &Document) -> BTreeMap> { - let mut copies: BTreeMap> = BTreeMap::new(); - for n in doc.nodes() { - let Some(s) = n.source else { continue }; - copies.entry(s).or_default().push(n.id); - if let (NodeKind::Instance { component, .. }, Some(NodeKind::Instance { component: other, .. })) = (&n.kind, doc.node(s).map(|x| &x.kind)) - && component != other - { - copies.entry(*component).or_default().push(n.id); - } - } - copies -} - -/// When main layer `b` → `n` only gained new plain layers (created in this edit: no instances, -/// components or property bindings in them) or lost layers that were deleted: those layers, which -/// [`patch_structure`] carries to the copies. `None` for anything else. -fn restructurable(doc: &Document, before: &Document, b: &Node, n: &Node) -> Option> { - let mut probe = n.clone(); - probe.children = b.children.clone(); - probe.overrides = b.overrides.clone(); - if probe != *b { - return None; - } - let mut layers = Vec::new(); - for c in n.children.iter().filter(|c| !b.children.contains(c)) { - for d in doc.subtree(*c) { - let x = doc.node(d)?; - let plain = !matches!(x.kind, NodeKind::Instance { .. } | NodeKind::Component { .. } | NodeKind::ComponentSet { .. }); - if before.contains(d) || !plain || !x.prop_refs.is_empty() { - return None; - } - layers.push(d); - } - } - for c in b.children.iter().filter(|c| !n.children.contains(c)) { - if doc.contains(*c) { - return None; - } - layers.extend(before.subtree(*c)); - } - Some(layers) -} - -/// Carry the new and deleted layers of main layers `seeds` ([`restructurable`]) to every copy, -/// through copies of copies (slots holding an instance's own content keep it). Returns false -/// (leaving a partial patch for the caller to rebuild over) when a copy can't follow exactly. -fn patch_structure(doc: &mut Document, before: &Document, seeds: &[NodeId]) -> bool { - let copies = copies_index(doc); - let mut work: Vec = seeds.to_vec(); - let mut steps = 0usize; - while let Some(p) = work.pop() { - steps += 1; - if steps > 10_000_000 { - return false; - } - let Some(pn) = doc.node(p).cloned() else { continue }; - for c in copies.get(&p).cloned().unwrap_or_default() { - let Some(cn) = doc.node(c).cloned() else { continue }; - if holds_content(&cn) { - continue; - } - if let (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) = (&pn.kind, &cn.kind) - && a != b - { - continue; - } - // The copy's layers by the layer of `p` they mirror. - let mut mirrors: BTreeMap = BTreeMap::new(); - for k in &cn.children { - let Some(src) = doc.node(*k).and_then(|x| x.source) else { return false }; - if mirrors.insert(src, *k).is_some() { - return false; - } - } - let mut order = Vec::with_capacity(pn.children.len()); - for pc in &pn.children { - if let Some(k) = mirrors.remove(pc) { - order.push(k); - continue; - } - // A layer the copy lacks must be new in this edit, placed as in the main. - let absolute = doc.node(*pc).is_some_and(|x| x.child_layout.absolute); - if before.contains(*pc) || !child_follows_main(doc, c, absolute) { - return false; - } - match copy_layers(doc, *pc, c) { - Some(k) => order.push(k), - None => return false, - } - } - // Copies of layers the main lost. - for (src, k) in mirrors { - if doc.contains(src) || doc.remove(k).is_err() { - return false; - } - } - if doc.node(c).is_some_and(|x| x.children != order) - && let Some(m) = doc.node_mut(c) - { - m.children = order; - } - work.push(c); - } - } - true -} - -/// Whether a layer placed in copy `parent` (absolutely positioned or not) takes its main layer's -/// place and size: nothing on the way up applies constraints ([`follows_main_geometry`]). -fn child_follows_main(doc: &Document, parent: NodeId, absolute: bool) -> bool { - let Some(p) = doc.node(parent) else { return false }; - let constrains = p.layout.is_none() || absolute; - let resized = sized_own(p) - || match &p.kind { - NodeKind::Instance { component, .. } => { - doc.node(*component).is_none_or(|m| (m.width - p.width).abs() > 1e-9 || (m.height - p.height).abs() > 1e-9) - } - _ => false, - }; - !(constrains && resized) && follows_main_geometry(doc, parent) -} - -/// Copy main layer `src` (with its layers) into copy `parent` as layers mirroring them; returns the -/// new copy (the caller places it among `parent`'s layers). -fn copy_layers(doc: &mut Document, src: NodeId, parent: NodeId) -> Option { - let sub = doc.subtree(src); - let map: BTreeMap = sub.iter().map(|d| (*d, doc.alloc_id())).collect(); - for d in &sub { - let mut n = doc.node(*d)?.clone(); - n.id = *map.get(d)?; - n.parent = if *d == src { Some(parent) } else { n.parent.and_then(|x| map.get(&x).copied()) }; - n.children = n.children.iter().filter_map(|k| map.get(k).copied()).collect(); - n.source = Some(*d); - n.overrides.clear(); - n.prop_refs.clear(); - n.annotations.clear(); - n.measurements.clear(); - insert_linked(doc, n).ok()?; - } - map.get(&src).copied() -} - -/// Carry value-only edits of main-component layers (`seeds`: layer, groups) to every copy, through -/// copies of copies, except where a copy overrides the group. Returns false (leaving a partial -/// patch for the caller to rebuild over) when a copy can't follow exactly. -fn patch_copies(doc: &mut Document, seeds: &[(NodeId, Vec<&'static str>)]) -> bool { - let copies = copies_index(doc); - let text = |g: &str| matches!(g, "characters" | "textStyle"); - let mut work: Vec<(NodeId, Vec<&'static str>)> = seeds.to_vec(); - let mut steps = 0usize; - while let Some((p, groups)) = work.pop() { - steps += 1; - if steps > 10_000_000 { - return false; - } - let Some(pn) = doc.node(p).cloned() else { continue }; - for c in copies.get(&p).cloned().unwrap_or_default() { - let Some(cn) = doc.node(c) else { continue }; - // A swapped nested instance shows its own main, not the layer it stands in for. - if let (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) = (&pn.kind, &cn.kind) - && a != b - { - continue; - } - // An instance keeps its own visibility, place, export settings and interactions; its - // name follows the main's unless renamed; a resized one lays its layers out against - // the main's size. - let root = matches!(pn.kind, NodeKind::Component { .. }); - if root && ((groups.contains(&"name") && !cn.overrides.contains("name")) || (groups.contains(&"size") && cn.overrides.contains("size"))) { - return false; - } - let gs: Vec<&'static str> = groups - .iter() - .copied() - .filter(|g| !(cn.overrides.contains(*g) || root && matches!(*g, "visible" | "name" | "position" | "exports" | "interactions"))) - .collect(); - if gs.is_empty() { - continue; - } - let geometry = gs.iter().any(|g| matches!(*g, "size" | "position")); - if geometry && !follows_main_geometry(doc, c) { - return false; - } - // Text content and style share their runs: a copy overriding one keeps its runs when the - // other follows the main (as a rebuild carries them). - let runs = match &cn.kind { - NodeKind::Text(t) if gs.iter().any(|g| text(g)) && cn.overrides.iter().any(|g| text(g)) => Some(t.runs.clone()), - _ => None, - }; - if let Some(m) = doc.node_mut(c) { - for g in &gs { - copy_group(&pn, m, g); - } - if let (Some(r), NodeKind::Text(t)) = (runs, &mut m.kind) { - t.runs = r; - } - } - if (geometry || gs.iter().any(|g| text(g))) - && let Some((w, h)) = doc.node(c).and_then(figcraft_text::auto_size) - && doc.node(c).is_some_and(|n| (n.width - w).abs() > 1e-6 || (n.height - h).abs() > 1e-6) - { - figcraft_layout::set_size(doc, c, w, h); - } - work.push((c, gs)); - } - } - true -} - -/// Whether a copy's size and place are its main layer's: no layer around it inside its instances -/// applies constraints on the way down to it (a resized instance or frame without auto layout, or -/// a resized auto layout instance holding it absolutely positioned). Auto layout re-flows the rest. -fn follows_main_geometry(doc: &Document, id: NodeId) -> bool { - let chain = scope(doc, id); - chain.windows(2).all(|w| { - let (Some(below), Some(a)) = (w.first().and_then(|i| doc.node(*i)), w.get(1).and_then(|i| doc.node(*i))) else { return false }; - let constrains = a.layout.is_none() || below.child_layout.absolute; - if sized_own(a) && constrains { - return false; - } - match &a.kind { - NodeKind::Instance { component, .. } => { - !constrains || doc.node(*component).is_some_and(|m| (m.width - a.width).abs() <= 1e-9 && (m.height - a.height).abs() <= 1e-9) - } - _ => true, - } - }) -} - -/// Whether a layer inside an instance overrides its size or resizing: it may not have its main -/// layer's size, so the layers in it can't take their main layers' places as they are. -fn sized_own(n: &Node) -> bool { - n.overrides.contains("size") || n.overrides.contains("resizing") -} - -/// Rebuild every instance of `comps` (and of the components that hold those instances, and so on -/// outwards), each outermost instance once, inner components first. -fn rebuild_instances(doc: &mut Document, comps: &BTreeSet) { - if comps.is_empty() { - return; - } - // Outermost instances by the components their layers instantiate. - let mut roots_of: BTreeMap> = BTreeMap::new(); - for n in doc.nodes() { - if let NodeKind::Instance { component, .. } = &n.kind { - roots_of.entry(*component).or_default().insert(root_instance(doc, n.id).unwrap_or(n.id)); - } - } - // Affected components, with how deep their dependencies go (an instance inside component X of - // component Y: X follows Y). - let mut level: BTreeMap = comps.iter().map(|c| (*c, 0)).collect(); - let mut outer: BTreeMap> = BTreeMap::new(); - let mut frontier: Vec = comps.iter().copied().collect(); - let mut rounds = 0; - while !frontier.is_empty() && rounds < 64 { - rounds += 1; - let mut next = Vec::new(); - for c in frontier { - let lc = level.get(&c).copied().unwrap_or(0); - for r in roots_of.get(&c).into_iter().flatten() { - let o = *outer.entry(*r).or_insert_with(|| owning_component(doc, *r)); - if let Some(x) = o - && x != c - { - let lx = level.entry(x).or_insert(0); - if *lx < lc + 1 { - *lx = lc + 1; - next.push(x); - } - } - } - } - frontier = next; - } - // Instances inside components by level, then the rest (each once). - let mut order: Vec<(usize, NodeId)> = outer.iter().map(|(r, o)| (o.and_then(|x| level.get(&x).copied()).unwrap_or(usize::MAX), *r)).collect(); - order.sort(); - for (_, r) in order { - if doc.contains(r) { - sync_instance(doc, r); - } - } -} - -/// Slot `slot` (inside an instance) takes the instance's own content: the layers it shows (the -/// main's default content until now) stop mirroring the main, and instances among them stand -/// alone, linked to their own mains. -fn own_slot_content(doc: &mut Document, slot: NodeId) { - if let Some(n) = doc.node_mut(slot) { - n.overrides.insert(SLOT.into()); - } - for nested in unlink_layers(doc, slot, None) { - rebase_instance(doc, nested); - } -} - -/// Unlink the layers below `top` that are layers of `owner` (an instance, or `None` for the -/// content of a slot): plain layers forget the main layer they mirrored and their property -/// bindings. The instances among them are returned, to re-link to their own mains (with -/// [`rebase_instance`]) once the caller is done. -pub fn unlink_layers(doc: &mut Document, top: NodeId, owner: Option) -> Vec { - let picked: Vec<(NodeId, bool)> = - doc.subtree(top).into_iter().filter(|d| *d != top && enclosing_instance(doc, *d) == owner).map(|d| (d, is_instance(doc, d))).collect(); - let mut nested = Vec::new(); - for (d, instance) in picked { - let Some(n) = doc.node_mut(d) else { continue }; - n.prop_refs.clear(); - if instance { - nested.push(d); - } else { - n.source = None; - n.overrides.clear(); - } - } - nested -} - -/// A new layer copied from inside an instance (pasted, duplicated) to where no instance owns it -/// stops mirroring the main; a copied nested instance is re-linked to its own main (its look kept). -fn relink_copy(doc: &mut Document, id: NodeId) { - let Some(n) = doc.node(id) else { return }; - match &n.kind { - NodeKind::Instance { component, .. } => { - let main_ok = doc.node(*component).is_some_and(|m| matches!(m.kind, NodeKind::Component { .. })); - if main_ok && !inside_instance(doc, id) && (n.source != Some(*component) || !instance_issues(doc, id).is_empty()) { - rebase_instance(doc, id); - } - } - _ => { - if (n.source.is_some() || !n.overrides.is_empty()) - && owning_instance(doc, id).is_none() - && let Some(m) = doc.node_mut(id) - { - m.source = None; - m.overrides.clear(); - } - } - } -} - -/// Refuse edits that add, remove or reorder layers inside an instance (only its main can), outside -/// its slots. -pub fn check_structure_edit(before: &Document, after: &Document) -> Result<(), String> { - for &id in after.dirty() { - let (Some(n), Some(b)) = (after.node(id), before.node(id)) else { continue }; - // Library copies change only through library updates (and are read-only otherwise). - if n.children == b.children || after.is_remote(id) { - continue; - } - if structure_locked(before, id) && structure_locked(after, id) { - let name = owning_instance(after, id).and_then(|i| after.node(i)).map(|i| i.name.clone()).unwrap_or_default(); - return Err(format!( - "layers can't be added, removed or reordered inside the instance \"{name}\": edit its main component, or detach the instance first" - )); - } - } - Ok(()) -} - -/// Refuse edits an instance doesn't take as overrides ([`refused_override`]); the edit is rolled -/// back. -pub fn check_override_edit(before: &Document, after: &Document) -> Result<(), String> { - match after.dirty().iter().find_map(|id| refused_override(before, after, *id)) { - Some(what) => Err(format!("{what} comes from the main component in an instance: edit the main component, or detach the instance")), - None => Ok(()), - } -} - -/// What about layer `id` an edit from `before` to `after` changed that only its main can: the -/// auto layout of an instance or a layer in one but its padding, gap and alignment, or the -/// position (rotation too), constraints or place in auto layout (absolute, grid cell) of a layer -/// inside an instance (its size and resizing are overrides). `None` when the edit is fine, also -/// when the main changed the same way in this edit (a library update). -pub fn refused_override(before: &Document, after: &Document, id: NodeId) -> Option<&'static str> { - let (n, b) = (after.node(id)?, before.node(id)?); - if after.is_remote(id) || n.source != b.source || owning_instance(before, id).is_none() || owning_instance(after, id).is_none() { - return None; - } - let (mb, ma) = (before.node(n.source?)?, after.node(n.source?)?); - let mode = |x: &Node| x.layout.as_ref().map(|l| l.mode); - // Auto layout settings other than the overridable ones. - let settings = |x: &Node| { - x.layout.clone().map(|mut l| { - (l.gap, l.cross_gap, l.padding, l.main_align, l.cross_align) = (0.0, 0.0, [0.0; 4], MainAlign::Min, CrossAlign::Min); - l.content_align = ContentAlign::Auto; - l - }) - }; - // Child layout settings other than the resizing ones. - let placing = |x: &Node| { - let mut c = x.child_layout.clone(); - (c.sizing_h, c.sizing_v, c.min_w, c.max_w, c.min_h, c.max_h) = (Sizing::Fixed, Sizing::Fixed, None, None, None, None); - c - }; - if mode(n) != mode(b) && mode(ma) == mode(mb) { - return Some("The auto layout direction"); - } - if settings(n) != settings(b) && settings(ma) == settings(mb) { - return Some("This auto layout setting"); - } - if !inside_instance(after, id) || !inside_instance(before, id) { - return None; - } - if n.transform != b.transform && ma.transform == mb.transform && !geometry_derived(after, before, id, "position") { - return Some("A layer's position"); - } - if n.constraints != b.constraints && ma.constraints == mb.constraints { - return Some("A layer's constraints"); - } - if placing(n) != placing(b) && placing(ma) == placing(mb) { - return Some("A layer's place in auto layout"); - } - None -} - -/// Bounded source and parent lookups while a rebase reads an unchanged document. -struct RebaseSources<'a> { - doc: &'a Document, - chains: BTreeMap>>, - parents: BTreeMap<(NodeId, NodeId), Option>, - main_changes: BTreeMap, -} - -impl<'a> RebaseSources<'a> { - const PARENTS: usize = 512; - const CACHE: usize = 8192; - - fn new(doc: &'a Document) -> Self { - Self { doc, chains: BTreeMap::new(), parents: BTreeMap::new(), main_changes: BTreeMap::new() } - } - - fn chain(&mut self, source: NodeId) -> Option> { - if let Some(chain) = self.chains.get(&source) { - return chain.clone(); - } - let read = || { - let mut chain = Vec::new(); - let mut seen = BTreeSet::new(); - let mut current = Some(source); - for _ in 0..MAX_DEPTH { - let Some(id) = current else { return Some(chain) }; - if !seen.insert(id) { - return None; - } - let n = self.doc.node(id)?; - chain.push(id); - current = n.source; - } - current.is_none().then_some(chain) - }; - let chain = read(); - if self.chains.len() < Self::CACHE { - self.chains.insert(source, chain.clone()); - } - chain - } - - /// Validate the complete parent chain, even if `main` was found before a cycle. - fn in_main(&mut self, id: NodeId, main: NodeId) -> Option { - if let Some(found) = self.parents.get(&(id, main)) { - return *found; - } - let read = || { - let mut current = Some(id); - let mut seen = BTreeSet::new(); - let mut found = false; - for _ in 0..Self::PARENTS { - let Some(id) = current else { return Some(found) }; - if !seen.insert(id) { - return None; - } - found |= id == main; - current = self.doc.node(id)?.parent; - } - current.is_none().then_some(found) - }; - let found = read(); - if self.parents.len() < Self::CACHE { - self.parents.insert((id, main), found); - } - found - } - - fn member(&mut self, source: NodeId, main: NodeId, target: NodeId) -> Option { - let chain = self.chain(source)?; - if chain.contains(&target) { - return None; - } - let mut member = None; - for id in chain { - if self.in_main(id, main)? && id != main && member.is_none() { - member = Some(id); - } - } - member - } - - /// Nearest enclosing instances and their current mains. Slots with own content end scope. - fn owners(&self, id: NodeId) -> Option> { - let mut owners = Vec::new(); - let mut current = self.doc.node(id)?.parent; - let mut seen = BTreeSet::from([id]); - for _ in 0..Self::PARENTS { - let Some(id) = current else { return Some(owners) }; - if !seen.insert(id) { - return None; - } - let n = self.doc.node(id)?; - if holds_content(n) { - return Some(owners); - } - if let NodeKind::Instance { component, .. } = &n.kind { - owners.push((id, *component)); - } - current = n.parent; - } - current.is_none().then_some(owners) - } - - fn contextual(&mut self, source: NodeId, owners: &[(NodeId, NodeId)]) -> bool { - owners.iter().any(|(_, main)| self.in_main(source, *main) == Some(true) && source != *main) - } - - /// A contextual child belongs to the same placed slot as its enclosing instance. - fn valid(&mut self, source: NodeId, target: NodeId, owners: &[(NodeId, NodeId)]) -> bool { - let Some((owner, main)) = owners.first() else { return false }; - if self.member(source, *main, target).is_none() { - return false; - } - if self.in_main(source, *main) == Some(true) { - return true; - } - let Some(source_owner) = self.doc.node(*owner).and_then(|n| n.source) else { return false }; - matches!(self.doc.node(source_owner).map(|n| &n.kind), Some(NodeKind::Instance { component, .. }) if component == main) - && self.owners(source).and_then(|owners| owners.first().copied()).is_some_and(|(id, _)| id == source_owner) - } - - fn placed(&mut self, id: NodeId, owners: &[(NodeId, NodeId)]) -> Option { - let source = self.doc.node(id)?.source?; - if !matches!(self.doc.node(source)?.kind, NodeKind::Instance { .. } | NodeKind::Frame) - || !self.valid(source, id, owners) - || !self.contextual(source, owners) - { - return None; - } - Some(source) - } - - fn beneath_swap(&mut self, owners: &[(NodeId, NodeId)], stop: NodeId) -> bool { - for (index, (id, main)) in owners.iter().enumerate() { - if *id == stop { - break; - } - let changed = if let Some(changed) = self.main_changes.get(id) { - *changed - } else { - let changed = self.placed(*id, &owners[index + 1..]).is_some_and(|source| match self.doc.node(source).map(|n| &n.kind) { - Some(NodeKind::Instance { component, .. }) => component != main, - Some(NodeKind::Frame) => true, - _ => false, - }); - if self.main_changes.len() < Self::CACHE { - self.main_changes.insert(*id, changed); - } - changed - }; - if changed { - return true; - } - } - false - } -} - -/// Re-derive `source` links and overrides of instance `inst` against its own main by comparing -/// values (after detaching an outer instance, restoring a main, or copying a nested instance out): -/// the look is kept exactly. Values a component property sets aren't overrides. -pub fn rebase_instance(doc: &mut Document, inst: NodeId) { - let Some(NodeKind::Instance { component, .. }) = doc.node(inst).map(|n| n.kind.clone()) else { return }; - let mine = path_map(doc, inst, true); - let theirs: BTreeMap = - if doc.contains(component) { path_map(doc, component, false).into_iter().map(|(k, v)| (v, k)).collect() } else { BTreeMap::new() }; - let value_groups = |a: &Node, b: &Node| -> Vec { - diff_groups(a, b).into_iter().filter(|g| !matches!(*g, "size" | "position")).map(str::to_string).collect() - }; - let mut updates = Vec::new(); - let mut sources = RebaseSources::new(doc); - let root_owners = sources.owners(inst).unwrap_or_default(); - let placed_source = sources.placed(inst, &root_owners); - let mut contexts = vec![(inst, component)]; - contexts.extend_from_slice(&root_owners); - for (id, path) in &mine { - let Some(n) = doc.node(*id) else { continue }; - let owners = sources.owners(*id).unwrap_or_default(); - let nearest_main = owners.first().map(|(_, main)| *main); - let valid_source = n.source.filter(|source| sources.valid(*source, *id, &owners)); - let mut main = valid_source.and_then(|source| sources.member(source, component, *id)); - // A swapped or Frame-to-Instance subtree is compared to its current main by the inner - // rebase. An outer name path must not rebind those children to the old expanded copies. - if main.is_none() && valid_source.is_some() && sources.beneath_swap(&owners, inst) { - updates.push((*id, valid_source, n.overrides.clone())); - continue; - } - main = main - .or_else(|| theirs.get(path).copied().filter(|source| nearest_main.is_some_and(|scope| sources.member(*source, scope, *id).is_some()))); - let source = valid_source.filter(|source| sources.contextual(*source, &contexts)).filter(|_| main.is_some()).or(main); - let mut overrides: BTreeSet = n.overrides.iter().filter(|g| !matches!(g.as_str(), "component" | "props")).cloned().collect(); - if let Some(m) = source.and_then(|source| doc.node(source)) { - let mut bound: Vec<&str> = m.prop_refs.keys().map(|f| field_group(f)).collect(); - bound.extend(variable_groups(n, m)); - overrides.extend(value_groups(m, n).into_iter().filter(|g| !bound.contains(&g.as_str()))); - if matches!((&m.kind, &n.kind), (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) if a != b) { - overrides.insert("component".into()); - } - } - updates.push((*id, source, overrides)); - } - // A placed nested root compares to its actual slot. A detached root compares to its own main. - let root_base = placed_source.or_else(|| { - let chain = sources.chain(component)?; - (!chain.contains(&inst) && sources.in_main(component, inst) == Some(false)).then_some(component) - }); - let root_groups = match (root_base.and_then(|source| doc.node(source)), doc.node(inst)) { - (Some(m), Some(n)) => { - let from_variables = variable_groups(n, m); - let mut groups: Vec = value_groups(m, n) - .into_iter() - .filter(|g| !matches!(g.as_str(), "name" | "exports" | "interactions") && !from_variables.contains(&g.as_str())) - .filter(|g| placed_source.is_some() || !matches!(g.as_str(), "resizing" | "modes")) - .collect(); - let named = if placed_source.is_some() { n.name != m.name } else { n.name != default_instance_name(doc, component) }; - if named { - groups.push("name".into()); - } - if placed_source.is_some() - && matches!((&m.kind, &n.kind), (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) if a != b) - { - groups.push("component".into()); - } - groups - } - _ => Vec::new(), - }; - drop(sources); - for (id, main, overrides) in updates { - if let Some(n) = doc.node_mut(id) { - n.source = main; - n.overrides = if main.is_some() { overrides } else { BTreeSet::new() }; - } - } - if let Some(n) = doc.node_mut(inst) { - n.source = root_base; - // A top-level instance's resizing and modes are its own (a nested one's override its main - // layer's). - n.overrides.retain(|g| { - !matches!(g.as_str(), "component" | "props" | "position") && (placed_source.is_some() || !matches!(g.as_str(), "resizing" | "modes")) - }); - n.overrides.extend(root_groups); - } -} - #[cfg(test)] #[path = "components_rebase_source_tests.rs"] mod rebase_source_tests; -/// A set's default variant: the one in its top-left corner (nearest to it, then the top-most, then -/// the left-most). -pub fn default_variant(doc: &Document, set: NodeId) -> Option { - let corner = |c: &NodeId| { - doc.node(*c).filter(|n| matches!(n.kind, NodeKind::Component { .. })).map(|n| n.transform.as_coeffs()).map(|[.., x, y]| (x + y, y, x)) - }; - let kids = &doc.node(set)?.children; - kids.iter() - .copied() - .filter_map(|c| corner(&c).map(|k| (k, c))) - .min_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) - .map(|(_, c)| c) -} - -/// Variants of a set sharing one combination of property values (each group two or more, in -/// layer order): a conflict to resolve by giving each a unique combination. -pub fn variant_conflicts(doc: &Document, set: NodeId) -> Vec> { - let mut by: BTreeMap, Vec> = BTreeMap::new(); - for c in doc.node(set).map(|s| s.children.clone()).unwrap_or_default() { - if let Some(n) = doc.node(c).filter(|n| matches!(n.kind, NodeKind::Component { .. })) { - by.entry(variant_props(&n.name)).or_default().push(c); - } - } - by.into_values().filter(|v| v.len() > 1).collect() -} - -/// Parse a variant name "Size=Large, State=Hover" into pairs. -pub fn variant_props(name: &str) -> BTreeMap { - name.split(',') - .filter_map(|kv| { - let (k, v) = kv.split_once('=')?; - Some((k.trim().to_string(), v.trim().to_string())) - }) - .collect() -} - -/// Format variant pairs as a layer name, in the given property order (others after, sorted). -pub fn variant_name(pairs: &BTreeMap, order: &[String]) -> String { - let mut keys: Vec<&String> = order.iter().filter(|k| pairs.contains_key(*k)).collect(); - keys.extend(pairs.keys().filter(|k| !order.contains(k))); - keys.iter().filter_map(|k| pairs.get(*k).map(|v| format!("{k}={v}"))).collect::>().join(", ") -} - -/// Variant properties of a set: property → values, in first-seen order. -pub fn variant_options(doc: &Document, set: NodeId) -> Vec<(String, Vec)> { - let mut out: Vec<(String, Vec)> = Vec::new(); - for c in doc.node(set).map(|s| s.children.clone()).unwrap_or_default() { - let Some(cn) = doc.node(c) else { continue }; - for kv in cn.name.split(',') { - let Some((k, v)) = kv.split_once('=') else { continue }; - let (k, v) = (k.trim().to_string(), v.trim().to_string()); - match out.iter_mut().find(|(p, _)| *p == k) { - Some((_, vals)) => { - if !vals.contains(&v) { - vals.push(v); - } - } - None => out.push((k, vec![v])), - } - } - } - out -} - -/// The variant in `set` matching `wanted` best: all given properties equal, or else the most. -pub fn find_variant(doc: &Document, set: NodeId, wanted: &BTreeMap) -> Option { - let s = doc.node(set)?; - let score = |c: &NodeId| doc.node(*c).map(|n| variant_props(&n.name)).map(|p| wanted.iter().filter(|(k, v)| p.get(*k) == Some(*v)).count()); - if let Some(exact) = s.children.iter().copied().find(|c| score(c) == Some(wanted.len())) { - return Some(exact); - } - s.children.iter().copied().filter(|c| score(c).is_some()).max_by_key(|c| score(c).unwrap_or(0)) -} - -/// Check that every instance mirrors its main's layer structure (tests and diagnostics): each -/// layer's `source` is the main layer at the same place, swapped nested instances mirror their -/// own main, slots holding the instance's own content aren't compared. Returns the problems found. -pub fn mirror_issues(doc: &Document) -> Vec { - doc.nodes() - .filter(|n| matches!(n.kind, NodeKind::Instance { .. }) && !inside_instance(doc, n.id)) - .flat_map(|n| instance_issues(doc, n.id)) - .collect() -} - -/// [`mirror_issues`] for one instance. -fn instance_issues(doc: &Document, id: NodeId) -> Vec { - fn walk(doc: &Document, inst: &Node, main: &Node, depth: usize, out: &mut Vec) { - if depth > MAX_DEPTH || holds_content(inst) { - return; - } - if inst.children.len() != main.children.len() { - out.push(format!("{} has {} layers, its main {} has {}", inst.id, inst.children.len(), main.id, main.children.len())); - return; - } - for (a, b) in inst.children.iter().zip(&main.children) { - let (Some(an), Some(bn)) = (doc.node(*a), doc.node(*b)) else { continue }; - if an.source != Some(bn.id) { - out.push(format!("{} mirrors {:?}, expected {}", an.id, an.source, bn.id)); - continue; - } - match (&an.kind, &bn.kind) { - (NodeKind::Instance { component: x, .. }, NodeKind::Instance { component: y, .. }) if x != y => { - if let Some(m) = doc.node(*x) { - walk(doc, an, m, depth + 1, out); - } - } - _ => walk(doc, an, bn, depth + 1, out), - } - } - } - let mut out = Vec::new(); - if let Some(n) = doc.node(id) - && let NodeKind::Instance { component, .. } = &n.kind - && let Some(m) = doc.node(*component) - { - walk(doc, n, m, 0, &mut out); - } - out -} - #[cfg(test)] #[path = "components_tests.rs"] pub(crate) mod tests; - -/// The nested instance copies inside instance `id` whose main-component counterpart is exposed -/// (top-most first; an exposed instance's own exposed nested instances follow it). -pub fn exposed_copies(doc: &Document, id: NodeId) -> Vec { - let mut out = Vec::new(); - let mut stack: Vec = doc.node(id).map(|n| n.children.iter().rev().copied().collect()).unwrap_or_default(); - let mut seen = 0usize; - while let Some(c) = stack.pop() { - seen += 1; - if seen > 100_000 { - break; - } - let Some(n) = doc.node(c) else { continue }; - // A hidden exposed instance hides its properties too. - let exposed = - n.visible && matches!(n.kind, NodeKind::Instance { .. }) && n.source.and_then(|src| doc.node(src)).is_some_and(|src| src.exposed); - if exposed { - out.push(c); - } - // Only exposed nested instances pass their own exposed instances up. - if !matches!(n.kind, NodeKind::Instance { .. }) || exposed { - stack.extend(n.children.iter().rev().copied()); - } - } - out -} diff --git a/crates/engine/src/assets/components/diff.rs b/crates/engine/src/assets/components/diff.rs new file mode 100644 index 00000000..bf742fed --- /dev/null +++ b/crates/engine/src/assets/components/diff.rs @@ -0,0 +1,218 @@ +use super::instances::PROP_BINDING; +use super::*; + +/// The property groups whose value layer `n` takes from the same variables as its main layer +/// `src` (they differ only where the two resolve in different variable modes). +pub fn variable_groups(n: &Node, src: &Node) -> Vec<&'static str> { + let mut out = Vec::new(); + for (k, v) in &n.bound { + let Some(g) = binding_group(k) else { continue }; + if src.bound.get(k) == Some(v) && !out.contains(&g) { + out.push(g); + } + } + out +} + +/// The override group a variable binding key feeds (`None` for component property and layout +/// grid bindings, which follow other rules). +pub(super) fn binding_group(key: &str) -> Option<&'static str> { + use crate::cmd::lists::{BoundList, list_binding}; + if key.starts_with(PROP_BINDING) { + return None; + } + match list_binding(key).map(|(l, _, _)| l) { + Some(BoundList::Fills) => Some("fills"), + Some(BoundList::Strokes) => Some("strokes"), + Some(BoundList::Effects) => Some("effects"), + Some(BoundList::Grids) => None, + None => Some(crate::cmd::props::override_group(key)).filter(|g| *g != "other"), + } +} + +/// The variable bindings of `n` that feed group `g`. +pub(super) fn bindings_of<'a>(n: &'a Node, g: &'a str) -> impl Iterator + 'a { + n.bound.iter().filter(move |(k, _)| binding_group(k) == Some(g)) +} + +/// Property groups that differ between two versions of a node. +pub fn diff_groups(a: &Node, b: &Node) -> Vec<&'static str> { + let mut out = Vec::new(); + if a.fills != b.fills { + out.push("fills"); + } + if a.strokes != b.strokes || a.stroke != b.stroke { + out.push("strokes"); + } + if a.effects != b.effects { + out.push("effects"); + } + if a.opacity != b.opacity || a.blend != b.blend { + out.push("opacity"); + } + if a.visible != b.visible { + out.push("visible"); + } + if a.name != b.name { + out.push("name"); + } + if a.corners != b.corners { + out.push("corners"); + } + if (a.width - b.width).abs() > 1e-6 || (a.height - b.height).abs() > 1e-6 { + out.push("size"); + } + if resizing(a) != resizing(b) { + out.push("resizing"); + } + if a.transform != b.transform { + out.push("position"); + } + if let (NodeKind::Text(x), NodeKind::Text(y)) = (&a.kind, &b.kind) { + if x.characters != y.characters { + out.push("characters"); + } + if text_style_differs(x, y) { + out.push("textStyle"); + } + } + if let (Some(x), Some(y)) = (&a.layout, &b.layout) { + if x.padding != y.padding { + out.push("padding"); + } + if x.gap != y.gap || x.cross_gap != y.cross_gap { + out.push("gap"); + } + if x.main_align != y.main_align || x.cross_align != y.cross_align || x.content_align != y.content_align { + out.push("alignment"); + } + } + if a.grids != b.grids || a.styles.grid != b.styles.grid { + out.push("grids"); + } + if a.exports != b.exports { + out.push("exports"); + } + if a.interactions != b.interactions { + out.push("interactions"); + } + if a.var_modes != b.var_modes { + out.push("modes"); + } + if let (NodeKind::Instance { component: x, props: px }, NodeKind::Instance { component: y, props: py }) = (&a.kind, &b.kind) { + if x != y { + out.push("component"); + } + if px != py { + out.push("props"); + } + } + out +} + +/// The `resizing` group of a layer: how it sizes on each axis, and its min and max sizes. +fn resizing(n: &Node) -> (Sizing, Sizing, [Option; 4]) { + let c = &n.child_layout; + (c.sizing_h, c.sizing_v, [c.min_w, c.max_w, c.min_h, c.max_h]) +} + +/// Whether two texts differ in anything but their characters (the `textStyle` group). +fn text_style_differs(x: &TextData, y: &TextData) -> bool { + x.style != y.style + || x.runs != y.runs + || x.auto_resize != y.auto_resize + || x.align_h != y.align_h + || x.align_v != y.align_v + || x.paragraph_spacing != y.paragraph_spacing + || x.paragraph_indent != y.paragraph_indent + || x.list != y.list + || x.paragraphs != y.paragraphs + || x.list_spacing != y.list_spacing + || x.hanging_punctuation != y.hanging_punctuation + || x.hanging_list != y.hanging_list + || x.max_lines != y.max_lines + || x.features != y.features +} + +/// Whether a geometry change of `id` comes from layout rather than the user: an ancestor was +/// resized in the same edit (constraints), the parent is an auto layout frame, or auto-sized text. +pub(super) fn geometry_derived(doc: &Document, before: &Document, id: NodeId, group: &str) -> bool { + for a in doc.ancestors(id) { + let (Some(x), Some(y)) = (doc.node(a), before.node(a)) else { continue }; + if (x.width - y.width).abs() > 1e-6 || (x.height - y.height).abs() > 1e-6 { + return true; + } + } + let Some(n) = doc.node(id) else { return true }; + if group == "position" && n.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.layout.is_some()) && !n.child_layout.absolute { + return true; + } + if group == "size" + && let NodeKind::Text(t) = &n.kind + { + let wchanged = before.node(id).is_some_and(|b| (b.width - n.width).abs() > 1e-6); + return match t.auto_resize { + TextAutoResize::WidthAndHeight => true, + TextAutoResize::Height => !wchanged, + _ => false, + }; + } + false +} + +/// The components sharing the properties of component `c`: the variants of its set, or `c` alone. +pub fn property_owners(doc: &Document, c: NodeId) -> Vec { + match variant_set(doc, c).and_then(|s| doc.node(s)) { + Some(set) => set.children.iter().copied().filter(|v| doc.node(*v).is_some_and(|n| matches!(n.kind, NodeKind::Component { .. }))).collect(), + None => vec![c], + } +} + +/// Make `value` the default of boolean property `name` on components `comps`, showing or hiding +/// their layers bound to it: on a main component, a boolean property's value is the visibility of +/// its layers. Returns the components and layers it changed. +pub fn set_boolean_default(doc: &mut Document, comps: &[NodeId], name: &str, value: bool) -> Vec { + let mut out = Vec::new(); + for &c in comps { + let Some(NodeKind::Component { props, .. }) = doc.node(c).map(|n| &n.kind) else { continue }; + let Some(PropDef::Boolean { default }) = props.get(name) else { continue }; + if *default != value + && let Some(NodeKind::Component { props, .. }) = doc.node_mut(c).map(|n| &mut n.kind) + { + props.insert(name.to_string(), PropDef::Boolean { default: value }); + out.push(c); + } + // The main's own layers (not those of instances nested in it, which follow their mains). + let layers: Vec = doc + .subtree(c) + .into_iter() + .filter(|d| *d != c && !inside_instance(doc, *d)) + .filter(|d| doc.node(*d).is_some_and(|n| n.visible != value && n.prop_refs.get("visible").map(String::as_str) == Some(name))) + .collect(); + for d in layers { + if let Some(n) = doc.node_mut(d) { + n.visible = value; + out.push(d); + } + } + } + out +} + +/// A main component's layer bound to a boolean property was shown or hidden: that is the +/// property's new default, which its other bound layers (in every variant) take too. Returns the +/// components and layers that changed as a result. +pub(super) fn follow_boolean_defaults(doc: &mut Document, before: &Document, changed: &[NodeId]) -> Vec { + let mut out = Vec::new(); + for &id in changed { + let (Some(n), Some(b)) = (doc.node(id), before.node(id)) else { continue }; + if n.visible == b.visible || inside_instance(doc, id) { + continue; + } + let Some(name) = n.prop_refs.get("visible").cloned() else { continue }; + let visible = n.visible; + let Some(c) = owning_component(doc, id).filter(|c| *c != id) else { continue }; + out.extend(set_boolean_default(doc, &property_owners(doc, c), &name, visible)); + } + out +} diff --git a/crates/engine/src/assets/components/guard.rs b/crates/engine/src/assets/components/guard.rs new file mode 100644 index 00000000..b320b7c4 --- /dev/null +++ b/crates/engine/src/assets/components/guard.rs @@ -0,0 +1,135 @@ +use super::diff::geometry_derived; +use super::rebase::rebase_instance; +use super::variants::instance_issues; +use super::*; + +/// Slot `slot` (inside an instance) takes the instance's own content: the layers it shows (the +/// main's default content until now) stop mirroring the main, and instances among them stand +/// alone, linked to their own mains. +pub(super) fn own_slot_content(doc: &mut Document, slot: NodeId) { + if let Some(n) = doc.node_mut(slot) { + n.overrides.insert(SLOT.into()); + } + for nested in unlink_layers(doc, slot, None) { + rebase_instance(doc, nested); + } +} + +/// Unlink the layers below `top` that are layers of `owner` (an instance, or `None` for the +/// content of a slot): plain layers forget the main layer they mirrored and their property +/// bindings. The instances among them are returned, to re-link to their own mains (with +/// [`rebase_instance`]) once the caller is done. +pub fn unlink_layers(doc: &mut Document, top: NodeId, owner: Option) -> Vec { + let picked: Vec<(NodeId, bool)> = + doc.subtree(top).into_iter().filter(|d| *d != top && enclosing_instance(doc, *d) == owner).map(|d| (d, is_instance(doc, d))).collect(); + let mut nested = Vec::new(); + for (d, instance) in picked { + let Some(n) = doc.node_mut(d) else { continue }; + n.prop_refs.clear(); + if instance { + nested.push(d); + } else { + n.source = None; + n.overrides.clear(); + } + } + nested +} + +/// A new layer copied from inside an instance (pasted, duplicated) to where no instance owns it +/// stops mirroring the main; a copied nested instance is re-linked to its own main (its look kept). +pub(super) fn relink_copy(doc: &mut Document, id: NodeId) { + let Some(n) = doc.node(id) else { return }; + match &n.kind { + NodeKind::Instance { component, .. } => { + let main_ok = doc.node(*component).is_some_and(|m| matches!(m.kind, NodeKind::Component { .. })); + if main_ok && !inside_instance(doc, id) && (n.source != Some(*component) || !instance_issues(doc, id).is_empty()) { + rebase_instance(doc, id); + } + } + _ => { + if (n.source.is_some() || !n.overrides.is_empty()) + && owning_instance(doc, id).is_none() + && let Some(m) = doc.node_mut(id) + { + m.source = None; + m.overrides.clear(); + } + } + } +} + +/// Refuse edits that add, remove or reorder layers inside an instance (only its main can), outside +/// its slots. +pub fn check_structure_edit(before: &Document, after: &Document) -> Result<(), String> { + for &id in after.dirty() { + let (Some(n), Some(b)) = (after.node(id), before.node(id)) else { continue }; + // Library copies change only through library updates (and are read-only otherwise). + if n.children == b.children || after.is_remote(id) { + continue; + } + if structure_locked(before, id) && structure_locked(after, id) { + let name = owning_instance(after, id).and_then(|i| after.node(i)).map(|i| i.name.clone()).unwrap_or_default(); + return Err(format!( + "layers can't be added, removed or reordered inside the instance \"{name}\": edit its main component, or detach the instance first" + )); + } + } + Ok(()) +} + +/// Refuse edits an instance doesn't take as overrides ([`refused_override`]); the edit is rolled +/// back. +pub fn check_override_edit(before: &Document, after: &Document) -> Result<(), String> { + match after.dirty().iter().find_map(|id| refused_override(before, after, *id)) { + Some(what) => Err(format!("{what} comes from the main component in an instance: edit the main component, or detach the instance")), + None => Ok(()), + } +} + +/// What about layer `id` an edit from `before` to `after` changed that only its main can: the +/// auto layout of an instance or a layer in one but its padding, gap and alignment, or the +/// position (rotation too), constraints or place in auto layout (absolute, grid cell) of a layer +/// inside an instance (its size and resizing are overrides). `None` when the edit is fine, also +/// when the main changed the same way in this edit (a library update). +pub fn refused_override(before: &Document, after: &Document, id: NodeId) -> Option<&'static str> { + let (n, b) = (after.node(id)?, before.node(id)?); + if after.is_remote(id) || n.source != b.source || owning_instance(before, id).is_none() || owning_instance(after, id).is_none() { + return None; + } + let (mb, ma) = (before.node(n.source?)?, after.node(n.source?)?); + let mode = |x: &Node| x.layout.as_ref().map(|l| l.mode); + // Auto layout settings other than the overridable ones. + let settings = |x: &Node| { + x.layout.clone().map(|mut l| { + (l.gap, l.cross_gap, l.padding, l.main_align, l.cross_align) = (0.0, 0.0, [0.0; 4], MainAlign::Min, CrossAlign::Min); + l.content_align = ContentAlign::Auto; + l + }) + }; + // Child layout settings other than the resizing ones. + let placing = |x: &Node| { + let mut c = x.child_layout.clone(); + (c.sizing_h, c.sizing_v, c.min_w, c.max_w, c.min_h, c.max_h) = (Sizing::Fixed, Sizing::Fixed, None, None, None, None); + c + }; + if mode(n) != mode(b) && mode(ma) == mode(mb) { + return Some("The auto layout direction"); + } + if settings(n) != settings(b) && settings(ma) == settings(mb) { + return Some("This auto layout setting"); + } + if !inside_instance(after, id) || !inside_instance(before, id) { + return None; + } + if n.transform != b.transform && ma.transform == mb.transform && !geometry_derived(after, before, id, "position") { + return Some("A layer's position"); + } + if n.constraints != b.constraints && ma.constraints == mb.constraints { + return Some("A layer's constraints"); + } + if placing(n) != placing(b) && placing(ma) == placing(mb) { + return Some("A layer's place in auto layout"); + } + None +} diff --git a/crates/engine/src/assets/components/instances.rs b/crates/engine/src/assets/components/instances.rs new file mode 100644 index 00000000..9fd4e2c2 --- /dev/null +++ b/crates/engine/src/assets/components/instances.rs @@ -0,0 +1,342 @@ +use super::diff::{binding_group, bindings_of}; +use super::sync::{insert_linked, sync_instance_depth}; +use super::*; + +/// Create an instance of `component` under `parent` at `(x, y)` (parent space). +pub fn create_instance(doc: &mut Document, component: NodeId, parent: NodeId, x: f64, y: f64) -> Result { + create_instance_at(doc, component, parent, x, y, None) +} + +/// [`create_instance`] at child `index` of `parent` (None: on top). +pub fn create_instance_at(doc: &mut Document, component: NodeId, parent: NodeId, x: f64, y: f64, index: Option) -> Result { + let comp = doc.get(component)?.clone(); + if !matches!(comp.kind, NodeKind::Component { .. }) { + return Err(DocError::Invalid(format!("{component} is not a component"))); + } + if doc.is_ancestor(component, parent) { + return Err(DocError::Invalid("an instance can't be placed inside its own main component".into())); + } + let id = doc.alloc_id(); + let mut root = comp.clone(); + root.id = id; + root.name = default_instance_name(doc, component); + root.kind = NodeKind::Instance { component, props: default_props(&comp) }; + root.children.clear(); + root.parent = None; + root.source = Some(component); + root.overrides.clear(); + root.transform = Affine::translate((x, y)); + root.prop_refs.clear(); + root.annotations.clear(); + root.code_links.clear(); + // The main's dev resources show on its instances without being copied. + root.dev_resources.clear(); + // Measurements belong to the layer they were placed on. + root.measurements.clear(); + // The main's interactions play on its instances in prototypes; the instance's own start empty. + root.interactions.clear(); + // So do the variable modes the main sets (see `Document::inherited_modes`). + root.var_modes.clear(); + doc.insert(root, parent, index)?; + sync_instance(doc, id); + Ok(id) +} + +/// Copy `id` within its own file under `parent` (at `index`), keeping its local transform. A file +/// has one main per component, so a main component's copy is an instance of it; a variant copied +/// into its set stays a new variant, and every other layer is deep-copied. +pub fn copy_in_file(doc: &mut Document, id: NodeId, parent: NodeId, index: Option) -> Result { + let n = doc.get(id)?; + let in_set = doc.node(parent).is_some_and(|p| matches!(p.kind, NodeKind::ComponentSet { .. })); + if !matches!(n.kind, NodeKind::Component { .. }) || in_set { + return doc.duplicate_into(id, parent, index); + } + let transform = n.transform; + let instance = create_instance_at(doc, id, parent, 0.0, 0.0, index)?; + doc.get_mut(instance)?.transform = transform; + Ok(instance) +} + +/// Which part of the carried overrides to apply. +#[derive(Clone, Copy, PartialEq)] +pub(super) enum Carry { + /// Paint, text, visibility, … (everything but geometry). + Values, + /// Values plus size (instance roots: their size decides how children follow constraints). + Root, + /// Size and position (children, after constraints ran). + Geometry, +} + +/// Copy the overridden property groups of `from` onto `to`. +pub(super) fn carry_overrides(from: &Node, to: &mut Node, what: Carry) { + for g in &from.overrides { + let geo = matches!(g.as_str(), "size" | "position"); + match what { + Carry::Values if geo => continue, + Carry::Root if g == "position" => continue, + Carry::Geometry if !geo => continue, + _ => {} + } + copy_group(from, to, g); + } + if what != Carry::Geometry { + to.overrides = from.overrides.clone(); + } +} + +/// An instance switched from variant `was` to `new`: its resizing (sizing and min / max per axis) +/// that is still `was`'s follows `new`; what the instance set itself stays in `own`. +pub(super) fn follow_variant_resizing(own: &mut ChildLayout, was: &ChildLayout, new: &ChildLayout) { + if own.sizing_h == was.sizing_h { + own.sizing_h = new.sizing_h; + } + if own.sizing_v == was.sizing_v { + own.sizing_v = new.sizing_v; + } + for (o, w, n) in [ + (&mut own.min_w, was.min_w, new.min_w), + (&mut own.max_w, was.max_w, new.max_w), + (&mut own.min_h, was.min_h, new.min_h), + (&mut own.max_h, was.max_h, new.max_h), + ] { + if *o == w { + *o = n; + } + } +} + +/// Copy the values of property group `g` of `from` onto `to`, with the variables bound to them. +pub(crate) fn copy_group(from: &Node, to: &mut Node, g: &str) { + if from.bound.iter().chain(to.bound.iter()).any(|(k, _)| binding_group(k) == Some(g)) { + to.bound.retain(|k, _| binding_group(k) != Some(g)); + to.bound.extend(bindings_of(from, g).map(|(k, v)| (k.clone(), v.clone()))); + } + match g { + "fills" => { + to.fills = from.fills.clone(); + to.styles.fill = from.styles.fill.clone(); + } + "strokes" => { + to.strokes = from.strokes.clone(); + to.stroke = from.stroke.clone(); + to.styles.stroke = from.styles.stroke.clone(); + } + "effects" => { + to.effects = from.effects.clone(); + to.styles.effect = from.styles.effect.clone(); + } + "opacity" => { + to.opacity = from.opacity; + to.blend = from.blend; + } + "visible" => to.visible = from.visible, + "name" => to.name = from.name.clone(), + "corners" => to.corners = from.corners, + "characters" => { + if let (NodeKind::Text(a), NodeKind::Text(b)) = (&from.kind, &mut to.kind) { + b.characters = a.characters.clone(); + b.runs = a.runs.clone(); + b.paragraphs = a.paragraphs.clone(); + } + } + "textStyle" => { + if let (NodeKind::Text(a), NodeKind::Text(b)) = (&from.kind, &mut to.kind) { + b.style = a.style.clone(); + b.runs = a.runs.clone(); + b.auto_resize = a.auto_resize; + b.align_h = a.align_h; + b.align_v = a.align_v; + b.paragraph_spacing = a.paragraph_spacing; + b.paragraph_indent = a.paragraph_indent; + b.list = a.list; + b.paragraphs = a.paragraphs.clone(); + b.list_spacing = a.list_spacing; + b.hanging_punctuation = a.hanging_punctuation; + b.hanging_list = a.hanging_list; + b.max_lines = a.max_lines; + b.features = a.features.clone(); + } + to.styles.text = from.styles.text.clone(); + } + "padding" | "gap" | "alignment" => { + if let (Some(a), Some(b)) = (&from.layout, &mut to.layout) { + match g { + "padding" => b.padding = a.padding, + "gap" => (b.gap, b.cross_gap) = (a.gap, a.cross_gap), + _ => (b.main_align, b.cross_align, b.content_align) = (a.main_align, a.cross_align, a.content_align), + } + } + } + "grids" => { + to.grids = from.grids.clone(); + to.styles.grid = from.styles.grid.clone(); + } + "exports" => to.exports = from.exports.clone(), + "interactions" => to.interactions = from.interactions.clone(), + "size" => { + to.width = from.width; + to.height = from.height; + if let (NodeKind::Text(a), NodeKind::Text(b)) = (&from.kind, &mut to.kind) { + b.auto_resize = a.auto_resize; + } + } + "resizing" => { + let (a, b) = (&from.child_layout, &mut to.child_layout); + (b.sizing_h, b.sizing_v, b.min_w, b.max_w, b.min_h, b.max_h) = (a.sizing_h, a.sizing_v, a.min_w, a.max_w, a.min_h, a.max_h); + } + "position" => to.transform = from.transform, + "modes" => to.var_modes = from.var_modes.clone(), + "component" => { + if let (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) = (&from.kind, &mut to.kind) { + *b = *a; + } + } + "props" => { + if let (NodeKind::Instance { props: a, .. }, NodeKind::Instance { props: b, .. }) = (&from.kind, &mut to.kind) { + *b = a.clone(); + } + carry_prop_bindings(from, to); + } + _ => {} + } +} + +/// Layer name paths below `root` ("Frame/Label", with `#n` for same-named siblings). With +/// `src_names` (an instance's layers), layers are named after the main layer they mirror (renames +/// don't break matching), and the content of slots holding the instance's own is left out. +pub(super) fn path_map(doc: &Document, root: NodeId, src_names: bool) -> BTreeMap { + let mut out = BTreeMap::new(); + let mut stack = vec![(root, String::new())]; + while let Some((id, prefix)) = stack.pop() { + let Some(n) = doc.node(id) else { continue }; + let mut seen: BTreeMap<&str, usize> = BTreeMap::new(); + for c in &n.children { + let Some(cn) = doc.node(*c) else { continue }; + // A nested instance linked only to its own component so far (an imported one) mirrors no + // layer yet: it goes by its own name, like the main's layer it will be matched with. + let unplaced = matches!(&cn.kind, NodeKind::Instance { component, .. } if cn.source == Some(*component)); + let mirrored = cn.source.filter(|_| src_names && !unplaced).and_then(|s| doc.node(s)); + let name = mirrored.map_or(cn.name.as_str(), |s| s.name.as_str()); + let k = seen.entry(name).or_insert(0); + let seg = if *k == 0 { name.to_string() } else { format!("{name}#{k}") }; + *k += 1; + let path = if prefix.is_empty() { seg } else { format!("{prefix}/{seg}") }; + out.insert(*c, path.clone()); + if out.len() > 1_000_000 { + return out; + } + if !(src_names && holds_content(cn)) { + stack.push((*c, path)); + } + } + } + out +} + +/// The binding key prefix of an instance's component property value +/// (`componentProperties/`): a boolean property takes a boolean variable, a text property a +/// string variable. +pub const PROP_BINDING: &str = "componentProperties/"; + +/// Give `to` the component property bindings of instance `from` (they go with its property +/// values, which an instance keeps across rebuilds). +pub(super) fn carry_prop_bindings(from: &Node, to: &mut Node) { + to.bound.retain(|k, _| !k.starts_with(PROP_BINDING)); + to.bound.extend(from.bound.iter().filter(|(k, _)| k.starts_with(PROP_BINDING)).map(|(k, v)| (k.clone(), v.clone()))); +} + +/// The override group a property-bound field belongs to. +pub(super) fn field_group(field: &str) -> &'static str { + match field { + "visible" => "visible", + "characters" => "characters", + "mainComponent" => "component", + _ => "", + } +} + +/// Apply one component-property value to a layer. +pub(super) fn apply_prop(doc: &Document, n: &mut Node, field: &str, val: &Value) { + match field { + "visible" => { + if let Some(b) = val.as_bool() { + n.visible = b; + } + } + "characters" => { + if let (NodeKind::Text(t), Some(sv)) = (&mut n.kind, val.as_str().map(crate::cmd::props::normalize_line_endings)) + && t.characters != sv + { + let len = t.char_count(); + t.replace_range(0, len, &sv); + } + } + "mainComponent" => { + if let (NodeKind::Instance { component: c, .. }, Some(v)) = (&mut n.kind, crate::params::id_of(val)) + && doc.node(v).is_some_and(|m| matches!(m.kind, NodeKind::Component { .. })) + { + *c = v; + } + } + _ => {} + } +} + +/// Rebuild instance `inst` from its main component, keeping ids, overrides and the content of its +/// slots. An instance whose main component was deleted keeps its layers as they are. +pub fn sync_instance(doc: &mut Document, inst: NodeId) { + // Layers the rebuild leaves as they were keep their shared snapshot (and aren't edits). + let old: Vec<(std::sync::Arc, bool)> = + doc.subtree(inst).into_iter().filter_map(|i| doc.node_arc(i).map(|a| (a.clone(), doc.dirty().contains(&i)))).collect(); + let kept = slot_content(doc, inst); + sync_instance_depth(doc, inst, 0, BTreeMap::new()); + restore_slot_content(doc, kept); + for (a, was_dirty) in old { + doc.keep_if_unchanged(a, was_dirty); + } +} + +/// The slots in instance `inst` (nested instances included) holding the instance's own content: +/// each with its content (the subtrees of its children, pre-order). +fn slot_content(doc: &Document, inst: NodeId) -> Vec<(NodeId, Vec)> { + let mut out = Vec::new(); + let mut stack: Vec = doc.node(inst).map(|n| n.children.clone()).unwrap_or_default(); + while let Some(c) = stack.pop() { + let Some(n) = doc.node(c) else { continue }; + if holds_content(n) { + let nodes = n.children.iter().flat_map(|k| doc.subtree(*k)).filter_map(|d| doc.node(d).cloned()).collect(); + out.push((c, nodes)); + } else { + stack.extend(n.children.iter().copied()); + } + } + out +} + +/// Put slot content back after a rebuild, into the slots that still hold the instance's own +/// content (a slot the main no longer has, or no longer marks as a slot, loses it). Constraints +/// and geometry overrides ran before, so the content keeps its place. +fn restore_slot_content(doc: &mut Document, kept: Vec<(NodeId, Vec)>) { + for (slot, nodes) in kept { + let keep = slot_edited(doc, slot) && is_slot(doc, slot) && nodes.iter().all(|n| !doc.contains(n.id)); + if !keep { + continue; + } + let ids: Vec = nodes.iter().map(|n| n.id).collect(); + for n in nodes { + // Pre-order: every parent is back before its children. + let _ = insert_linked(doc, n); + } + // Never leave links to a layer that couldn't come back. + for id in ids.into_iter().chain([slot]) { + let dangling = doc.node(id).is_some_and(|n| n.children.iter().any(|c| !doc.contains(*c))); + if dangling { + let live: Vec = doc.node(id).map(|n| n.children.iter().copied().filter(|c| doc.contains(*c)).collect()).unwrap_or_default(); + if let Some(n) = doc.node_mut(id) { + n.children = live; + } + } + } + } +} diff --git a/crates/engine/src/assets/components/patch.rs b/crates/engine/src/assets/components/patch.rs new file mode 100644 index 00000000..8c61f0b8 --- /dev/null +++ b/crates/engine/src/assets/components/patch.rs @@ -0,0 +1,528 @@ +use super::diff::{binding_group, bindings_of, diff_groups, follow_boolean_defaults, geometry_derived, variable_groups}; +use super::guard::{own_slot_content, relink_copy}; +use super::instances::{copy_group, field_group, sync_instance}; +use super::sync::insert_linked; +use super::*; + +/// After an edit: record overrides inside instances, then rebuild instances whose main changed, +/// whose properties or main were switched, or whose overrides were reset. +pub fn sync_after_edit(doc: &mut Document, before: &Document, changed: &[NodeId], touched: &BTreeSet) { + let mut changed = changed.to_vec(); + for id in follow_boolean_defaults(doc, before, &changed) { + if !changed.contains(&id) { + changed.push(id); + } + } + let changed = changed.as_slice(); + // 0. A slot whose layers were added, removed or reordered holds the instance's own content + // from now on; copies of instance layers placed elsewhere stop mirroring their main. + for &id in changed { + let (Some(n), Some(b)) = (doc.node(id), before.node(id)) else { continue }; + if n.children == b.children || owning_instance(doc, id).is_none() { + continue; + } + if let Some(slot) = slot_scope(doc, id) + && !slot_edited(doc, slot) + { + own_slot_content(doc, slot); + } + } + for &id in changed { + if !before.contains(id) { + relink_copy(doc, id); + } + } + let mut resync: BTreeSet = BTreeSet::new(); + // 1. Overrides for layers edited inside instances. + for &id in changed { + let (Some(n), Some(b)) = (doc.node(id), before.node(id)) else { continue }; + let Some(inst) = owning_instance(doc, id) else { continue }; + let top_root = inst == id && !inside_instance(doc, id); + if top_root + && let (NodeKind::Instance { component: c1, props: p1 }, NodeKind::Instance { component: c2, props: p2 }) = (&n.kind, &b.kind) + && (c1 != c2 || p1 != p2) + { + // Switching the main or a property value of an instance isn't an override. + resync.insert(id); + } + let raw = diff_groups(b, n); + let mut groups = raw.clone(); + // An instance's own place, resizing, export settings, interactions and variable modes are + // its own, not overrides. + if top_root { + groups.retain(|g| !matches!(*g, "component" | "props" | "position" | "resizing" | "exports" | "interactions" | "modes")); + } + if groups.iter().any(|g| matches!(*g, "size" | "position")) { + groups.retain(|g| !matches!(*g, "size" | "position") || !geometry_derived(doc, before, id, g)); + } + // A value equal to the main's isn't an override (setting it back clears the override). + let mut cleared = Vec::new(); + if let Some(src) = n.source.and_then(|s| doc.node(s)) { + let mut differs = diff_groups(src, n); + if inst == id { + differs.retain(|g| *g != "name"); + if n.name != default_instance_name(doc, src.id) { + differs.push("name"); + } + } + // A value from the same variable as the main's (resolved in this layer's modes) isn't an + // override either. + let from_variables = variable_groups(n, src); + groups.retain(|g| differs.contains(g) && !from_variables.contains(g)); + // Binding another variable (or none) where the main binds one is an override. + for (k, _) in b.bound.iter().chain(n.bound.iter()) { + let Some(g) = binding_group(k) else { continue }; + let rebound = b.bound.get(k) != n.bound.get(k) && !bindings_of(n, g).eq(bindings_of(src, g)); + if rebound && !groups.contains(&g) { + groups.push(g); + } + } + cleared = + raw.iter().filter(|g| !matches!(**g, "size" | "position") && !differs.contains(g) && n.overrides.contains(**g)).copied().collect(); + if top_root { + cleared.retain(|g| !matches!(*g, "component" | "props")); + } + } + let lost = b.overrides.iter().any(|g| !n.overrides.contains(g)); + let nested_switch = !top_root && raw.iter().any(|g| matches!(*g, "component" | "props")); + if (!groups.is_empty() || !cleared.is_empty()) + && let Some(m) = doc.node_mut(id) + { + for g in groups { + m.overrides.insert(g.to_string()); + } + for g in cleared { + m.overrides.remove(g); + } + } + if (lost || nested_switch) + && let Some(r) = root_instance(doc, id) + { + resync.insert(r); + } + } + // 2. Components whose layers changed, were added or removed. Value-only edits (paint, text, + // visibility, …) are carried to the copies directly; anything else rebuilds the instances. + let mut comps: BTreeSet = BTreeSet::new(); + let mut patches: Vec<(NodeId, Vec<&'static str>)> = Vec::new(); + // Main layers that only gained new layers or lost some: their copies take the same change. + let mut restructured: Vec = Vec::new(); + let mut covered: BTreeSet = BTreeSet::new(); + for &id in changed { + if let (Some(b), Some(n)) = (before.node(id), doc.node(id)) + && b.children != n.children + && owning_instance(doc, id).is_none() + && owning_component(doc, id).is_some() + && let Some(layers) = restructurable(doc, before, b, n) + { + restructured.push(id); + covered.extend(layers); + } + } + for &id in changed { + if covered.contains(&id) || restructured.contains(&id) { + continue; + } + let Some(c) = owning_component(doc, id) else { continue }; + let (Some(b), Some(n)) = (before.node(id), doc.node(id)) else { + comps.insert(c); + continue; + }; + if b == n { + continue; + } + match patchable(doc, b, n) { + Some(groups) => patches.push((id, groups)), + None => { + comps.insert(c); + } + } + } + for &id in touched { + if !doc.contains(id) + && !covered.contains(&id) + && let Some(c) = owning_component(before, id) + && doc.contains(c) + { + comps.insert(c); + } + } + for r in resync { + if doc.contains(r) { + sync_instance(doc, r); + if let Some(c) = owning_component(doc, r) { + comps.insert(c); + } + } + } + // A copy the patch can't follow exactly rebuilds instead (as does any other change). + let patched = comps.is_empty() + && !full_rebuild() + && (restructured.is_empty() || patch_structure(doc, before, &restructured)) + && (patches.is_empty() || patch_copies(doc, &patches)); + #[cfg(test)] + if patched && (!restructured.is_empty() || !patches.is_empty()) { + PATCHED.with(|c| c.set(c.get() + 1)); + } + if !patched { + comps.extend(patches.iter().map(|(id, _)| *id).chain(restructured.iter().copied()).filter_map(|id| owning_component(doc, id))); + } + // 3. Instances of changed components follow; an instance inside another component makes that + // one change too. + rebuild_instances(doc, &comps); +} + +#[cfg(test)] +thread_local! { + /// Tests: rebuild instances even for edits that could be patched (to compare both ways). + pub(crate) static FULL_REBUILD: std::cell::Cell = const { std::cell::Cell::new(false) }; + /// Tests: edits carried to the copies without rebuilding instances. + pub(crate) static PATCHED: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +fn full_rebuild() -> bool { + FULL_REBUILD.with(std::cell::Cell::get) +} + +#[cfg(not(test))] +fn full_rebuild() -> bool { + false +} + +/// The property groups of a main-component layer edited from `b` to `n` when the edit changed +/// values only (paint, text, visibility, size, position, …) that copies take as they are: carried to them without +/// rebuilding the instances ([`patch_copies`]). `None` when anything else changed. +fn patchable(doc: &Document, b: &Node, n: &Node) -> Option> { + const VALUES: [&str; 17] = [ + "fills", + "strokes", + "effects", + "opacity", + "visible", + "name", + "corners", + "characters", + "textStyle", + "padding", + "gap", + "alignment", + "grids", + "exports", + "interactions", + "size", + "position", + ]; + if b.children != n.children || b.parent != n.parent || b.kind.type_name() != n.kind.type_name() { + return None; + } + let groups = diff_groups(b, n); + if groups.is_empty() || groups.iter().any(|g| !VALUES.contains(g)) { + return None; + } + // Exactly those groups (and the layer's own overrides) changed. + let mut probe = n.clone(); + for g in &groups { + copy_group(b, &mut probe, g); + } + probe.overrides = b.overrides.clone(); + if probe != *b { + return None; + } + // Values a component property sets (on this layer or a main it mirrors) come from instances. + let mut cur = Some(n); + for _ in 0..=MAX_DEPTH { + let Some(x) = cur else { break }; + if x.prop_refs.keys().any(|f| groups.contains(&field_group(f))) { + return None; + } + cur = x.source.and_then(|s| doc.node(s)); + } + Some(groups) +} + +/// Copies by the layer they mirror; nested instances swapped to a main also follow it. +fn copies_index(doc: &Document) -> BTreeMap> { + let mut copies: BTreeMap> = BTreeMap::new(); + for n in doc.nodes() { + let Some(s) = n.source else { continue }; + copies.entry(s).or_default().push(n.id); + if let (NodeKind::Instance { component, .. }, Some(NodeKind::Instance { component: other, .. })) = (&n.kind, doc.node(s).map(|x| &x.kind)) + && component != other + { + copies.entry(*component).or_default().push(n.id); + } + } + copies +} + +/// When main layer `b` → `n` only gained new plain layers (created in this edit: no instances, +/// components or property bindings in them) or lost layers that were deleted: those layers, which +/// [`patch_structure`] carries to the copies. `None` for anything else. +fn restructurable(doc: &Document, before: &Document, b: &Node, n: &Node) -> Option> { + let mut probe = n.clone(); + probe.children = b.children.clone(); + probe.overrides = b.overrides.clone(); + if probe != *b { + return None; + } + let mut layers = Vec::new(); + for c in n.children.iter().filter(|c| !b.children.contains(c)) { + for d in doc.subtree(*c) { + let x = doc.node(d)?; + let plain = !matches!(x.kind, NodeKind::Instance { .. } | NodeKind::Component { .. } | NodeKind::ComponentSet { .. }); + if before.contains(d) || !plain || !x.prop_refs.is_empty() { + return None; + } + layers.push(d); + } + } + for c in b.children.iter().filter(|c| !n.children.contains(c)) { + if doc.contains(*c) { + return None; + } + layers.extend(before.subtree(*c)); + } + Some(layers) +} + +/// Carry the new and deleted layers of main layers `seeds` ([`restructurable`]) to every copy, +/// through copies of copies (slots holding an instance's own content keep it). Returns false +/// (leaving a partial patch for the caller to rebuild over) when a copy can't follow exactly. +fn patch_structure(doc: &mut Document, before: &Document, seeds: &[NodeId]) -> bool { + let copies = copies_index(doc); + let mut work: Vec = seeds.to_vec(); + let mut steps = 0usize; + while let Some(p) = work.pop() { + steps += 1; + if steps > 10_000_000 { + return false; + } + let Some(pn) = doc.node(p).cloned() else { continue }; + for c in copies.get(&p).cloned().unwrap_or_default() { + let Some(cn) = doc.node(c).cloned() else { continue }; + if holds_content(&cn) { + continue; + } + if let (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) = (&pn.kind, &cn.kind) + && a != b + { + continue; + } + // The copy's layers by the layer of `p` they mirror. + let mut mirrors: BTreeMap = BTreeMap::new(); + for k in &cn.children { + let Some(src) = doc.node(*k).and_then(|x| x.source) else { return false }; + if mirrors.insert(src, *k).is_some() { + return false; + } + } + let mut order = Vec::with_capacity(pn.children.len()); + for pc in &pn.children { + if let Some(k) = mirrors.remove(pc) { + order.push(k); + continue; + } + // A layer the copy lacks must be new in this edit, placed as in the main. + let absolute = doc.node(*pc).is_some_and(|x| x.child_layout.absolute); + if before.contains(*pc) || !child_follows_main(doc, c, absolute) { + return false; + } + match copy_layers(doc, *pc, c) { + Some(k) => order.push(k), + None => return false, + } + } + // Copies of layers the main lost. + for (src, k) in mirrors { + if doc.contains(src) || doc.remove(k).is_err() { + return false; + } + } + if doc.node(c).is_some_and(|x| x.children != order) + && let Some(m) = doc.node_mut(c) + { + m.children = order; + } + work.push(c); + } + } + true +} + +/// Whether a layer placed in copy `parent` (absolutely positioned or not) takes its main layer's +/// place and size: nothing on the way up applies constraints ([`follows_main_geometry`]). +fn child_follows_main(doc: &Document, parent: NodeId, absolute: bool) -> bool { + let Some(p) = doc.node(parent) else { return false }; + let constrains = p.layout.is_none() || absolute; + let resized = sized_own(p) + || match &p.kind { + NodeKind::Instance { component, .. } => { + doc.node(*component).is_none_or(|m| (m.width - p.width).abs() > 1e-9 || (m.height - p.height).abs() > 1e-9) + } + _ => false, + }; + !(constrains && resized) && follows_main_geometry(doc, parent) +} + +/// Copy main layer `src` (with its layers) into copy `parent` as layers mirroring them; returns the +/// new copy (the caller places it among `parent`'s layers). +fn copy_layers(doc: &mut Document, src: NodeId, parent: NodeId) -> Option { + let sub = doc.subtree(src); + let map: BTreeMap = sub.iter().map(|d| (*d, doc.alloc_id())).collect(); + for d in &sub { + let mut n = doc.node(*d)?.clone(); + n.id = *map.get(d)?; + n.parent = if *d == src { Some(parent) } else { n.parent.and_then(|x| map.get(&x).copied()) }; + n.children = n.children.iter().filter_map(|k| map.get(k).copied()).collect(); + n.source = Some(*d); + n.overrides.clear(); + n.prop_refs.clear(); + n.annotations.clear(); + n.measurements.clear(); + insert_linked(doc, n).ok()?; + } + map.get(&src).copied() +} + +/// Carry value-only edits of main-component layers (`seeds`: layer, groups) to every copy, through +/// copies of copies, except where a copy overrides the group. Returns false (leaving a partial +/// patch for the caller to rebuild over) when a copy can't follow exactly. +fn patch_copies(doc: &mut Document, seeds: &[(NodeId, Vec<&'static str>)]) -> bool { + let copies = copies_index(doc); + let text = |g: &str| matches!(g, "characters" | "textStyle"); + let mut work: Vec<(NodeId, Vec<&'static str>)> = seeds.to_vec(); + let mut steps = 0usize; + while let Some((p, groups)) = work.pop() { + steps += 1; + if steps > 10_000_000 { + return false; + } + let Some(pn) = doc.node(p).cloned() else { continue }; + for c in copies.get(&p).cloned().unwrap_or_default() { + let Some(cn) = doc.node(c) else { continue }; + // A swapped nested instance shows its own main, not the layer it stands in for. + if let (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) = (&pn.kind, &cn.kind) + && a != b + { + continue; + } + // An instance keeps its own visibility, place, export settings and interactions; its + // name follows the main's unless renamed; a resized one lays its layers out against + // the main's size. + let root = matches!(pn.kind, NodeKind::Component { .. }); + if root && ((groups.contains(&"name") && !cn.overrides.contains("name")) || (groups.contains(&"size") && cn.overrides.contains("size"))) { + return false; + } + let gs: Vec<&'static str> = groups + .iter() + .copied() + .filter(|g| !(cn.overrides.contains(*g) || root && matches!(*g, "visible" | "name" | "position" | "exports" | "interactions"))) + .collect(); + if gs.is_empty() { + continue; + } + let geometry = gs.iter().any(|g| matches!(*g, "size" | "position")); + if geometry && !follows_main_geometry(doc, c) { + return false; + } + // Text content and style share their runs: a copy overriding one keeps its runs when the + // other follows the main (as a rebuild carries them). + let runs = match &cn.kind { + NodeKind::Text(t) if gs.iter().any(|g| text(g)) && cn.overrides.iter().any(|g| text(g)) => Some(t.runs.clone()), + _ => None, + }; + if let Some(m) = doc.node_mut(c) { + for g in &gs { + copy_group(&pn, m, g); + } + if let (Some(r), NodeKind::Text(t)) = (runs, &mut m.kind) { + t.runs = r; + } + } + if (geometry || gs.iter().any(|g| text(g))) + && let Some((w, h)) = doc.node(c).and_then(figcraft_text::auto_size) + && doc.node(c).is_some_and(|n| (n.width - w).abs() > 1e-6 || (n.height - h).abs() > 1e-6) + { + figcraft_layout::set_size(doc, c, w, h); + } + work.push((c, gs)); + } + } + true +} + +/// Whether a copy's size and place are its main layer's: no layer around it inside its instances +/// applies constraints on the way down to it (a resized instance or frame without auto layout, or +/// a resized auto layout instance holding it absolutely positioned). Auto layout re-flows the rest. +fn follows_main_geometry(doc: &Document, id: NodeId) -> bool { + let chain = scope(doc, id); + chain.windows(2).all(|w| { + let (Some(below), Some(a)) = (w.first().and_then(|i| doc.node(*i)), w.get(1).and_then(|i| doc.node(*i))) else { return false }; + let constrains = a.layout.is_none() || below.child_layout.absolute; + if sized_own(a) && constrains { + return false; + } + match &a.kind { + NodeKind::Instance { component, .. } => { + !constrains || doc.node(*component).is_some_and(|m| (m.width - a.width).abs() <= 1e-9 && (m.height - a.height).abs() <= 1e-9) + } + _ => true, + } + }) +} + +/// Whether a layer inside an instance overrides its size or resizing: it may not have its main +/// layer's size, so the layers in it can't take their main layers' places as they are. +fn sized_own(n: &Node) -> bool { + n.overrides.contains("size") || n.overrides.contains("resizing") +} + +/// Rebuild every instance of `comps` (and of the components that hold those instances, and so on +/// outwards), each outermost instance once, inner components first. +fn rebuild_instances(doc: &mut Document, comps: &BTreeSet) { + if comps.is_empty() { + return; + } + // Outermost instances by the components their layers instantiate. + let mut roots_of: BTreeMap> = BTreeMap::new(); + for n in doc.nodes() { + if let NodeKind::Instance { component, .. } = &n.kind { + roots_of.entry(*component).or_default().insert(root_instance(doc, n.id).unwrap_or(n.id)); + } + } + // Affected components, with how deep their dependencies go (an instance inside component X of + // component Y: X follows Y). + let mut level: BTreeMap = comps.iter().map(|c| (*c, 0)).collect(); + let mut outer: BTreeMap> = BTreeMap::new(); + let mut frontier: Vec = comps.iter().copied().collect(); + let mut rounds = 0; + while !frontier.is_empty() && rounds < 64 { + rounds += 1; + let mut next = Vec::new(); + for c in frontier { + let lc = level.get(&c).copied().unwrap_or(0); + for r in roots_of.get(&c).into_iter().flatten() { + let o = *outer.entry(*r).or_insert_with(|| owning_component(doc, *r)); + if let Some(x) = o + && x != c + { + let lx = level.entry(x).or_insert(0); + if *lx < lc + 1 { + *lx = lc + 1; + next.push(x); + } + } + } + } + frontier = next; + } + // Instances inside components by level, then the rest (each once). + let mut order: Vec<(usize, NodeId)> = outer.iter().map(|(r, o)| (o.and_then(|x| level.get(&x).copied()).unwrap_or(usize::MAX), *r)).collect(); + order.sort(); + for (_, r) in order { + if doc.contains(r) { + sync_instance(doc, r); + } + } +} diff --git a/crates/engine/src/assets/components/rebase.rs b/crates/engine/src/assets/components/rebase.rs new file mode 100644 index 00000000..748a99d3 --- /dev/null +++ b/crates/engine/src/assets/components/rebase.rs @@ -0,0 +1,249 @@ +use super::diff::{diff_groups, variable_groups}; +use super::instances::{field_group, path_map}; +use super::*; + +/// Bounded source and parent lookups while a rebase reads an unchanged document. +pub(super) struct RebaseSources<'a> { + doc: &'a Document, + chains: BTreeMap>>, + parents: BTreeMap<(NodeId, NodeId), Option>, + main_changes: BTreeMap, +} + +impl<'a> RebaseSources<'a> { + const PARENTS: usize = 512; + const CACHE: usize = 8192; + + pub(super) fn new(doc: &'a Document) -> Self { + Self { doc, chains: BTreeMap::new(), parents: BTreeMap::new(), main_changes: BTreeMap::new() } + } + + fn chain(&mut self, source: NodeId) -> Option> { + if let Some(chain) = self.chains.get(&source) { + return chain.clone(); + } + let read = || { + let mut chain = Vec::new(); + let mut seen = BTreeSet::new(); + let mut current = Some(source); + for _ in 0..MAX_DEPTH { + let Some(id) = current else { return Some(chain) }; + if !seen.insert(id) { + return None; + } + let n = self.doc.node(id)?; + chain.push(id); + current = n.source; + } + current.is_none().then_some(chain) + }; + let chain = read(); + if self.chains.len() < Self::CACHE { + self.chains.insert(source, chain.clone()); + } + chain + } + + /// Validate the complete parent chain, even if `main` was found before a cycle. + fn in_main(&mut self, id: NodeId, main: NodeId) -> Option { + if let Some(found) = self.parents.get(&(id, main)) { + return *found; + } + let read = || { + let mut current = Some(id); + let mut seen = BTreeSet::new(); + let mut found = false; + for _ in 0..Self::PARENTS { + let Some(id) = current else { return Some(found) }; + if !seen.insert(id) { + return None; + } + found |= id == main; + current = self.doc.node(id)?.parent; + } + current.is_none().then_some(found) + }; + let found = read(); + if self.parents.len() < Self::CACHE { + self.parents.insert((id, main), found); + } + found + } + + fn member(&mut self, source: NodeId, main: NodeId, target: NodeId) -> Option { + let chain = self.chain(source)?; + if chain.contains(&target) { + return None; + } + let mut member = None; + for id in chain { + if self.in_main(id, main)? && id != main && member.is_none() { + member = Some(id); + } + } + member + } + + /// Nearest enclosing instances and their current mains. Slots with own content end scope. + pub(super) fn owners(&self, id: NodeId) -> Option> { + let mut owners = Vec::new(); + let mut current = self.doc.node(id)?.parent; + let mut seen = BTreeSet::from([id]); + for _ in 0..Self::PARENTS { + let Some(id) = current else { return Some(owners) }; + if !seen.insert(id) { + return None; + } + let n = self.doc.node(id)?; + if holds_content(n) { + return Some(owners); + } + if let NodeKind::Instance { component, .. } = &n.kind { + owners.push((id, *component)); + } + current = n.parent; + } + current.is_none().then_some(owners) + } + + fn contextual(&mut self, source: NodeId, owners: &[(NodeId, NodeId)]) -> bool { + owners.iter().any(|(_, main)| self.in_main(source, *main) == Some(true) && source != *main) + } + + /// A contextual child belongs to the same placed slot as its enclosing instance. + fn valid(&mut self, source: NodeId, target: NodeId, owners: &[(NodeId, NodeId)]) -> bool { + let Some((owner, main)) = owners.first() else { return false }; + if self.member(source, *main, target).is_none() { + return false; + } + if self.in_main(source, *main) == Some(true) { + return true; + } + let Some(source_owner) = self.doc.node(*owner).and_then(|n| n.source) else { return false }; + matches!(self.doc.node(source_owner).map(|n| &n.kind), Some(NodeKind::Instance { component, .. }) if component == main) + && self.owners(source).and_then(|owners| owners.first().copied()).is_some_and(|(id, _)| id == source_owner) + } + + fn placed(&mut self, id: NodeId, owners: &[(NodeId, NodeId)]) -> Option { + let source = self.doc.node(id)?.source?; + if !matches!(self.doc.node(source)?.kind, NodeKind::Instance { .. } | NodeKind::Frame) + || !self.valid(source, id, owners) + || !self.contextual(source, owners) + { + return None; + } + Some(source) + } + + fn beneath_swap(&mut self, owners: &[(NodeId, NodeId)], stop: NodeId) -> bool { + for (index, (id, main)) in owners.iter().enumerate() { + if *id == stop { + break; + } + let changed = if let Some(changed) = self.main_changes.get(id) { + *changed + } else { + let changed = self.placed(*id, &owners[index + 1..]).is_some_and(|source| match self.doc.node(source).map(|n| &n.kind) { + Some(NodeKind::Instance { component, .. }) => component != main, + Some(NodeKind::Frame) => true, + _ => false, + }); + if self.main_changes.len() < Self::CACHE { + self.main_changes.insert(*id, changed); + } + changed + }; + if changed { + return true; + } + } + false + } +} + +/// Re-derive `source` links and overrides of instance `inst` against its own main by comparing +/// values (after detaching an outer instance, restoring a main, or copying a nested instance out): +/// the look is kept exactly. Values a component property sets aren't overrides. +pub fn rebase_instance(doc: &mut Document, inst: NodeId) { + let Some(NodeKind::Instance { component, .. }) = doc.node(inst).map(|n| n.kind.clone()) else { return }; + let mine = path_map(doc, inst, true); + let theirs: BTreeMap = + if doc.contains(component) { path_map(doc, component, false).into_iter().map(|(k, v)| (v, k)).collect() } else { BTreeMap::new() }; + let value_groups = |a: &Node, b: &Node| -> Vec { + diff_groups(a, b).into_iter().filter(|g| !matches!(*g, "size" | "position")).map(str::to_string).collect() + }; + let mut updates = Vec::new(); + let mut sources = RebaseSources::new(doc); + let root_owners = sources.owners(inst).unwrap_or_default(); + let placed_source = sources.placed(inst, &root_owners); + let mut contexts = vec![(inst, component)]; + contexts.extend_from_slice(&root_owners); + for (id, path) in &mine { + let Some(n) = doc.node(*id) else { continue }; + let owners = sources.owners(*id).unwrap_or_default(); + let nearest_main = owners.first().map(|(_, main)| *main); + let valid_source = n.source.filter(|source| sources.valid(*source, *id, &owners)); + let mut main = valid_source.and_then(|source| sources.member(source, component, *id)); + // A swapped or Frame-to-Instance subtree is compared to its current main by the inner + // rebase. An outer name path must not rebind those children to the old expanded copies. + if main.is_none() && valid_source.is_some() && sources.beneath_swap(&owners, inst) { + updates.push((*id, valid_source, n.overrides.clone())); + continue; + } + main = main + .or_else(|| theirs.get(path).copied().filter(|source| nearest_main.is_some_and(|scope| sources.member(*source, scope, *id).is_some()))); + let source = valid_source.filter(|source| sources.contextual(*source, &contexts)).filter(|_| main.is_some()).or(main); + let mut overrides: BTreeSet = n.overrides.iter().filter(|g| !matches!(g.as_str(), "component" | "props")).cloned().collect(); + if let Some(m) = source.and_then(|source| doc.node(source)) { + let mut bound: Vec<&str> = m.prop_refs.keys().map(|f| field_group(f)).collect(); + bound.extend(variable_groups(n, m)); + overrides.extend(value_groups(m, n).into_iter().filter(|g| !bound.contains(&g.as_str()))); + if matches!((&m.kind, &n.kind), (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) if a != b) { + overrides.insert("component".into()); + } + } + updates.push((*id, source, overrides)); + } + // A placed nested root compares to its actual slot. A detached root compares to its own main. + let root_base = placed_source.or_else(|| { + let chain = sources.chain(component)?; + (!chain.contains(&inst) && sources.in_main(component, inst) == Some(false)).then_some(component) + }); + let root_groups = match (root_base.and_then(|source| doc.node(source)), doc.node(inst)) { + (Some(m), Some(n)) => { + let from_variables = variable_groups(n, m); + let mut groups: Vec = value_groups(m, n) + .into_iter() + .filter(|g| !matches!(g.as_str(), "name" | "exports" | "interactions") && !from_variables.contains(&g.as_str())) + .filter(|g| placed_source.is_some() || !matches!(g.as_str(), "resizing" | "modes")) + .collect(); + let named = if placed_source.is_some() { n.name != m.name } else { n.name != default_instance_name(doc, component) }; + if named { + groups.push("name".into()); + } + if placed_source.is_some() + && matches!((&m.kind, &n.kind), (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) if a != b) + { + groups.push("component".into()); + } + groups + } + _ => Vec::new(), + }; + drop(sources); + for (id, main, overrides) in updates { + if let Some(n) = doc.node_mut(id) { + n.source = main; + n.overrides = if main.is_some() { overrides } else { BTreeSet::new() }; + } + } + if let Some(n) = doc.node_mut(inst) { + n.source = root_base; + // A top-level instance's resizing and modes are its own (a nested one's override its main + // layer's). + n.overrides.retain(|g| { + !matches!(g.as_str(), "component" | "props" | "position") && (placed_source.is_some() || !matches!(g.as_str(), "resizing" | "modes")) + }); + n.overrides.extend(root_groups); + } +} diff --git a/crates/engine/src/assets/components/sync.rs b/crates/engine/src/assets/components/sync.rs new file mode 100644 index 00000000..c502322d --- /dev/null +++ b/crates/engine/src/assets/components/sync.rs @@ -0,0 +1,459 @@ +use super::diff::diff_groups; +use super::instances::{Carry, apply_prop, carry_overrides, carry_prop_bindings, copy_group, field_group, follow_variant_resizing, path_map}; +use super::*; + +/// `spare`: layers the outer rebuild didn't reuse that were below `inst` (a swapped nested +/// instance): its layers the outer main doesn't have, matched here so they keep ids and overrides. +pub(super) fn sync_instance_depth(doc: &mut Document, inst: NodeId, depth: usize, spare: BTreeMap>) { + if depth > MAX_DEPTH { + return; + } + let Some(mut root) = doc.node(inst).cloned() else { return }; + let NodeKind::Instance { component, props: root_props } = &root.kind else { return }; + let component = *component; + let Some(comp) = doc.node(component).cloned() else { return }; + if !matches!(comp.kind, NodeKind::Component { .. }) || doc.is_ancestor(inst, component) || doc.is_ancestor(component, inst) { + // A missing main, or an instance inside its own main (would recurse forever). + return; + } + let mut eff_props = default_props(&comp); + for (k, v) in root_props { + if eff_props.contains_key(k) { + eff_props.insert(k.clone(), v.clone()); + } + } + // Old layers (kept alive for their overrides), by (parent, source), by source alone (when only + // one layer mirrors it) and, only when those don't match, by name path. + let mut old_ids = old_layers(doc, inst); + old_ids.extend(spare.keys().copied()); + old_ids.sort(); + old_ids.dedup(); + let mut old: BTreeMap> = spare; + let mut by_source: BTreeMap<(NodeId, NodeId), NodeId> = BTreeMap::new(); + let mut by_src: BTreeMap> = BTreeMap::new(); + for i in &old_ids { + let Some(n) = doc.node_arc(*i).or_else(|| old.get(i)).cloned() else { continue }; + if let Some(s) = n.source { + if let Some(p) = n.parent { + by_source.entry((p, s)).or_insert(*i); + } + by_src.entry(s).and_modify(|e| *e = None).or_insert(Some(*i)); + } + old.insert(*i, n); + } + let mut by_path: Option> = None; + let mut main_paths: Option> = None; + // Ids for the copies: reuse the matching old layer's id (a layer the main moved to another + // parent keeps its id and overrides). + let src: Vec = doc.subtree(component).into_iter().filter(|i| *i != component).collect(); + let mut map: BTreeMap = BTreeMap::new(); + map.insert(component, inst); + let mut used: BTreeSet = BTreeSet::new(); + // Slots holding the instance's own content (new ids), and main layers not copied: the + // default content of those slots. + let mut edited: BTreeSet = BTreeSet::new(); + let mut skipped: BTreeSet = BTreeSet::new(); + let mut fresh_ids = Vec::new(); + for s in &src { + let Some(sn) = doc.node(*s) else { continue }; + if sn.parent.is_some_and(|p| skipped.contains(&p) || map.get(&p).is_some_and(|pn| edited.contains(pn))) { + skipped.insert(*s); + continue; + } + let parent_new = sn.parent.and_then(|p| map.get(&p).copied()); + let mut found = parent_new.and_then(|pn| by_source.get(&(pn, *s)).copied()).or_else(|| by_src.get(s).copied().flatten()); + if found.is_none() { + let paths = by_path.get_or_insert_with(|| path_map(doc, inst, true).into_iter().map(|(k, v)| (v, k)).collect()); + let mains = main_paths.get_or_insert_with(|| path_map(doc, component, false)); + found = mains.get(s).and_then(|p| paths.get(p)).copied(); + } + let nid = match found { + Some(o) if used.insert(o) => o, + _ => { + fresh_ids.push(*s); + continue; + } + }; + if let Some(m) = old.get(&nid).and_then(|o| variant_kept(doc, o, *s)) { + old.insert(nid, std::sync::Arc::new(m)); + } + if old.get(&nid).is_some_and(|o| holds_content(o)) && is_slot(doc, *s) { + edited.insert(nid); + } + map.insert(*s, nid); + } + for c in root.children.clone() { + let _ = doc.remove(c); + } + for s in fresh_ids { + let nid = doc.alloc_id(); + map.insert(s, nid); + } + // Build the copies (pre-order: parents first). + let mut enclosing: BTreeMap> = BTreeMap::new(); + enclosing.insert(inst, vec![inst]); + let mut inst_props: BTreeMap> = BTreeMap::new(); + inst_props.insert(inst, eff_props); + let mut nested = Vec::new(); + let mut new_nodes = Vec::new(); + // Text layers showing other text or style than their main layer (to re-measure). + let mut remeasure = Vec::new(); + for s in &src { + let Some(orig) = doc.node(*s) else { continue }; + let Some(&nid) = map.get(s) else { continue }; + let Some(parent) = orig.parent.and_then(|p| map.get(&p).copied()) else { continue }; + let mut n = orig.clone(); + n.id = nid; + n.parent = Some(parent); + n.children = orig.children.iter().filter_map(|c| map.get(c).copied()).collect(); + n.source = Some(*s); + n.overrides.clear(); + n.prop_refs.clear(); + let prev = old.get(&nid); + // Annotations belong to the layer they were added to, not to its copies. + n.annotations = prev.map(|o| o.annotations.clone()).unwrap_or_default(); + n.measurements = prev.map(|o| o.measurements.clone()).unwrap_or_default(); + if let Some(o) = prev { + carry_overrides(o, &mut n, Carry::Values); + keep_laid_out(doc, orig, o, &mut n); + } + if !edited.contains(&nid) { + // No longer a slot in the main: it mirrors the main again. + n.overrides.remove(SLOT); + } + // Component properties: the main layer's bindings use this instance's values; layers of + // nested instances use the nested instance's values (one level per source hop). + let enc = enclosing.get(&parent).cloned().unwrap_or_default(); + let mut chain: Vec<&Node> = vec![orig]; + while chain.len() < enc.len() { + let Some(next) = chain.last().and_then(|c| c.source).and_then(|x| doc.node(x)) else { break }; + chain.push(next); + } + for (j, level) in chain.iter().enumerate().rev() { + let Some(props) = enc.get(j).and_then(|e| inst_props.get(e)) else { continue }; + for (field, prop) in &level.prop_refs { + let g = field_group(field); + if n.overrides.contains(g) || (j > 0 && orig.overrides.contains(g)) { + continue; + } + if let Some(val) = props.get(prop) { + apply_prop(doc, &mut n, field, val); + } + } + } + if let NodeKind::Instance { component: c, props } = &n.kind { + inst_props.insert(nid, props.clone()); + let mut e = enc; + e.push(nid); + enclosing.insert(nid, e); + if !matches!(orig.kind, NodeKind::Instance { component: oc, .. } if oc == *c) { + nested.push(nid); + } + } else { + enclosing.insert(nid, enc); + } + if let (NodeKind::Text(a), NodeKind::Text(b)) = (&orig.kind, &n.kind) + && a != b + { + remeasure.push(nid); + } + new_nodes.push(n); + } + for n in new_nodes { + let (id, parent) = (n.id, n.parent); + if insert_linked(doc, n).is_err() + && let Some(p) = parent.and_then(|p| doc.node_mut(p)) + { + p.children.retain(|c| *c != id); + } + } + // Root: follows the main except its own placement, name, size and overrides. + let order: Vec = comp.children.iter().filter_map(|c| map.get(c).copied()).filter(|c| doc.contains(*c)).collect(); + let mut fresh = comp.clone(); + fresh.id = inst; + fresh.parent = root.parent; + fresh.children = order; + fresh.transform = root.transform; + fresh.kind = root.kind.clone(); + carry_prop_bindings(&root, &mut fresh); + // A nested instance keeps mirroring its layer of the outer main. + let nested_copy = inside_instance(doc, inst); + fresh.source = if nested_copy { root.source } else { Some(component) }; + fresh.constraints = root.constraints; + fresh.child_layout = root.child_layout.clone(); + fresh.interactions = root.interactions.clone(); + fresh.exports = root.exports.clone(); + fresh.prop_refs = root.prop_refs.clone(); + fresh.plugin_data = root.plugin_data.clone(); + fresh.annotations = root.annotations.clone(); + // Code links belong to the main component; instances read them from it. + fresh.code_links.clear(); + // An instance's own dev resources (its main's are listed with them, not copied). + fresh.dev_resources = root.dev_resources.clone(); + fresh.measurements = root.measurements.clone(); + fresh.locked = root.locked; + fresh.visible = root.visible; + fresh.var_modes = root.var_modes.clone(); + fresh.is_mask = root.is_mask; + fresh.exposed = root.exposed; + fresh.guides = root.guides.clone(); + fresh.dev_status = root.dev_status; + fresh.dev_changed = root.dev_changed; + fresh.dev_note = root.dev_note.clone(); + fresh.constrain_proportions = root.constrain_proportions; + // How the instance combines in a boolean group it sits in is its own. + fresh.bool_op = root.bool_op; + // A swapped nested instance keeps the name its layer was given in the outer main. + let renamed = nested_copy + && root.source.and_then(|s| doc.node(s)).is_some_and(|s| match &s.kind { + NodeKind::Instance { component: c, .. } => s.name != default_instance_name(doc, *c), + _ => false, + }); + fresh.name = if renamed { root.name.clone() } else { default_instance_name(doc, component) }; + // Switched to another variant: its own overrides stay where both variants agreed. + if let Some(prev) = root.source.and_then(|p| doc.node(p)).filter(|p| sibling_variants(doc, p.id, component)) { + let differs = diff_groups(prev, &comp); + root.overrides.retain(|g| g == "name" || !differs.contains(&g.as_str())); + follow_variant_resizing(&mut fresh.child_layout, &prev.child_layout, &comp.child_layout); + } + carry_overrides(&root, &mut fresh, Carry::Root); + if let Some(r) = doc.node_mut(inst) { + *r = fresh; + } + // A resized instance: children follow their constraints (auto layout re-flows later). + let (iw, ih) = doc.node(inst).map(|n| (n.width, n.height)).unwrap_or((comp.width, comp.height)); + if (iw - comp.width).abs() > 1e-9 || (ih - comp.height).abs() > 1e-9 { + figcraft_layout::apply_constraints(doc, inst, comp.width, comp.height); + remeasure = map.values().copied().collect(); + } + // Geometry overrides of children win over constraints. + for s in &src { + let Some(nid) = map.get(s).copied() else { continue }; + let Some(o) = old.get(&nid) else { continue }; + // A frame that kept the size its own resizing gave it: its layers (copied at their main + // layers' places) follow their constraints from its main layer's size. + if o.overrides.contains("resizing") + && !o.overrides.contains("size") + && doc.node(nid).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none()) + && let Some((mw, mh)) = doc.node(*s).map(|m| (m.width, m.height)) + { + figcraft_layout::apply_constraints(doc, nid, mw, mh); + } + if !o.overrides.contains("size") && !o.overrides.contains("position") { + continue; + } + remeasure.push(nid); + let Some((ow, oh)) = doc.node(nid).map(|n| (n.width, n.height)) else { continue }; + if o.overrides.contains("size") { + // Through set_size so vector networks scale with the box. + figcraft_layout::set_size(doc, nid, o.width, o.height); + if doc.node(nid).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none()) { + figcraft_layout::apply_constraints(doc, nid, ow, oh); + } + } + if let Some(n) = doc.node_mut(nid) { + carry_overrides(o, n, Carry::Geometry); + } + } + // Text whose characters or style differ from the main re-measures. + for nid in remeasure { + if let Some(n) = doc.node(nid) + && let Some((w, h)) = figcraft_text::auto_size(n) + && ((w - n.width).abs() > 1e-6 || (h - n.height).abs() > 1e-6) + { + figcraft_layout::set_size(doc, nid, w, h); + } + } + // Nested instances showing another main than the main's copy (swapped): rebuild from it, + // with the old layers below them this rebuild didn't reuse. + let unused: BTreeMap> = + old.iter().filter(|(i, _)| !used.contains(*i) && !doc.contains(**i)).map(|(i, x)| (*i, x.clone())).collect(); + for n in nested { + let mut below: BTreeMap> = unused + .iter() + .filter(|(_, x)| { + let mut cur = x.parent; + for _ in 0..=MAX_DEPTH * 4 { + match cur { + Some(p) if p == n => return true, + Some(p) => cur = unused.get(&p).and_then(|a| a.parent), + None => return false, + } + } + false + }) + .map(|(i, x)| (*i, x.clone())) + .collect(); + take_placing_overrides(doc, n, &old, &mut below); + sync_instance_depth(doc, n, depth + 1, below); + } +} + +/// Before swapped nested instance `n` is rebuilt from its new main: each of its old layers (the +/// copies just made of the outer main's, and `below`) takes the overrides held along the source +/// chain it had before this rebuild (`before`), through layers inside instances in the mains that +/// place `n`. Matched by name, they carry onto the new main's layers as the instance's own, so a +/// swap takes what the outer main set once: later rebuilds find layers mirroring the new main. +/// Geometry and slot content follow the new main; groups the layer overrides itself keep its value. +fn take_placing_overrides( + doc: &mut Document, + n: NodeId, + before: &BTreeMap>, + below: &mut BTreeMap>, +) { + let placing = placing_mains(doc, n); + for id in doc.subtree(n).into_iter().filter(|i| *i != n) { + let held = before.get(&id).map(|b| held_along(doc, b, &placing)).unwrap_or_default(); + if let Some(layer) = doc.node_mut(id) { + take(layer, &held); + } + } + for layer in below.values_mut() { + let held = held_along(doc, layer, &placing); + if !held.is_empty() { + take(std::sync::Arc::make_mut(layer), &held); + } + } +} + +/// The mains placing nested instance `n`: those of the layers it mirrors, outward. +fn placing_mains(doc: &Document, n: NodeId) -> BTreeSet { + let mut out = BTreeSet::new(); + let mut cur = doc.node(n).and_then(|x| x.source); + for _ in 0..=MAX_DEPTH { + let Some(s) = cur.and_then(|c| doc.node(c)).filter(|s| !matches!(s.kind, NodeKind::Component { .. })) else { break }; + out.extend(owning_component(doc, s.id)); + cur = s.source; + } + out +} + +/// The overrides but geometry and slot content held along `layer`'s source chain while it runs +/// through layers inside instances in `placing` mains, each with the nearest layer holding it. +fn held_along(doc: &Document, layer: &Node, placing: &BTreeSet) -> BTreeMap> { + let mut out = BTreeMap::new(); + let mut cur = layer.source; + for _ in 0..=MAX_DEPTH { + let Some(s) = cur.and_then(|c| doc.node_arc(c)) else { break }; + if !inside_instance(doc, s.id) || !owning_component(doc, s.id).is_some_and(|m| placing.contains(&m)) { + break; + } + for g in s.overrides.iter().filter(|g| !matches!(g.as_str(), "size" | "position" | SLOT)) { + out.entry(g.clone()).or_insert_with(|| s.clone()); + } + cur = s.source; + } + out +} + +/// Give `layer` the `held` groups it has no override of its own for. +fn take(layer: &mut Node, held: &BTreeMap>) { + for (g, from) in held { + if layer.overrides.insert(g.clone()) { + copy_group(from, layer, g); + } + } +} + +/// Whether main components `a` and `b` are two variants of one set. +fn sibling_variants(doc: &Document, a: NodeId, b: NodeId) -> bool { + a != b && variant_set(doc, a).is_some_and(|s| variant_set(doc, b) == Some(s)) +} + +/// Old copy `o` matched by name to main layer `s` of another variant (the instance switched +/// variants): `o` without the overrides of properties the two variants' layers differ in (`None` +/// when it keeps them all). Text layers keep every override. +fn variant_kept(doc: &Document, o: &Node, s: NodeId) -> Option { + let from = o.source.filter(|f| *f != s).and_then(|f| doc.node(f))?; + if o.overrides.is_empty() || matches!(o.kind, NodeKind::Text(_)) { + return None; + } + if !sibling_variants(doc, owning_component(doc, from.id)?, owning_component(doc, s)?) { + return None; + } + let differs = diff_groups(from, doc.node(s)?); + let mut m = o.clone(); + m.overrides.retain(|g| !differs.contains(&g.as_str())); + (m.overrides.len() != o.overrides.len()).then_some(m) +} + +/// Drop the overrides of instance `inst` but its text layers' (and its own size and the content of +/// its slots): what swapping it from the Assets panel keeps. +pub fn keep_text_overrides(doc: &mut Document, inst: NodeId) { + for d in old_layers(doc, inst).into_iter().chain([inst]) { + let Some(n) = doc.node(d) else { continue }; + let text = matches!(n.kind, NodeKind::Text(_)); + let keep = |g: &String| text || g == SLOT || (d == inst && g == "size"); + if !n.overrides.iter().all(keep) + && let Some(m) = doc.node_mut(d) + { + m.overrides.retain(keep); + } + } +} + +/// Where auto layout places and sizes a rebuilt copy `n` of main layer `orig`, keep the old +/// copy's `o` values (an instance's auto layout re-flows from its own content, so they usually +/// differ from the main's): unchanged layers then stay unchanged, and auto layout re-flows wherever +/// something that sizes or places layers did change. +fn keep_laid_out(doc: &Document, orig: &Node, o: &Node, n: &mut Node) { + if o.parent != n.parent || o.overrides.contains("size") || o.overrides.contains("position") { + return; + } + let flows = !n.child_layout.absolute && orig.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.layout.is_some()); + if flows { + // The main's rotation and skew, the old place. + let [a, b, c, d, _, _] = n.transform.as_coeffs(); + let [.., e, f] = o.transform.as_coeffs(); + n.transform = Affine::new([a, b, c, d, e, f]); + } + // Only layers whose shape is their box (a vector's points scale with it). + let boxed = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Text(_) | NodeKind::Rectangle | NodeKind::Ellipse { .. }); + let derived = |s: Sizing| boxed && ((s == Sizing::Hug && n.layout.is_some()) || (s == Sizing::Fill && flows)); + if derived(n.child_layout.sizing_h) { + n.width = o.width; + } + if derived(n.child_layout.sizing_v) { + n.height = o.height; + } +} + +/// The layers of instance `inst` a rebuild matches against (every layer below it but the content +/// of slots holding its own), ascending. +fn old_layers(doc: &Document, inst: NodeId) -> Vec { + let mut out = Vec::new(); + let mut stack = vec![inst]; + while let Some(id) = stack.pop() { + let Some(n) = doc.node(id) else { continue }; + for c in &n.children { + out.push(*c); + if out.len() > 1_000_000 { + break; + } + if doc.node(*c).is_some_and(|cn| !holds_content(cn)) { + stack.push(*c); + } + } + } + out.sort(); + out.dedup(); + out +} + +/// Insert a node whose parent/children links are already set (used when rebuilding instances). +pub(super) fn insert_linked(doc: &mut Document, n: Node) -> Result<(), DocError> { + let parent = n.parent.ok_or_else(|| DocError::Invalid("no parent".into()))?; + let id = n.id; + let mut n = n; + let kids = std::mem::take(&mut n.children); + doc.insert(n, parent, None)?; + if let Some(m) = doc.node_mut(id) { + m.children = kids; + } + // The parent may already list us (its children were copied with ids): keep the first. + if let Some(p) = doc.node_mut(parent) { + let mut seen = BTreeSet::new(); + p.children.retain(|c| seen.insert(*c)); + } + Ok(()) +} diff --git a/crates/engine/src/assets/components/variants.rs b/crates/engine/src/assets/components/variants.rs new file mode 100644 index 00000000..40ac23ea --- /dev/null +++ b/crates/engine/src/assets/components/variants.rs @@ -0,0 +1,147 @@ +use super::*; + +/// A set's default variant: the one in its top-left corner (nearest to it, then the top-most, then +/// the left-most). +pub fn default_variant(doc: &Document, set: NodeId) -> Option { + let corner = |c: &NodeId| { + doc.node(*c).filter(|n| matches!(n.kind, NodeKind::Component { .. })).map(|n| n.transform.as_coeffs()).map(|[.., x, y]| (x + y, y, x)) + }; + let kids = &doc.node(set)?.children; + kids.iter() + .copied() + .filter_map(|c| corner(&c).map(|k| (k, c))) + .min_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .map(|(_, c)| c) +} + +/// Variants of a set sharing one combination of property values (each group two or more, in +/// layer order): a conflict to resolve by giving each a unique combination. +pub fn variant_conflicts(doc: &Document, set: NodeId) -> Vec> { + let mut by: BTreeMap, Vec> = BTreeMap::new(); + for c in doc.node(set).map(|s| s.children.clone()).unwrap_or_default() { + if let Some(n) = doc.node(c).filter(|n| matches!(n.kind, NodeKind::Component { .. })) { + by.entry(variant_props(&n.name)).or_default().push(c); + } + } + by.into_values().filter(|v| v.len() > 1).collect() +} + +/// Parse a variant name "Size=Large, State=Hover" into pairs. +pub fn variant_props(name: &str) -> BTreeMap { + name.split(',') + .filter_map(|kv| { + let (k, v) = kv.split_once('=')?; + Some((k.trim().to_string(), v.trim().to_string())) + }) + .collect() +} + +/// Format variant pairs as a layer name, in the given property order (others after, sorted). +pub fn variant_name(pairs: &BTreeMap, order: &[String]) -> String { + let mut keys: Vec<&String> = order.iter().filter(|k| pairs.contains_key(*k)).collect(); + keys.extend(pairs.keys().filter(|k| !order.contains(k))); + keys.iter().filter_map(|k| pairs.get(*k).map(|v| format!("{k}={v}"))).collect::>().join(", ") +} + +/// Variant properties of a set: property → values, in first-seen order. +pub fn variant_options(doc: &Document, set: NodeId) -> Vec<(String, Vec)> { + let mut out: Vec<(String, Vec)> = Vec::new(); + for c in doc.node(set).map(|s| s.children.clone()).unwrap_or_default() { + let Some(cn) = doc.node(c) else { continue }; + for kv in cn.name.split(',') { + let Some((k, v)) = kv.split_once('=') else { continue }; + let (k, v) = (k.trim().to_string(), v.trim().to_string()); + match out.iter_mut().find(|(p, _)| *p == k) { + Some((_, vals)) => { + if !vals.contains(&v) { + vals.push(v); + } + } + None => out.push((k, vec![v])), + } + } + } + out +} + +/// The variant in `set` matching `wanted` best: all given properties equal, or else the most. +pub fn find_variant(doc: &Document, set: NodeId, wanted: &BTreeMap) -> Option { + let s = doc.node(set)?; + let score = |c: &NodeId| doc.node(*c).map(|n| variant_props(&n.name)).map(|p| wanted.iter().filter(|(k, v)| p.get(*k) == Some(*v)).count()); + if let Some(exact) = s.children.iter().copied().find(|c| score(c) == Some(wanted.len())) { + return Some(exact); + } + s.children.iter().copied().filter(|c| score(c).is_some()).max_by_key(|c| score(c).unwrap_or(0)) +} + +/// Check that every instance mirrors its main's layer structure (tests and diagnostics): each +/// layer's `source` is the main layer at the same place, swapped nested instances mirror their +/// own main, slots holding the instance's own content aren't compared. Returns the problems found. +pub fn mirror_issues(doc: &Document) -> Vec { + doc.nodes() + .filter(|n| matches!(n.kind, NodeKind::Instance { .. }) && !inside_instance(doc, n.id)) + .flat_map(|n| instance_issues(doc, n.id)) + .collect() +} + +/// [`mirror_issues`] for one instance. +pub(super) fn instance_issues(doc: &Document, id: NodeId) -> Vec { + fn walk(doc: &Document, inst: &Node, main: &Node, depth: usize, out: &mut Vec) { + if depth > MAX_DEPTH || holds_content(inst) { + return; + } + if inst.children.len() != main.children.len() { + out.push(format!("{} has {} layers, its main {} has {}", inst.id, inst.children.len(), main.id, main.children.len())); + return; + } + for (a, b) in inst.children.iter().zip(&main.children) { + let (Some(an), Some(bn)) = (doc.node(*a), doc.node(*b)) else { continue }; + if an.source != Some(bn.id) { + out.push(format!("{} mirrors {:?}, expected {}", an.id, an.source, bn.id)); + continue; + } + match (&an.kind, &bn.kind) { + (NodeKind::Instance { component: x, .. }, NodeKind::Instance { component: y, .. }) if x != y => { + if let Some(m) = doc.node(*x) { + walk(doc, an, m, depth + 1, out); + } + } + _ => walk(doc, an, bn, depth + 1, out), + } + } + } + let mut out = Vec::new(); + if let Some(n) = doc.node(id) + && let NodeKind::Instance { component, .. } = &n.kind + && let Some(m) = doc.node(*component) + { + walk(doc, n, m, 0, &mut out); + } + out +} + +/// The nested instance copies inside instance `id` whose main-component counterpart is exposed +/// (top-most first; an exposed instance's own exposed nested instances follow it). +pub fn exposed_copies(doc: &Document, id: NodeId) -> Vec { + let mut out = Vec::new(); + let mut stack: Vec = doc.node(id).map(|n| n.children.iter().rev().copied().collect()).unwrap_or_default(); + let mut seen = 0usize; + while let Some(c) = stack.pop() { + seen += 1; + if seen > 100_000 { + break; + } + let Some(n) = doc.node(c) else { continue }; + // A hidden exposed instance hides its properties too. + let exposed = + n.visible && matches!(n.kind, NodeKind::Instance { .. }) && n.source.and_then(|src| doc.node(src)).is_some_and(|src| src.exposed); + if exposed { + out.push(c); + } + // Only exposed nested instances pass their own exposed instances up. + if !matches!(n.kind, NodeKind::Instance { .. }) || exposed { + stack.extend(n.children.iter().rev().copied()); + } + } + out +}