Skip to main content

blockworx/doc_ng/
block_model.rs

1//! The block model: entities and namespaces of the document.
2//! Rationale: `docs/doc-ng-design-notes.md`.
3
4use crate::doc_ng::{
5    entity::{Entity, entity},
6    geometry::{FracVal, GridPoint, GridRect, ScreenRect, Waypoint},
7    hash::AssetHash,
8    id::{BlockId, CommentId, ImageId, PinId, RouteId, RouteLabelId, TextId},
9    register::{Applied, Register},
10    values::{LabelSide, PinDir, Role},
11    write_order::WriteOrder,
12};
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15
16#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize)]
17pub enum Liveness {
18    Alive,
19    #[default]
20    Deleted,
21}
22
23/// Presence register + retained inner — entities only. Edit commands cannot
24/// target `presence`, so delete-wins is structural.
25#[derive(Default, Clone, Serialize)]
26pub struct Live<T> {
27    pub presence: Register<Liveness>,
28    pub inner: T,
29}
30
31impl<T: Entity> Live<T> {
32    pub fn new(inner: T, order: WriteOrder) -> Self {
33        Self {
34            presence: Register::new(&Liveness::Alive, order),
35            inner,
36        }
37    }
38    pub fn delete(&mut self, order: WriteOrder) -> Applied {
39        self.presence.apply(&Liveness::Deleted, order)
40    }
41    pub fn restore(&mut self, order: WriteOrder) -> Applied {
42        self.presence.apply(&Liveness::Alive, order)
43    }
44    pub fn is_alive(&self) -> bool {
45        self.presence.as_ref() == &Liveness::Alive
46    }
47    pub fn alive(&self) -> Option<&T> {
48        if self.is_alive() {
49            Some(&self.inner)
50        } else {
51            None
52        }
53    }
54    /// Applies even when tombstoned: the retained inner keeps absorbing
55    /// updates, which surface on `Restore` — delete-wins is structural
56    /// (edits cannot target `presence`), not a drop policy.
57    pub fn apply_update(&mut self, update: &T::Update, order: WriteOrder) -> Applied {
58        self.inner.apply(update, order)
59    }
60    pub fn max_order(&self) -> WriteOrder {
61        self.presence.order().max(self.inner.max_order())
62    }
63}
64
65impl<T> AsRef<T> for Live<T> {
66    fn as_ref(&self) -> &T {
67        &self.inner
68    }
69}
70
71/// One atomic value on its block; the zero icon (null hash, empty rect)
72/// means "no image".
73#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
74pub struct Icon {
75    pub asset: AssetHash,
76    pub rect: ScreenRect,
77}
78
79entity! {
80    /// A namespace of four independent registers — no id, no lifecycle.
81    pub struct Label(init LabelInit, update LabelUpdate, id ()) {
82        registers {
83            Name => name: String,
84            Side => side: LabelSide,
85            Offset => offset: FracVal,
86            Hidden => hidden: bool,
87        }
88        namespaces {}
89        constants {}
90    }
91}
92
93entity! {
94    pub struct Block(init BlockInit, update BlockUpdate, id BlockId) {
95        registers {
96            /// `Id::NULL` = the document itself.
97            Parent => parent: BlockId,
98            Rect => rect: GridRect,
99            Locked => locked: bool,
100            Icon => icon: Icon,
101        }
102        namespaces {
103            Title => title: Label,
104            TypeLabel => type_label: Label,
105        }
106        constants {}
107    }
108}
109
110entity! {
111    pub struct Pin(init PinInit, update PinUpdate, id PinId) {
112        registers {
113            Owner => owner: BlockId,
114            Name => name: String,
115            TypeName => type_name: String,
116            Tag => tag: String,
117            TagHidden => tag_hidden: bool,
118            Rect => rect: GridRect,
119            Dir => dir: PinDir,
120            PinAccent => pin_accent: Role,
121            PortAccent => port_accent: Role,
122            PortPinAccent => port_pin_accent: Role,
123            FlipLR => flip_lr: bool,
124        }
125        namespaces {}
126        constants {}
127    }
128}
129
130entity! {
131    pub struct Route(init RouteInit, update RouteUpdate, id RouteId) {
132        registers {
133            Owner => owner: BlockId,
134            Name => name: String,
135            Role => role: Role,
136            Waypoints => waypoints: Vec<Waypoint>,
137        }
138        namespaces {}
139        constants {
140            /// Endpoints are creation-time constants; re-pointing one is
141            /// delete-and-recreate.
142            from: PinId,
143            to: PinId,
144        }
145    }
146}
147
148entity! {
149    pub struct RouteLabel(init RouteLabelInit, update RouteLabelUpdate, id RouteLabelId) {
150        registers {
151            Owner => owner: RouteId,
152            /// Offset along the route.
153            Pos => pos: FracVal,
154        }
155        namespaces {}
156        constants {}
157    }
158}
159
160entity! {
161    pub struct Text(init TextInit, update TextUpdate, id TextId) {
162        registers {
163            Owner => owner: BlockId,
164            Text => text: String,
165            Pos => pos: GridPoint,
166            Role => role: Role,
167        }
168        namespaces {}
169        constants {}
170    }
171}
172
173entity! {
174    pub struct Comment(init CommentInit, update CommentUpdate, id CommentId) {
175        registers {
176            Owner => owner: BlockId,
177            Rect => rect: GridRect,
178            Role => role: Role,
179        }
180        namespaces {
181            Title => title: Label,
182        }
183        constants {}
184    }
185}
186
187entity! {
188    /// A free-floating placed image (block icons are the atomic [`Icon`] value).
189    pub struct Image(init ImageInit, update ImageUpdate, id ImageId) {
190        registers {
191            Owner => owner: BlockId,
192            Asset => asset: AssetHash,
193            Rect => rect: ScreenRect,
194        }
195        namespaces {}
196        constants {}
197    }
198}
199
200/// `Arc`: crosses the sync thread boundary.
201pub enum Asset {
202    Svg(Arc<[u8]>),
203    Png(Arc<[u8]>),
204}