Files
figcraft/crates/engine/src/assets/components/sync.rs
T

506 lines
22 KiB
Rust

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::{
Affine, BTreeMap, BTreeSet, DocError, Document, MAX_DEPTH, Node, NodeId, NodeKind, SLOT, Sizing, Value, default_instance_name, default_props,
holds_content, inside_instance, is_slot, owning_component, variant_set,
};
/// `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<NodeId, std::sync::Arc<Node>>) {
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 eff_props = effective_props(&comp, root_props);
// 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, by_source, by_src) = index_old_layers(doc, inst, spare);
let mut by_path: Option<BTreeMap<String, NodeId>> = None;
let mut main_paths: Option<BTreeMap<NodeId, String>> = 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<NodeId> = doc.subtree(component).into_iter().filter(|i| *i != component).collect();
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
map.insert(component, inst);
let mut used: BTreeSet<NodeId> = 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<NodeId> = BTreeSet::new();
let mut skipped: BTreeSet<NodeId> = 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<NodeId, Vec<NodeId>> = BTreeMap::new();
enclosing.insert(inst, vec![inst]);
let mut inst_props: BTreeMap<NodeId, BTreeMap<String, Value>> = 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();
apply_enclosing_props(doc, &mut n, orig, &enc, &inst_props);
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);
}
}
let fresh = fresh_root(doc, &mut root, &comp, inst, component, &map);
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();
}
follow_geometry_overrides(doc, &src, &map, &old, &mut remeasure);
remeasure_text(doc, remeasure);
rebuild_nested(doc, nested, &old, &used, depth);
}
/// Geometry overrides of children win over constraints.
fn follow_geometry_overrides(
doc: &mut Document,
src: &[NodeId],
map: &BTreeMap<NodeId, NodeId>,
old: &BTreeMap<NodeId, std::sync::Arc<Node>>,
remeasure: &mut Vec<NodeId>,
) {
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);
}
}
}
/// 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.
fn rebuild_nested(doc: &mut Document, nested: Vec<NodeId>, old: &BTreeMap<NodeId, std::sync::Arc<Node>>, used: &BTreeSet<NodeId>, depth: usize) {
let unused: BTreeMap<NodeId, std::sync::Arc<Node>> =
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<NodeId, std::sync::Arc<Node>> = 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);
}
}
fn apply_enclosing_props(doc: &Document, n: &mut Node, orig: &Node, enc: &[NodeId], inst_props: &BTreeMap<NodeId, BTreeMap<String, Value>>) {
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, n, field, val);
}
}
}
}
/// Text whose characters or style differ from the main re-measures.
fn remeasure_text(doc: &mut Document, remeasure: Vec<NodeId>) {
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);
}
}
}
/// The component's property defaults with the instance's values for the ones it has.
fn effective_props(comp: &Node, root_props: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
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());
}
}
eff_props
}
type OldIndex = (BTreeMap<NodeId, std::sync::Arc<Node>>, BTreeMap<(NodeId, NodeId), NodeId>, BTreeMap<NodeId, Option<NodeId>>);
/// The old layers of `inst` (and `spare`) by id, by (parent, source) and by source alone (`None`
/// when several layers mirror it).
fn index_old_layers(doc: &Document, inst: NodeId, spare: BTreeMap<NodeId, std::sync::Arc<Node>>) -> OldIndex {
let mut old_ids = old_layers(doc, inst);
old_ids.extend(spare.keys().copied());
old_ids.sort();
old_ids.dedup();
let mut old = spare;
let mut by_source: BTreeMap<(NodeId, NodeId), NodeId> = BTreeMap::new();
let mut by_src: BTreeMap<NodeId, Option<NodeId>> = 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);
}
(old, by_source, by_src)
}
/// The rebuilt root of instance `inst` of `comp`.
fn fresh_root(doc: &Document, root: &mut Node, comp: &Node, inst: NodeId, component: NodeId, map: &BTreeMap<NodeId, NodeId>) -> Node {
// Root: follows the main except its own placement, name, size and overrides.
let order: Vec<NodeId> = 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);
fresh
}
/// 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<NodeId, std::sync::Arc<Node>>,
below: &mut BTreeMap<NodeId, std::sync::Arc<Node>>,
) {
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<NodeId> {
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<NodeId>) -> BTreeMap<String, std::sync::Arc<Node>> {
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<String, std::sync::Arc<Node>>) {
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<Node> {
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<NodeId> {
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(())
}