Skip to main content

blockworx/document/
mod.rs

1//! The legacy document model: dormant since the flag day (F7), demolished in
2//! phase 7.
3//!
4//! Nothing in the editor reads a legacy `Document` any more — the bridge
5//! (`crate::schema::lower`) reads the *schema*, and everything downstream is
6//! `blockworx_doc`. What survives here is what the schema parse half and the
7//! tutorial embeds still compile against, plus the handful of value types the
8//! editor kept (`GridPos`, `Lock`, `LinearDistance`). Being unplugged, most of
9//! it has no caller, so `dead_code` is allowed module-wide rather than
10//! decorated item by item — the lid comes off by deleting the module, not by
11//! finding callers for it.
12#![allow(dead_code)]
13
14pub mod auto_route;
15pub mod change;
16pub mod coord;
17pub mod decorations;
18pub mod image;
19pub mod label;
20pub mod linear_distance;
21pub mod model;
22pub mod pin_side;
23pub mod pin_type;
24pub mod schema_convert;
25
26pub use auto_route::AutoRoute;
27pub use coord::{GridPos, GridRect, GridSize, GridVec};
28pub use decorations::Decorations;
29pub use image::{Asset, Image, ImageData};
30pub use label::{BlockLabel, LabelSide};
31pub use linear_distance::LinearDistance;
32pub use model::Document;
33pub use pin_side::PinSide;
34pub use pin_type::PinType;
35
36use serde::{Deserialize, Serialize};
37
38use crate::presentation::store::{IdMap, IdSet};
39use crate::store::{CommentId, ImageId, PinId, RectId, RouteId, TextId};
40
41/// A node in the persistent document tree.
42///
43/// `pins` of a Block, when the Block is the one whose interior is being drawn,
44/// are visualized as Ports on the canvas boundary. When the Block is a child
45/// (in some other Block's `children`), it is visualized as a rect with its
46/// pins on its edges.
47#[derive(Clone, Debug, PartialEq)]
48pub struct Block {
49    pub inner: GridRect,
50    /// The block's label decorations. `decorations.title.name` is the single
51    /// source of truth for the block's name.
52    pub decorations: Decorations,
53    pub pins: IdMap<PinId, PinPort>,
54    /// Ids of this block's children. The child `Block`s themselves live in the
55    /// document-global [`Document::blocks`] map; this set only records which of
56    /// them are this block's children and in what order (render z-order). Stored
57    /// as an [`IdSet`] — insertion-ordered, so child z-order is stable.
58    pub children: IdSet<RectId>,
59    pub routes: IdMap<RouteId, AutoRoute>,
60    /// Free-floating text annotations placed on this block's interior. They
61    /// carry no pins and never participate in routing.
62    pub texts: IdMap<TextId, TextBox>,
63    /// Free-floating boundary [`Comment`]s placed on this block's interior. Like
64    /// [`texts`](Self::texts) they carry no pins, never participate in routing,
65    /// and are not part of the hierarchy (never listed in any block's
66    /// `children`). They render on a layer above the blocks and routes.
67    pub comments: IdMap<CommentId, Comment>,
68    /// Free-floating background image annotations placed on this block's
69    /// interior. Like [`texts`](Self::texts) and [`comments`](Self::comments)
70    /// they carry no pins, never participate in routing, and are not part of the
71    /// hierarchy. They paint behind everything (the background layer).
72    pub images: IdMap<ImageId, Image>,
73    /// This block's optional foreground icon: a [`Image`] centered on the block
74    /// that travels with it. Unlike a background [`images`](Self::images) image
75    /// it is only selectable while the block itself is selected.
76    pub icon: Option<Image>,
77    /// Optional accent index (`0..=7`) selecting the block's outline color via
78    /// [`Accent0`](crate::theme::Role::Accent0)..[`Accent7`](crate::theme::Role::Accent7);
79    /// `None` uses [`AccentDefault`](crate::theme::Role::AccentDefault).
80    pub role: Option<u8>,
81    /// Whether the block's pin/port interface is frozen. A locked block can still
82    /// be moved, deleted, resized, recolored, and have its title/type edited, but
83    /// its pins and ports are protected from material edits (rename, retype, tag
84    /// text, add, delete).
85    pub locked: bool,
86}
87
88/// Whether an interface is frozen against edits — the parameter form of a
89/// `locked` flag, so a call site reads `Lock::Locked` instead of a bare `true`.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
91pub enum Lock {
92    Locked,
93    Unlocked,
94}
95
96impl Lock {
97    pub fn is_locked(self) -> bool {
98        self == Lock::Locked
99    }
100}
101
102impl From<bool> for Lock {
103    fn from(locked: bool) -> Self {
104        if locked { Lock::Locked } else { Lock::Unlocked }
105    }
106}
107
108/// Whether a pin/port draws its tag. The parameter form of `tag_hidden`.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
110pub enum TagVisibility {
111    Shown,
112    Hidden,
113}
114
115impl TagVisibility {
116    pub fn is_hidden(self) -> bool {
117        self == TagVisibility::Hidden
118    }
119}
120
121/// Whether a block's pin interface accepts edits. The parameter form of
122/// `locked`.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum InterfaceLock {
125    Locked,
126    Unlocked,
127}
128
129impl InterfaceLock {
130    pub fn is_locked(self) -> bool {
131        self == InterfaceLock::Locked
132    }
133}
134
135impl From<bool> for InterfaceLock {
136    fn from(locked: bool) -> Self {
137        if locked {
138            InterfaceLock::Locked
139        } else {
140            InterfaceLock::Unlocked
141        }
142    }
143}
144
145impl From<bool> for TagVisibility {
146    fn from(hidden: bool) -> Self {
147        if hidden {
148            TagVisibility::Hidden
149        } else {
150            TagVisibility::Shown
151        }
152    }
153}
154
155/// A free-floating multi-line text annotation. Unlike a [`Block`] it has no
156/// pins, title, tag, or bounding box — just text pinned at a single grid
157/// `anchor`. It implements [`crate::shape::BaseShape`] so it can be
158/// selected, moved, and deleted through the same machinery as blocks and ports
159/// (but it is never resized — its extent is whatever the text needs).
160#[derive(Clone, Debug, PartialEq)]
161pub struct TextBox {
162    /// The annotation's content. May contain `\n` for multiple lines; the user
163    /// governs the layout, there is no wrapping.
164    pub text: String,
165    /// Top-left anchor (grid coordinate). Text flows right/down from here.
166    /// The measured on-canvas extent is derived state
167    /// ([`crate::presentation::TextExtents`]), not stored here.
168    pub anchor: GridPos,
169    /// Optional accent index (`0..=7`) selecting the outline color; `None`
170    /// strokes with [`crate::theme::Role::TextBoxStroke`].
171    pub role: Option<u8>,
172}
173
174/// A free-floating boundary annotation: a rectangle drawn as an outline only
175/// (no fill) with a single repositionable [`BlockLabel`] title. Unlike a
176/// [`Block`] it has no pins, tag, children, or routes and never participates in
177/// the hierarchy — it exists purely to group components visually. It implements
178/// [`crate::shape::BaseShape`] so it is selected, moved, resized, and renamed
179/// through the same machinery as blocks, and it renders on a layer above the
180/// blocks and routes.
181#[derive(Clone, Debug, PartialEq)]
182pub struct Comment {
183    /// Position and size in grid coordinates (resizable, like a block).
184    pub inner: GridRect,
185    /// The comment's repositionable title. `title.name` is the comment's name.
186    pub title: BlockLabel,
187    /// Optional accent index (`0..=7`) selecting the outline color; `None`
188    /// strokes with [`crate::theme::Role::CommentStroke`].
189    pub role: Option<u8>,
190}
191
192/// Unified Pin/Port data structure. When the entry lives in a child block's
193/// `pins` store it is acting as an inner pin and `rect` is unused (the
194/// position is computed from the parent block's rect + `side`/`offset`).
195/// When the entry lives in the current block's `pins` store it is rendered
196/// as a Port and `rect` carries its on-canvas position.
197#[derive(Clone, Debug, PartialEq)]
198pub struct PinPort {
199    /// The pin's primary label, a single line. (Legacy documents may carry a
200    /// second line after a `\n`; it is ignored — the render/edit paths use only
201    /// the first line.)
202    pub name: String,
203    /// The pin's secondary label — its "type" — drawn a little smaller, below the
204    /// name. Single-line, like `name`. Written on disk as `type`. Distinct from
205    /// [`PinType`] (`kind`), which is the I/O direction, not free text.
206    pub type_label: String,
207    /// A short user-editable label — the pin's "location" designator (e.g. "A1").
208    /// Defaults to the pin's id (as a string) at creation.
209    pub tag: String,
210    /// Whether the `tag` label is hidden. Kept beside `tag` since it governs it.
211    /// The user toggles it via the per-pin eye button shown when the pin's
212    /// owning shape is selected. (There is deliberately no `name`-visibility
213    /// flag: an unlabelled pin breaks the hierarchy, so to drop a label the user
214    /// clears its text instead.)
215    pub tag_hidden: bool,
216    pub side: PinSide,
217    pub offset: u32,
218    pub rect: GridRect,
219    /// Signal direction, stored from the pin's point of view. Flipped when
220    /// this `PinPort` is rendered as a port. See [`PinType`].
221    pub kind: PinType,
222    /// The user-picked accent of this pin drawn as a boundary port. The
223    /// stub accents that used to sit beside it (`pin_accent`,
224    /// `port_pin_accent`) are propagated from route roles — derived
225    /// state, computed in [`crate::presentation::PinAccents`], never stored
226    /// here.
227    pub port_accent: Option<u8>,
228    /// Which way the stub faces when this `PinPort` is drawn as a boundary
229    /// *port*, independent of `side` (which edge of the parent it occupies as a
230    /// pin). `None` derives it as `side.flip()` — a port faces opposite the edge
231    /// its pin sits on, so its interior stub points inward. The flip-L/R tool
232    /// sets it explicitly (and a parent flip freezes it) so it stops tracking
233    /// `side`. Ignored when this `PinPort` is drawn as a block's edge pin.
234    /// Written on disk as `facing`.
235    pub port_orientation: Option<PinSide>,
236}
237
238#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
239pub struct Waypoint {
240    pub pos: GridPos,
241    pub locked: bool,
242}
243
244/// Absolute reference to a pin on either the current block (`Port`) or
245/// a child block (`Pin`). Ordered so anchors can key an ordered set — the
246/// order is arbitrary but stable, which is what a change set needs.
247#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
248pub enum LineAnchor {
249    Port(PinId),
250    Pin { block: RectId, pin: PinId },
251}
252
253/// The on-disk spelling: `p2` for a port (relative to the block owning the
254/// route) and `b3:p2` for a child block's pin.
255impl std::fmt::Display for LineAnchor {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        match self {
258            LineAnchor::Port(p) => write!(f, "{p}"),
259            LineAnchor::Pin { block, pin } => write!(f, "{block}:{pin}"),
260        }
261    }
262}
263
264/// Width (grid cells) a top block is born with. Deliberately small and fixed
265/// rather than derived from what the block contains: the top is a block like any
266/// other once a level is added above it, and a size the user drags to fit beats a
267/// rect as large as the whole drawing.
268pub const TOP_BLOCK_DEFAULT_WIDTH: u32 = 8;
269/// Height (grid cells) a top block is born with. One of the valid block heights
270/// (see [`crate::grid::snap_block_height_cells`]), leaving room for five pin
271/// slots.
272pub const TOP_BLOCK_DEFAULT_HEIGHT: u32 = 16;
273
274/// The rect a top block is born with, at the world origin.
275pub const TOP_BLOCK_DEFAULT_RECT: GridRect = GridRect::new(
276    GridPos::new(0, 0),
277    GridSize::new(TOP_BLOCK_DEFAULT_WIDTH, TOP_BLOCK_DEFAULT_HEIGHT),
278);
279
280impl Default for Block {
281    fn default() -> Self {
282        Self {
283            inner: TOP_BLOCK_DEFAULT_RECT,
284            decorations: Decorations {
285                title: BlockLabel {
286                    name: "top".to_string(),
287                    ..BlockLabel::default()
288                },
289                ..Decorations::default()
290            },
291            pins: IdMap::default(),
292            children: IdSet::default(),
293            routes: IdMap::default(),
294            texts: IdMap::default(),
295            comments: IdMap::default(),
296            images: IdMap::default(),
297            icon: None,
298            role: None,
299            locked: false,
300        }
301    }
302}