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}