mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 04:46:50 -06:00
237 lines
10 KiB
Rust
237 lines
10 KiB
Rust
use super::*;
|
|
|
|
/// Nodes whose `Arc` differs from `before` (new or edited).
|
|
pub fn changed_nodes(doc: &Document, before: &Document) -> Vec<NodeId> {
|
|
doc.ids()
|
|
.filter(|id| match (doc.node_arc(*id), before.node_arc(*id)) {
|
|
(Some(a), Some(b)) => !Arc::ptr_eq(a, b),
|
|
_ => true,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Keep derived state consistent after edits: text boxes resize to their content, groups wrap
|
|
/// their children, instances follow their main components, auto layout re-flows.
|
|
pub fn normalize(doc: &mut Document, before: &Document) {
|
|
// Only what was touched since the last normalisation (edited, added or removed).
|
|
let mut touched: BTreeSet<NodeId> = doc.take_dirty();
|
|
// A variable or style edit changes no layer directly, but bound and linked layers must follow it.
|
|
if touched.is_empty() && doc.variables == before.variables && doc.styles == before.styles {
|
|
return;
|
|
}
|
|
let m = measure::TextMeasure;
|
|
// 0. Variable bindings and style links feed node values.
|
|
let scope = binding_scope(doc, before, &touched);
|
|
if doc.variables != before.variables || doc.styles != before.styles {
|
|
style_vars::settle(doc);
|
|
}
|
|
cmd::styles::apply_bindings_to(doc, scope.as_ref());
|
|
style_vars::refresh(doc, Some(before), scope.as_ref());
|
|
if doc.styles != before.styles {
|
|
let changed_styles: Vec<String> = doc.styles.iter().filter(|s| !before.styles.contains(s)).map(|s| s.id.clone()).collect();
|
|
for sid in changed_styles {
|
|
cmd::styles::propagate_style(doc, &sid);
|
|
}
|
|
}
|
|
touched.extend(doc.take_dirty());
|
|
// Lists and text chars edited away from their style unlink from it.
|
|
let live_now: Vec<NodeId> = touched.iter().copied().filter(|i| doc.contains(*i)).collect();
|
|
cmd::styles::sync_list_styles(doc, before, &live_now);
|
|
cmd::styles::sync_text_styles(doc, &live_now);
|
|
touched.extend(doc.take_dirty());
|
|
let live = |doc: &Document, t: &BTreeSet<NodeId>| -> Vec<NodeId> { t.iter().copied().filter(|i| doc.contains(*i)).collect() };
|
|
// 0b. Fill and hug only where they can apply (a layer moved out of auto layout, a parent that
|
|
// started to hug or lost auto layout): the rest becomes fixed at its current size.
|
|
for id in live(doc, &touched) {
|
|
figcraft_layout::coherent_sizing(doc, id);
|
|
}
|
|
touched.extend(doc.take_dirty());
|
|
// 1. Text auto-size (a sticky's note first takes the size its length calls for).
|
|
board::settle_text(doc, before, &touched);
|
|
touched.extend(doc.take_dirty());
|
|
for id in live(doc, &touched) {
|
|
let Some(n) = doc.node(id) else { continue };
|
|
if 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, id, w, h);
|
|
}
|
|
}
|
|
touched.extend(doc.take_dirty());
|
|
// 2. Instances follow edited main components (and record overrides inside instances).
|
|
let changed = live(doc, &touched);
|
|
components::sync_after_edit(doc, before, &changed, &touched);
|
|
// Rebuilt copies took their mains' values: their bindings resolve in their own modes.
|
|
let synced = doc.take_dirty();
|
|
if !synced.is_empty() {
|
|
cmd::styles::apply_bindings_to(doc, Some(&synced));
|
|
style_vars::refresh(doc, Some(before), Some(&synced));
|
|
touched.extend(synced);
|
|
}
|
|
touched.extend(doc.take_dirty());
|
|
// Parents of removed nodes (as they were before) count as touched.
|
|
let removed_parents: Vec<NodeId> =
|
|
touched.iter().filter(|i| !doc.contains(**i)).filter_map(|i| before.node(*i).and_then(|n| n.parent)).filter(|p| doc.contains(*p)).collect();
|
|
// 3. Groups / booleans wrap their children.
|
|
let mut fit: BTreeSet<NodeId> = BTreeSet::new();
|
|
for id in live(doc, &touched).into_iter().chain(removed_parents.iter().copied()) {
|
|
if doc.node(id).is_some_and(|n| n.kind.bounds_follow_children()) {
|
|
fit.insert(id);
|
|
}
|
|
for a in doc.ancestors(id) {
|
|
if doc.node(a).is_some_and(|n| n.kind.bounds_follow_children()) {
|
|
fit.insert(a);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
let mut fit: Vec<NodeId> = fit.into_iter().collect();
|
|
fit.sort_by_key(|id| std::cmp::Reverse(doc.ancestors(*id).len()));
|
|
for id in fit {
|
|
if doc.node(id).is_some_and(|n| n.children.is_empty()) {
|
|
// Empty groups disappear, like the incumbent.
|
|
let _ = doc.remove(id);
|
|
} else {
|
|
let _ = doc.fit_to_children(id);
|
|
}
|
|
}
|
|
touched.extend(doc.take_dirty());
|
|
// 4. Auto layout for frames where something that sizes or places layers changed.
|
|
let mut frames: BTreeSet<NodeId> = BTreeSet::new();
|
|
let moved = live(doc, &touched).into_iter().filter(|id| affects_layout(before, doc, *id));
|
|
for id in moved.chain(removed_parents) {
|
|
if !doc.contains(id) {
|
|
continue;
|
|
}
|
|
let mut chain = vec![id];
|
|
chain.extend(doc.ancestors(id));
|
|
for c in chain {
|
|
if doc.node(c).is_some_and(|n| n.layout.is_some()) {
|
|
frames.insert(c);
|
|
}
|
|
}
|
|
}
|
|
let mut frames: Vec<NodeId> = frames.into_iter().collect();
|
|
frames.sort_by_key(|id| std::cmp::Reverse(doc.ancestors(*id).len()));
|
|
for f in frames {
|
|
// Layers the layout leaves where they were stay untouched.
|
|
let kept: Vec<(Arc<Node>, bool)> = doc
|
|
.node(f)
|
|
.map(|n| std::iter::once(f).chain(n.children.iter().copied()).collect::<Vec<_>>())
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter_map(|i| doc.node_arc(i).map(|a| (a.clone(), doc.dirty().contains(&i))))
|
|
.collect();
|
|
figcraft_layout::layout_frame(doc, f, &m);
|
|
for (a, was_dirty) in kept {
|
|
doc.keep_if_unchanged(a, was_dirty);
|
|
}
|
|
}
|
|
touched.extend(doc.take_dirty());
|
|
// 4b. Board connectors follow the layers they join; their labels sit at their middles.
|
|
board::settle_routes(doc, before, &touched);
|
|
touched.extend(doc.take_dirty());
|
|
// 5. Ready-for-dev layers whose contents changed show "Changed".
|
|
mark_dev_changes(doc, before, &touched);
|
|
doc.take_dirty();
|
|
// 5. Comment pins follow their layers to other pages and stay put when the layers are deleted.
|
|
cmd::comment::follow_anchors(doc, before);
|
|
}
|
|
|
|
/// Whether layer `id` changed (since `before`) in a way that can move or resize layers in an auto
|
|
/// layout frame around it: new, resized, moved, shown or hidden, restructured, other text or
|
|
/// layout settings.
|
|
fn affects_layout(before: &Document, doc: &Document, id: NodeId) -> bool {
|
|
let (Some(a), Some(b)) = (before.node(id), doc.node(id)) else { return true };
|
|
a.width != b.width
|
|
|| a.height != b.height
|
|
|| a.transform != b.transform
|
|
|| a.visible != b.visible
|
|
|| a.parent != b.parent
|
|
|| a.children != b.children
|
|
|| a.layout != b.layout
|
|
|| a.child_layout != b.child_layout
|
|
|| a.stroke != b.stroke
|
|
|| a.strokes.len() != b.strokes.len()
|
|
|| a.kind != b.kind
|
|
}
|
|
|
|
/// The layers whose variable bindings an edit may have changed: every bound layer when a variable
|
|
/// changed, else the touched layers and everything below those that moved, are new, chose
|
|
/// another variable mode or became an instance of another main (modes are inherited, an
|
|
/// instance's from its main too), and the instances of a main that chose another mode.
|
|
fn binding_scope(doc: &Document, before: &Document, touched: &BTreeSet<NodeId>) -> Option<BTreeSet<NodeId>> {
|
|
if doc.variables != before.variables {
|
|
return None;
|
|
}
|
|
let main = |n: &Node| match n.kind {
|
|
figcraft_doc::NodeKind::Instance { component, .. } => Some(component),
|
|
_ => None,
|
|
};
|
|
let mut out = BTreeSet::new();
|
|
for &id in touched {
|
|
let Some(n) = doc.node(id) else { continue };
|
|
let Some(b) = before.node(id) else {
|
|
out.extend(doc.subtree(id));
|
|
continue;
|
|
};
|
|
let modes = b.var_modes != n.var_modes;
|
|
if modes || b.parent != n.parent || main(b) != main(n) {
|
|
out.extend(doc.subtree(id));
|
|
} else {
|
|
out.insert(id);
|
|
}
|
|
if modes && matches!(n.kind, figcraft_doc::NodeKind::Component { .. }) {
|
|
for i in components::instances_of(doc, id) {
|
|
out.extend(doc.subtree(i));
|
|
}
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
/// Flag the frames and sections marked ready for dev (before this edit) that contain a layer whose
|
|
/// design `touched` changed (added, removed or edited; the handoff status itself and hiding a
|
|
/// section's contents don't count).
|
|
fn mark_dev_changes(doc: &mut Document, before: &Document, touched: &BTreeSet<NodeId>) {
|
|
let ready = |d: &Document, id: NodeId| d.node(id).is_some_and(|n| n.dev_status == figcraft_doc::DevStatus::ReadyForDev);
|
|
let mut changed: BTreeSet<NodeId> = BTreeSet::new();
|
|
for &id in touched {
|
|
// Ready-for-dev frames around it first (the cheap test).
|
|
let mut chain = vec![id];
|
|
chain.extend(if doc.contains(id) { doc.ancestors(id) } else { before.ancestors(id) });
|
|
chain.retain(|m| ready(before, *m) && ready(doc, *m));
|
|
if chain.is_empty() {
|
|
continue;
|
|
}
|
|
let differs = match (before.node(id), doc.node(id)) {
|
|
(Some(a), Some(b)) => !a.same_design(b),
|
|
(None, None) => false,
|
|
_ => true,
|
|
};
|
|
if differs {
|
|
changed.extend(chain);
|
|
}
|
|
}
|
|
for m in changed {
|
|
if doc.node(m).is_some_and(|n| !n.dev_changed) {
|
|
// Its first change since it was marked: keep what it looked like (for Compare changes).
|
|
if let Some(snap) = before.snapshot_subtree(m) {
|
|
doc.dev_snapshots.insert(m, Arc::new(snap));
|
|
}
|
|
if let Some(n) = doc.node_mut(m) {
|
|
n.dev_changed = true;
|
|
}
|
|
}
|
|
}
|
|
// Snapshots last until the change is resolved, the layer is marked again or it's deleted.
|
|
if !doc.dev_snapshots.is_empty() {
|
|
let keep = |d: &Document, id: NodeId| d.node(id).is_some_and(|n| n.dev_status == figcraft_doc::DevStatus::ReadyForDev && n.dev_changed);
|
|
let stale: Vec<NodeId> = doc.dev_snapshots.keys().copied().filter(|id| !keep(doc, *id)).collect();
|
|
for id in stale {
|
|
doc.dev_snapshots.remove(&id);
|
|
}
|
|
}
|
|
}
|