//! 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 mod vector_regions; 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 { // Accept "12" and Figma-style "0:12". let t = s.trim(); let t = t.rsplit(':').next().unwrap_or(t); t.parse::().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 = Result; /// The set of touched node ids, and whether any layer is a mask (worked out when first asked /// after a change: hit testing skips its mask checks without one). Never part of equality or /// serialisation. #[derive(Clone, Debug, Default)] pub struct Dirty(pub std::collections::BTreeSet, std::sync::OnceLock); impl Dirty { /// Record node `id` as touched (edited, added or removed). fn touch(&mut self, id: NodeId) { self.0.insert(id); self.1 = std::sync::OnceLock::new(); } } 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>, 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, nodes: imbl::OrdMap>, /// 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