figcraft/crates/doc/src/lib.rs

1023 lines
37 KiB
Rust

//! The FigCraft document model.
//!
//! A [`Document`] holds pages; each page is the root of a tree of [`Node`]s (frames, groups, shapes,
//! text, components, instances…). Nodes are stored by id in a map of `Arc`s, so cloning a document
//! (an undo snapshot) only copies pointers, and editing one node copies only that node.
//!
//! Everything here is pure data plus queries and structural edits; commands, history and rendering
//! live in higher crates.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod colors;
pub mod comments;
pub mod geometry;
pub mod handoff;
pub mod ids;
pub mod library;
pub mod node;
pub mod props;
pub mod proto;
pub mod styles;
pub mod text;
pub mod vector;
pub mod vector_faces;
pub use colors::*;
pub use comments::*;
pub use figcraft_color as color;
pub use figcraft_geom as geom;
pub use handoff::*;
pub use library::*;
pub use node::*;
pub use props::*;
pub use styles::*;
pub use text::*;
pub use vector::*;
pub use vector_faces::Face;
use figcraft_color::{Color, ColorProfile};
use figcraft_geom::{Affine, Point, Rect};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::Arc;
/// Stable node identifier, unique within a document.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NodeId(pub u64);
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for NodeId {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Accept "12" and Figma-style "0:12".
let t = s.trim();
let t = t.rsplit(':').next().unwrap_or(t);
t.parse::<u64>().map(NodeId)
}
}
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum DocError {
#[error("no such node {0}")]
NoNode(NodeId),
#[error("{0} can't contain children")]
NotContainer(NodeId),
#[error("moving {0} into {1} would create a cycle")]
Cycle(NodeId, NodeId),
#[error("{0}")]
Invalid(String),
}
pub type DocResult<T> = Result<T, DocError>;
/// The set of touched node ids. Never part of equality or serialisation.
#[derive(Clone, Debug, Default)]
pub struct Dirty(pub std::collections::BTreeSet<NodeId>);
impl PartialEq for Dirty {
fn eq(&self, _: &Self) -> bool {
true
}
}
/// Hands out node ids. A single-user document counts up through the whole id range; in a live
/// collaboration session each participant allocates in its own space, `client << 32 | counter`, so
/// ids made at the same time on different machines never collide. Saved as the file's `next_id`;
/// the client id is never saved. Never part of equality: two documents with the same content are
/// equal however far their counters went.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(transparent)]
struct IdAlloc {
/// The next id to hand out.
next: u64,
/// The client whose space ids come from (0: the whole range, as in a single-user file).
#[serde(skip)]
client: u32,
}
impl PartialEq for IdAlloc {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl IdAlloc {
/// Whether `id` lies in the space this allocator hands out (any id without a client).
fn owns(&self, id: NodeId) -> bool {
self.client == 0 || id.0 >> 32 == u64::from(self.client)
}
/// Never hand out `id` (or anything below it in this space) again.
fn reserve(&mut self, id: NodeId) {
if self.owns(id) && id.0 >= self.next {
self.next = id.0.saturating_add(1);
}
}
fn alloc(&mut self) -> NodeId {
let id = NodeId(self.next);
let next = self.next.saturating_add(1);
// A client space holds 2^32 ids; a full one keeps returning its last id (an insert then
// fails as a duplicate) rather than spilling into another client's space.
if self.owns(NodeId(next)) {
self.next = next;
}
id
}
}
/// Raw image bytes (encoded PNG/JPEG/…) kept by content hash.
#[derive(Clone, Debug, PartialEq)]
pub struct ImageData {
pub bytes: Arc<Vec<u8>>,
pub width: u32,
pub height: u32,
}
/// A whole design file.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Document {
pub name: String,
/// Page ids in order.
pub pages: Vec<NodeId>,
nodes: imbl::OrdMap<NodeId, Arc<Node>>,
/// Nodes touched since the last [`take_dirty`](Self::take_dirty) (edited, added or removed).
#[serde(skip)]
dirty: Dirty,
next_id: IdAlloc,
#[serde(default)]
pub styles: Vec<Style>,
#[serde(default)]
pub variables: Variables,
/// Images by content hash; serialized separately by the format crate.
#[serde(skip)]
pub images: BTreeMap<String, ImageData>,
/// The page shown when the file opens.
#[serde(default)]
pub current_page: Option<NodeId>,
/// Colours used recently / saved in "Document colors".
#[serde(default)]
pub document_colors: Vec<Color>,
/// Comment threads on any page.
#[serde(default)]
pub comments: Vec<Comment>,
/// Enabled libraries and the components and styles copied from them.
#[serde(default, skip_serializing_if = "Libraries::is_unused")]
pub libraries: Arc<Libraries>,
/// This file's published library; serialized separately by the format crate.
#[serde(skip)]
pub published: Option<Arc<PublishedLibrary>>,
/// The colour space colour values are in (sRGB unless the file says otherwise).
#[serde(default, skip_serializing_if = "ColorProfile::is_srgb")]
pub color_profile: ColorProfile,
/// Image adjustment presets saved in this file.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub image_presets: Vec<ImagePreset>,
/// Annotation categories (`None`: [`AnnotationCategory::defaults`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotation_categories: Option<Vec<AnnotationCategory>>,
/// What ready-for-dev frames and sections looked like before they changed (by layer; see
/// [`handoff`]).
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dev_snapshots: BTreeMap<NodeId, Arc<DevSnapshot>>,
}
impl Default for Document {
fn default() -> Self {
Self::new("Untitled")
}
}
impl Document {
/// A new file with one empty page ("Page 1").
pub fn new(name: impl Into<String>) -> Self {
let mut doc = Self::without_pages(name);
let p = doc.add_page("Page 1");
doc.current_page = Some(p);
doc
}
/// A document with no pages at all: not a valid file on its own (see [`repair`](Self::repair)),
/// only a base to build one onto, e.g. by applying a collaboration snapshot.
pub fn without_pages(name: impl Into<String>) -> Self {
Self {
name: name.into(),
pages: Vec::new(),
nodes: imbl::OrdMap::new(),
dirty: Dirty::default(),
next_id: IdAlloc { next: 1, client: 0 },
styles: Vec::new(),
variables: Variables::default(),
images: BTreeMap::new(),
current_page: None,
document_colors: Vec::new(),
comments: Vec::new(),
libraries: Arc::default(),
published: None,
color_profile: ColorProfile::Srgb,
image_presets: Vec::new(),
annotation_categories: None,
dev_snapshots: BTreeMap::new(),
}
}
/// Node ids touched since the last call (edited, added or removed), clearing the record.
pub fn take_dirty(&mut self) -> std::collections::BTreeSet<NodeId> {
std::mem::take(&mut self.dirty.0)
}
/// Record `ids` as touched, for passes that follow the dirty set (e.g. after changes made
/// elsewhere were applied).
pub fn mark_dirty(&mut self, ids: impl IntoIterator<Item = NodeId>) {
self.dirty.0.extend(ids);
}
/// Node ids touched so far (without clearing).
pub fn dirty(&self) -> &std::collections::BTreeSet<NodeId> {
&self.dirty.0
}
/// A fresh unused id (in this document's client space during a collaboration session; see
/// [`set_client_id`](Self::set_client_id)).
pub fn alloc_id(&mut self) -> NodeId {
self.next_id.alloc()
}
/// The id [`alloc_id`](Self::alloc_id) returns next.
pub fn next_id(&self) -> NodeId {
NodeId(self.next_id.next)
}
/// The collaboration client ids are allocated for (0: none, a single-user document).
pub fn client_id(&self) -> u32 {
self.next_id.client
}
/// Allocate new ids as `client << 32 | counter`, continuing after the highest id of that space
/// already in the document, so participants of a live session never make the same id. 0 goes
/// back to single-user allocation (after the highest id of any space).
pub fn set_client_id(&mut self, client: u32) {
let (lo, hi) = if client == 0 { (0, u64::MAX) } else { (u64::from(client) << 32, (u64::from(client) << 32) | u64::from(u32::MAX)) };
let highest = self.nodes.range(NodeId(lo)..=NodeId(hi)).next_back().map(|(k, _)| *k);
// Re-setting the same client keeps its counter (ids handed out stay handed out).
let keep = if self.next_id.client == client { self.next_id.next } else { 0 };
let start = if client == 0 { keep.max(1) } else { keep.max(lo | 1) };
self.next_id = IdAlloc { next: start, client };
if let Some(h) = highest {
self.next_id.reserve(h);
}
}
pub fn node(&self, id: NodeId) -> Option<&Node> {
self.nodes.get(&id).map(Arc::as_ref)
}
pub fn node_arc(&self, id: NodeId) -> Option<&Arc<Node>> {
self.nodes.get(&id)
}
/// Mutable access (copy-on-write when a snapshot shares the node).
pub fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
let n = self.nodes.get_mut(&id).map(Arc::make_mut);
if n.is_some() {
self.dirty.0.insert(id);
}
n
}
/// Undo a write that changed nothing: when node `old.id` equals `old` by value, `old` goes back
/// in its place (shared with the snapshots holding it again) and, unless `was_dirty`, the node
/// leaves the dirty set. Returns whether it did.
pub fn keep_if_unchanged(&mut self, old: Arc<Node>, was_dirty: bool) -> bool {
let id = old.id;
let same = self.nodes.get(&id).is_some_and(|n| Arc::ptr_eq(n, &old) || **n == *old);
if same {
self.nodes.insert(id, old);
if !was_dirty {
self.dirty.0.remove(&id);
}
}
same
}
pub fn get(&self, id: NodeId) -> DocResult<&Node> {
self.node(id).ok_or(DocError::NoNode(id))
}
pub fn get_mut(&mut self, id: NodeId) -> DocResult<&mut Node> {
self.node_mut(id).ok_or(DocError::NoNode(id))
}
pub fn contains(&self, id: NodeId) -> bool {
self.nodes.contains_key(&id)
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
/// Ids of the nodes added, removed or changed (by value) since `before`, ascending, or `None`
/// when more than `limit` differ. Structure the two versions share is skipped, so comparing an
/// edit with the version it came from costs about the size of the edit, not of the document.
pub fn changed_ids(&self, before: &Document, limit: usize) -> Option<Vec<NodeId>> {
let mut out = Vec::new();
for d in before.nodes.diff(&self.nodes) {
if out.len() >= limit {
return None;
}
out.push(match d {
imbl::ordmap::DiffItem::Add(k, _) | imbl::ordmap::DiffItem::Remove(k, _) => *k,
imbl::ordmap::DiffItem::Update { new: (k, _), .. } => *k,
});
}
Some(out)
}
/// Every node id (pages included), ascending.
pub fn ids(&self) -> impl Iterator<Item = NodeId> + '_ {
self.nodes.keys().copied()
}
pub fn nodes(&self) -> impl Iterator<Item = &Node> + '_ {
self.nodes.values().map(Arc::as_ref)
}
/// Add a page at the end.
pub fn add_page(&mut self, name: &str) -> NodeId {
let id = self.alloc_id();
let node = Node::new(
id,
NodeKind::Page { background: Color::DEFAULT_PAGE, flows: Vec::new(), prototype: Default::default(), guides: Vec::new() },
name,
0.0,
0.0,
);
self.nodes.insert(id, Arc::new(node));
self.dirty.0.insert(id);
self.pages.push(id);
id
}
/// The page a node is on (a page is on itself).
pub fn page_of(&self, id: NodeId) -> Option<NodeId> {
let mut cur = id;
for _ in 0..10_000 {
let n = self.node(cur)?;
if n.is_page() {
return Some(cur);
}
cur = n.parent?;
}
None
}
pub fn first_page(&self) -> Option<NodeId> {
self.pages.first().copied()
}
/// Insert `node` (its id must come from [`alloc_id`](Self::alloc_id)) as a child of `parent` at
/// `index` (None = on top).
pub fn insert(&mut self, mut node: Node, parent: NodeId, index: Option<usize>) -> DocResult<NodeId> {
let p = self.get(parent)?;
if !p.kind.is_container() {
return Err(DocError::NotContainer(parent));
}
let id = node.id;
if self.nodes.contains_key(&id) {
return Err(DocError::Invalid(format!("node {id} already exists")));
}
self.reserve_ids(id);
node.parent = Some(parent);
self.nodes.insert(id, Arc::new(node));
self.dirty.0.insert(id);
let p = self.get_mut(parent)?;
let at = index.unwrap_or(p.children.len()).min(p.children.len());
p.children.insert(at, id);
Ok(id)
}
/// Create a node of `kind` under `parent`, positioned at `(x, y)` in the parent's space.
pub fn create(&mut self, parent: NodeId, kind: NodeKind, name: &str, x: f64, y: f64, w: f64, h: f64) -> DocResult<NodeId> {
let id = self.alloc_id();
let mut node = Node::new(id, kind, name, w, h);
node.transform = Affine::translate((x, y));
self.insert(node, parent, None)
}
/// Whether `ancestor` is `id` or one of its ancestors.
pub fn is_ancestor(&self, ancestor: NodeId, id: NodeId) -> bool {
let mut cur = Some(id);
let mut guard = 0;
while let Some(c) = cur {
if c == ancestor {
return true;
}
guard += 1;
if guard > 10_000 {
return false;
}
cur = self.node(c).and_then(|n| n.parent);
}
false
}
/// Ancestors from the parent up to (and including) the page.
pub fn ancestors(&self, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let mut cur = self.node(id).and_then(|n| n.parent);
while let Some(c) = cur {
if out.len() > 10_000 {
break;
}
out.push(c);
cur = self.node(c).and_then(|n| n.parent);
}
out
}
/// `id` and all its descendants, pre-order.
pub fn subtree(&self, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let mut stack = vec![id];
while let Some(c) = stack.pop() {
if out.len() > 5_000_000 {
break;
}
let Some(n) = self.node(c) else { continue };
out.push(c);
stack.extend(n.children.iter().rev());
}
out
}
/// Remove a node and its subtree. Pages can only be removed while another page remains.
pub fn remove(&mut self, id: NodeId) -> DocResult<()> {
let node = self.get(id)?;
let parent = node.parent;
if node.is_page() {
if self.pages.len() <= 1 {
return Err(DocError::Invalid("a file needs at least one page".into()));
}
self.pages.retain(|p| *p != id);
if self.current_page == Some(id) {
self.current_page = self.pages.first().copied();
}
}
if let Some(parent) = parent
&& let Some(p) = self.node_mut(parent)
{
p.children.retain(|c| *c != id);
}
for d in self.subtree(id) {
self.nodes.remove(&d);
self.dirty.0.insert(d);
}
Ok(())
}
/// Index of `id` among its siblings.
pub fn index_in_parent(&self, id: NodeId) -> Option<usize> {
let parent = self.node(id)?.parent?;
self.node(parent)?.children.iter().position(|c| *c == id)
}
/// Move `id` under `new_parent` at `index` (None = top), keeping its world position.
pub fn reparent(&mut self, id: NodeId, new_parent: NodeId, index: Option<usize>) -> DocResult<()> {
if self.is_ancestor(id, new_parent) {
return Err(DocError::Cycle(id, new_parent));
}
let np = self.get(new_parent)?;
if !np.kind.is_container() {
return Err(DocError::NotContainer(new_parent));
}
let node = self.get(id)?;
if node.is_page() {
return Err(DocError::Invalid("pages can't be moved into nodes".into()));
}
let world = self.world_transform(id);
let old_parent = node.parent;
let old_index = self.index_in_parent(id);
if let Some(op) = old_parent
&& let Some(p) = self.node_mut(op)
{
p.children.retain(|c| *c != id);
}
let parent_world = self.world_transform(new_parent);
let np = self.get_mut(new_parent)?;
let mut at = index.unwrap_or(np.children.len());
// Moving within the same parent: the removal shifted later indices.
if old_parent == Some(new_parent)
&& let (Some(oi), Some(_)) = (old_index, index)
&& oi < at
{
at = at.saturating_sub(1);
}
let at = at.min(np.children.len());
np.children.insert(at, id);
let n = self.get_mut(id)?;
n.parent = Some(new_parent);
n.transform = parent_world.inverse() * world;
Ok(())
}
/// Add page node `node` (its id unused; no parent, no children) at the end of the page list.
pub fn insert_page(&mut self, mut node: Node) -> DocResult<NodeId> {
let id = node.id;
if !node.is_page() {
return Err(DocError::Invalid(format!("{id} is not a page")));
}
if self.nodes.contains_key(&id) {
return Err(DocError::Invalid(format!("node {id} already exists")));
}
self.reserve_ids(id);
node.parent = None;
node.children.clear();
self.nodes.insert(id, Arc::new(node));
self.dirty.0.insert(id);
self.pages.push(id);
Ok(id)
}
/// Move `id` under `parent` at `index` (clamped) keeping its local transform, unlike
/// [`reparent`](Self::reparent), which keeps its place on the page.
pub fn place(&mut self, id: NodeId, parent: NodeId, index: usize) -> DocResult<()> {
if self.get(id)?.is_page() {
return Err(DocError::Invalid("pages can't be moved into nodes".into()));
}
if !self.get(parent)?.kind.is_container() {
return Err(DocError::NotContainer(parent));
}
if self.is_ancestor(id, parent) {
return Err(DocError::Cycle(id, parent));
}
if let Some(old) = self.get(id)?.parent
&& let Some(p) = self.node_mut(old)
{
p.children.retain(|c| *c != id);
}
let p = self.get_mut(parent)?;
let at = index.min(p.children.len());
p.children.insert(at, id);
self.get_mut(id)?.parent = Some(parent);
Ok(())
}
/// Reorder `id` within its parent to `index`.
pub fn reorder(&mut self, id: NodeId, index: usize) -> DocResult<()> {
let parent = self.get(id)?.parent.ok_or(DocError::Invalid("page".into()))?;
let p = self.get_mut(parent)?;
p.children.retain(|c| *c != id);
let at = index.min(p.children.len());
p.children.insert(at, id);
Ok(())
}
/// Transform from `id`'s local space to its page.
pub fn world_transform(&self, id: NodeId) -> Affine {
let mut xf = Affine::IDENTITY;
let mut cur = Some(id);
let mut guard = 0;
while let Some(c) = cur {
let Some(n) = self.node(c) else { break };
if n.is_page() {
break;
}
xf = n.transform * xf;
cur = n.parent;
guard += 1;
if guard > 10_000 {
break;
}
}
xf
}
/// Set `id`'s transform so that its world transform becomes `world`.
pub fn set_world_transform(&mut self, id: NodeId, world: Affine) -> DocResult<()> {
let parent = self.get(id)?.parent;
let pw = parent.map(|p| self.world_transform(p)).unwrap_or(Affine::IDENTITY);
self.get_mut(id)?.transform = pw.inverse() * world;
Ok(())
}
/// Axis-aligned bounds of the node's box in page coordinates.
pub fn world_bounds(&self, id: NodeId) -> Option<Rect> {
let n = self.node(id)?;
if n.is_page() {
return self.page_content_bounds(id);
}
Some(figcraft_geom::transform_rect_bbox(self.world_transform(id), n.local_rect()))
}
/// Bounds of everything painted by a node, effects and strokes included (page coordinates).
pub fn world_render_bounds(&self, id: NodeId) -> Option<Rect> {
let n = self.node(id)?;
let reach = n.paint_reach();
let mut r = figcraft_geom::transform_rect_bbox(self.world_transform(id), n.local_rect().inflate(reach, reach));
if !(n.clip && n.kind.is_frame_like()) || n.kind.bounds_follow_children() {
for c in &n.children {
if let Some(cr) = self.world_render_bounds(*c) {
r = r.union(cr);
}
}
}
Some(r)
}
/// Union of the top-level nodes' bounds on a page.
pub fn page_content_bounds(&self, page: NodeId) -> Option<Rect> {
let p = self.node(page)?;
p.children.iter().filter_map(|c| self.world_bounds(*c)).reduce(|a, b| a.union(b))
}
/// Union of several nodes' world bounds.
pub fn bounds_of(&self, ids: &[NodeId]) -> Option<Rect> {
ids.iter().filter_map(|i| self.world_bounds(*i)).reduce(|a, b| a.union(b))
}
/// Resize a group/boolean so its box tightly wraps its children (children keep world position).
pub fn fit_to_children(&mut self, id: NodeId) -> DocResult<()> {
let n = self.get(id)?;
if n.children.is_empty() {
return Ok(());
}
let world = self.world_transform(id);
let inv = world.inverse();
// Children bounds in the group's current local space.
let mut local: Option<Rect> = None;
for c in &n.children {
let Some(cn) = self.node(*c) else { continue };
let r = figcraft_geom::transform_rect_bbox(inv * self.world_transform(*c), cn.local_rect());
local = Some(local.map_or(r, |l| l.union(r)));
}
let Some(local) = local else { return Ok(()) };
let children = n.children.clone();
let child_worlds: Vec<(NodeId, Affine)> = children.iter().map(|c| (*c, self.world_transform(*c))).collect();
let shift = Affine::translate((local.x0, local.y0));
let nw = world * shift;
{
let g = self.get_mut(id)?;
g.transform *= shift;
g.width = local.width();
g.height = local.height();
}
let ninv = nw.inverse();
for (c, w) in child_worlds {
if let Some(cn) = self.node_mut(c) {
cn.transform = ninv * w;
}
}
Ok(())
}
/// Fit every group/boolean ancestor of `id` (innermost first).
pub fn fit_ancestors(&mut self, id: NodeId) {
for a in self.ancestors(id) {
let follows = self.node(a).is_some_and(|n| n.kind.bounds_follow_children());
if follows {
let _ = self.fit_to_children(a);
} else {
break;
}
}
}
/// Deep-copy `id`'s subtree under `parent` (at `index`), returning the new root id. The copy
/// keeps the original's local transform; ids are fresh.
pub fn duplicate_into(&mut self, id: NodeId, parent: NodeId, index: Option<usize>) -> DocResult<NodeId> {
let mut map = BTreeMap::new();
let src_nodes: Vec<Node> = self.subtree(id).into_iter().filter_map(|i| self.node(i).cloned()).collect();
for n in &src_nodes {
let nid = self.alloc_id();
map.insert(n.id, nid);
}
self.paste_nodes(&src_nodes, &map, id, parent, index)
}
/// Insert copies of `nodes` (a subtree rooted at `root`, from this or another document) with ids
/// remapped by `map`. Interactions and instance references pointing inside the subtree are
/// remapped too.
pub fn paste_nodes(
&mut self,
nodes: &[Node],
map: &BTreeMap<NodeId, NodeId>,
root: NodeId,
parent: NodeId,
index: Option<usize>,
) -> DocResult<NodeId> {
let p = self.get(parent)?;
if !p.kind.is_container() {
return Err(DocError::NotContainer(parent));
}
let new_root = *map.get(&root).ok_or(DocError::NoNode(root))?;
for n in nodes {
let Some(new_id) = map.get(&n.id) else { continue };
let mut c = n.clone();
c.id = *new_id;
c.children = n.children.iter().filter_map(|k| map.get(k).copied()).collect();
c.parent = if n.id == root { Some(parent) } else { n.parent.and_then(|p| map.get(&p).copied()) };
for it in &mut c.interactions {
proto::walk_mut(&mut it.actions, &mut |a| remap_action(a, map));
}
if let NodeKind::Instance { component, .. } = &mut c.kind
&& let Some(m) = map.get(component)
{
*component = *m;
}
self.reserve_ids(*new_id);
self.nodes.insert(*new_id, Arc::new(c));
self.dirty.0.insert(*new_id);
}
let p = self.get_mut(parent)?;
let at = index.unwrap_or(p.children.len()).min(p.children.len());
p.children.insert(at, new_root);
Ok(new_root)
}
/// Find nodes on `page` (or all pages) whose name contains `query` (case-insensitive).
pub fn find_by_name(&self, query: &str, page: Option<NodeId>) -> Vec<NodeId> {
let q = query.to_lowercase();
let roots: Vec<NodeId> = page.map(|p| vec![p]).unwrap_or_else(|| self.pages.clone());
roots
.into_iter()
.flat_map(|r| self.subtree(r))
.filter(|i| self.node(*i).is_some_and(|n| !n.is_page() && n.name.to_lowercase().contains(&q)))
.collect()
}
/// The nearest frame-like ancestor (excluding `id` itself).
pub fn containing_frame(&self, id: NodeId) -> Option<NodeId> {
self.ancestors(id).into_iter().find(|a| self.node(*a).is_some_and(|n| n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. })))
}
/// The top-level frame containing `id` (or `id` if it is one): the outermost frame, on the
/// page or in a section (sections nest and hold top-level frames).
pub fn top_level_frame(&self, id: NodeId) -> Option<NodeId> {
let mut chain = vec![id];
chain.extend(self.ancestors(id));
// chain ends with the page; below it, past any sections, is the top-level node.
let page_pos = chain.iter().position(|c| self.node(*c).is_some_and(Node::is_page))?;
let top = chain.iter().take(page_pos).rev().copied().find(|c| !self.node(*c).is_some_and(|n| matches!(n.kind, NodeKind::Section { .. })))?;
self.node(top).filter(|n| n.kind.is_frame_like()).map(|_| top)
}
/// Top-level frames of a page (also those in sections) in reading order (rows top to bottom,
/// then left to right).
pub fn top_frames(&self, page: NodeId) -> Vec<NodeId> {
self.frames_in(page)
}
/// The top-level frames inside page or section `container` (and its sections, at any depth), in
/// reading order.
pub fn frames_in(&self, container: NodeId) -> Vec<NodeId> {
fn walk(doc: &Document, id: NodeId, depth: usize, out: &mut Vec<(NodeId, Rect)>) {
let Some(n) = doc.node(id) else { return };
for c in &n.children {
let Some(cn) = doc.node(*c) else { continue };
if cn.kind.is_frame_like() {
if let Some(b) = doc.world_bounds(*c) {
out.push((*c, b));
}
} else if matches!(cn.kind, NodeKind::Section { .. }) && depth < 64 {
walk(doc, *c, depth + 1, out);
}
}
}
let mut v = Vec::new();
walk(self, container, 0, &mut v);
v.sort_by(|a, b| a.1.y0.total_cmp(&b.1.y0).then(a.1.x0.total_cmp(&b.1.x0)));
v.into_iter().map(|(i, _)| i).collect()
}
/// The sections on page `page` (nested ones after their section), in layer order.
pub fn sections_in(&self, page: NodeId) -> Vec<NodeId> {
fn walk(doc: &Document, id: NodeId, depth: usize, out: &mut Vec<NodeId>) {
let Some(n) = doc.node(id) else { return };
for c in &n.children {
if doc.node(*c).is_some_and(|cn| matches!(cn.kind, NodeKind::Section { .. })) && depth < 64 {
out.push(*c);
walk(doc, *c, depth + 1, out);
}
}
}
let mut v = Vec::new();
walk(self, page, 0, &mut v);
v
}
/// A unique name like "Frame 3" for a new node of a kind.
pub fn next_name(&self, base: &str) -> String {
let mut max = 0u64;
for n in self.nodes() {
if let Some(rest) = n.name.strip_prefix(base)
&& let Some(num) = rest.strip_prefix(' ')
&& let Ok(k) = num.parse::<u64>()
{
max = max.max(k);
}
}
format!("{base} {}", max + 1)
}
pub fn style(&self, id: &str) -> Option<&Style> {
self.styles.iter().find(|s| s.id == id || s.name == id)
}
/// The effective variable modes for a node (walking up its ancestors).
pub fn modes_for(&self, id: NodeId) -> BTreeMap<CollectionId, ModeId> {
let mut out = BTreeMap::new();
let mut chain = vec![id];
chain.extend(self.ancestors(id));
for c in chain.into_iter().rev() {
if let Some(n) = self.node(c) {
for (k, v) in &n.var_modes {
out.insert(k.clone(), v.clone());
}
}
}
out
}
/// Check structural invariants (used by tests and after loading untrusted files).
pub fn validate(&self) -> Vec<String> {
let mut issues = Vec::new();
for (id, n) in self.nodes.iter() {
if n.id != *id {
issues.push(format!("node key {id} has id {}", n.id));
}
for c in &n.children {
match self.node(*c) {
Some(cn) if cn.parent == Some(*id) => {}
Some(_) => issues.push(format!("child {c} of {id} has another parent")),
None => issues.push(format!("child {c} of {id} missing")),
}
}
if let Some(p) = n.parent
&& !self.node(p).is_some_and(|pn| pn.children.contains(id))
{
issues.push(format!("{id} not listed in parent {p}"));
}
// The library store's root is a page that is never listed.
let listed = self.pages.contains(id) || (n.is_page() && self.libraries.store == Some(*id));
if n.is_page() != listed {
issues.push(format!("page list mismatch for {id}"));
}
}
issues
}
/// Repair what [`validate`](Self::validate) reports (lenient loading of untrusted files).
pub fn repair(&mut self) {
let ids: Vec<NodeId> = self.nodes.keys().copied().collect();
for id in &ids {
if let Some(n) = self.nodes.get_mut(id) {
let n = Arc::make_mut(n);
n.id = *id;
}
}
// Drop dangling children and children claimed by another parent.
let parents: BTreeMap<NodeId, Option<NodeId>> = self.nodes.iter().map(|(k, n)| (*k, n.parent)).collect();
for id in &ids {
let Some(n) = self.nodes.get(id) else { continue };
let keep: Vec<NodeId> = n.children.iter().copied().filter(|c| parents.get(c).copied().flatten() == Some(*id)).collect();
if keep != n.children
&& let Some(n) = self.nodes.get_mut(id)
{
Arc::make_mut(n).children = keep;
}
}
// Orphans (parent missing or not listing them) are removed.
loop {
let orphan = self.nodes.iter().find_map(|(k, n)| {
if n.is_page() {
return None;
}
match n.parent {
Some(p) if self.nodes.get(&p).is_some_and(|pn| pn.children.contains(k)) => None,
_ => Some(*k),
}
});
let Some(o) = orphan else { break };
for d in self.subtree(o) {
self.nodes.remove(&d);
}
}
// Snapshots of layers that are gone or no longer changed since marked ready for dev, or that
// don't start with their layer, are dropped.
self.dev_snapshots.retain(|k, snap| {
self.nodes.get(k).is_some_and(|n| n.dev_status == DevStatus::ReadyForDev && n.dev_changed)
&& snap.nodes.first().is_some_and(|n| n.id == *k)
});
let store = self.libraries.store.filter(|s| self.nodes.get(s).is_some_and(|n| n.is_page()));
if store != self.libraries.store {
Arc::make_mut(&mut self.libraries).store = store;
}
self.pages.retain(|p| self.nodes.get(p).is_some_and(|n| n.is_page()) && Some(*p) != store);
for (k, n) in self.nodes.iter() {
if n.is_page() && !self.pages.contains(k) && Some(*k) != store {
self.pages.push(*k);
}
}
if self.pages.is_empty() {
let p = self.add_page("Page 1");
self.current_page = Some(p);
}
let client = self.client_id();
self.set_client_id(client);
if self.current_page.is_none_or(|p| !self.pages.contains(&p)) {
self.current_page = self.pages.first().copied();
}
self.link_layer_text_styles();
}
/// Text layers linked to a text style whose chars carry no link (files from before per-char
/// links, importers that link layers): the chars showing the style link to it, and the layer
/// stays linked only if they all do.
pub fn link_layer_text_styles(&mut self) {
let defs: BTreeMap<String, TextStyle> = self
.styles
.iter()
.filter_map(|s| match &s.kind {
StyleKind::Text { style } => Some((s.id.clone(), style.clone())),
_ => None,
})
.collect();
let legacy = |n: &Node| match &n.kind {
NodeKind::Text(t) => n.styles.text.is_some() && t.spans().iter().all(|(_, _, s)| s.style_id.is_none()),
_ => false,
};
let ids: Vec<NodeId> = self.nodes.iter().filter(|(_, n)| legacy(n)).map(|(k, _)| *k).collect();
for id in ids {
let Some(n) = self.nodes.get_mut(&id) else { continue };
let n = Arc::make_mut(n);
let sid = n.styles.text.clone().unwrap_or_default();
if let NodeKind::Text(t) = &mut n.kind {
if let Some(def) = defs.get(&sid) {
t.each_style(|s| {
if s.shows_text_style(def) {
s.style_id = Some(sid.clone());
}
});
}
n.styles.text = t.text_style_link().flatten().map(str::to_string);
}
}
}
/// Hit test: the topmost visible, unlocked node under `pt` on `page` for a click.
/// `deep` selects the innermost leaf (Cmd-click); otherwise top-level children of frames
/// resolve like Figma's click-through rules (see [`geometry::select_target`]).
pub fn hit_test(&self, page: NodeId, pt: Point, tolerance: f64) -> Option<NodeId> {
geometry::hit_leaf(self, page, pt, tolerance)
}
}
fn remap_action(a: &mut proto::Action, map: &BTreeMap<NodeId, NodeId>) {
use proto::Action::*;
match a {
Navigate { dest, .. } | ScrollTo { dest } | OpenOverlay { dest, .. } | SwapOverlay { dest } | ChangeTo { dest } => {
if let Some(m) = map.get(dest) {
*dest = *m;
}
}
_ => {}
}
}
#[cfg(test)]
mod blur_tests;
#[cfg(test)]
mod boolean_hit_tests;
#[cfg(test)]
mod client_id_tests;
#[cfg(test)]
mod colors_tests;
#[cfg(test)]
mod diff_tests;
#[cfg(test)]
mod effects_tests;
#[cfg(test)]
mod handoff_tests;
#[cfg(test)]
mod ids_tests;
#[cfg(test)]
mod print_tests;
#[cfg(test)]
mod proto_expr_mode_tests;
#[cfg(test)]
mod proto_expr_parse_tests;
#[cfg(test)]
mod proto_expr_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod width_sides_tests;
#[cfg(test)]
mod width_tests;