blockworx/derived/mod.rs
1//! Derived state: everything computed *from* the document that the
2//! editor needs *beside* it — solver geometry, measurement caches,
3//! propagated accents. Never authored, never undone, never persisted:
4//! the document is authored state only, and this is where the rest
5//! lives (editor-swap playbook, step 7).
6//!
7//! Owned by the app for the life of a document — rebuilt wholesale on
8//! load/undo/restore, incrementally by the passes that used to write
9//! the document — and threaded through
10//! [`Drawing`](crate::widget::drawing::Drawing) like the spatial index,
11//! mutably, because the solver passes that fill it run inside `Drawing`
12//! methods.
13
14pub mod route;
15
16pub use route::{Crossing, LocAndDirection, RouteGeometry};
17
18use ahash::HashMap;
19
20use crate::document::model::Generation;
21use crate::document::{Document, GridSize, LineAnchor};
22use crate::store::{PinId, RectId, RouteId, TextId};
23
24#[derive(Default)]
25pub struct Derived {
26 pub pin_accents: PinAccents,
27 pub text_extents: TextExtents,
28 /// Solved geometry per owning block, then per route. A missing entry is a
29 /// route that has not been materialized — nothing to draw or hit-test yet.
30 pub routes: ScopedRoutes,
31}
32
33/// Route geometry keyed by the owning block first. The outer key is
34/// load-bearing, not organizational: `RouteId`s are minted per block map, so
35/// two scopes' routes share ids, and a flat map would let each materialized
36/// scope overwrite the last one's geometry — the same hazard [`PinAccents`]
37/// documents for `PinId`.
38#[derive(Default)]
39pub struct ScopedRoutes {
40 map: HashMap<RectId, HashMap<RouteId, RouteGeometry>>,
41}
42
43impl ScopedRoutes {
44 /// The geometry of `block`'s routes, if any were materialized.
45 pub fn scope(&self, block: RectId) -> Option<&HashMap<RouteId, RouteGeometry>> {
46 self.map.get(&block)
47 }
48
49 /// The geometry map of `block`'s routes, created empty on first touch.
50 pub fn scope_mut(&mut self, block: RectId) -> &mut HashMap<RouteId, RouteGeometry> {
51 self.map.entry(block).or_default()
52 }
53
54 /// One route's solved geometry within its owning block.
55 pub fn get(&self, block: RectId, route: RouteId) -> Option<&RouteGeometry> {
56 self.scope(block)?.get(&route)
57 }
58
59 /// Drop a deleted block's geometry wholesale. Also guards id reuse: the
60 /// flat map re-mints a deleted `RectId`, and the new block must not
61 /// inherit the dead one's scope.
62 pub fn remove_scope(&mut self, block: RectId) {
63 self.map.remove(&block);
64 }
65}
66
67impl Derived {
68 /// Bring the accent propagation up to date with `document`. Gated on
69 /// the generation stamp, so the once-per-borrow call sites stay
70 /// cheap.
71 pub fn refresh_accents(&mut self, document: &Document) {
72 if self.pin_accents.generation == Some(document.generation()) {
73 return;
74 }
75 self.pin_accents.recompute(document);
76 }
77}
78
79/// Route-role propagation onto pin stubs: a wire's accent colors the
80/// pins it lands on. Derived fresh from the routes — the last route at
81/// a level to touch an anchor wins, including a plain (`None`) role —
82/// where the document used to store the propagation and let it go
83/// stale. Keys carry the owning block because a `PinId` is unique per
84/// block, not per document.
85#[derive(Default)]
86pub struct PinAccents {
87 generation: Option<Generation>,
88 /// A child block's pin, as colored by its parent level's routes.
89 pin: HashMap<(RectId, PinId), Option<u8>>,
90 /// A port's own pin, as colored by the port's level's routes.
91 port_pin: HashMap<(RectId, PinId), Option<u8>>,
92}
93
94impl PinAccents {
95 /// The stub accent of `pin` drawn as part of child block `block`.
96 pub fn pin(&self, block: RectId, pin: PinId) -> Option<u8> {
97 self.pin.get(&(block, pin)).copied().flatten()
98 }
99
100 /// The stub accent of `pin` drawn as part of a port on `level`.
101 pub fn port_pin(&self, level: RectId, pin: PinId) -> Option<u8> {
102 self.port_pin.get(&(level, pin)).copied().flatten()
103 }
104
105 fn recompute(&mut self, document: &Document) {
106 self.pin.clear();
107 self.port_pin.clear();
108 for (&level, block) in document.blocks.iter() {
109 for (_, route) in &block.routes {
110 for anchor in [route.start(), route.finish()] {
111 match anchor {
112 LineAnchor::Pin { block, pin } => {
113 self.pin.insert((block, pin), route.role());
114 }
115 LineAnchor::Port(pin) => {
116 self.port_pin.insert((level, pin), route.role());
117 }
118 }
119 }
120 }
121 }
122 self.generation = Some(document.generation());
123 }
124}
125
126/// Galley-measured text-box extents, keyed by the text they were
127/// measured for: an entry is consulted only while the box still holds
128/// that text, so the cache can never serve a stale rect — undo, load,
129/// or restore leave it self-invalidated and readers fall back to the
130/// character-count estimate until the box is next edited.
131#[derive(Default)]
132pub struct TextExtents {
133 map: HashMap<TextId, (String, GridSize)>,
134}
135
136impl TextExtents {
137 pub fn set(&mut self, id: TextId, text: String, size: GridSize) {
138 self.map.insert(id, (text, size));
139 }
140
141 /// The measured extent, if it was measured for exactly `current_text`.
142 pub fn valid_for(&self, id: TextId, current_text: &str) -> Option<GridSize> {
143 self.map
144 .get(&id)
145 .filter(|(text, _)| text == current_text)
146 .map(|&(_, size)| size)
147 }
148}
149
150/// The accent lookup for one shape's pins, resolved by a caller that
151/// knows the shape's id: a block's pins read the child-pin view, a
152/// port's pin reads the port view at its own level.
153#[derive(Clone, Copy)]
154pub struct ShapeAccents<'a> {
155 owner: RectId,
156 accents: &'a PinAccents,
157}
158
159impl<'a> ShapeAccents<'a> {
160 pub fn new(owner: RectId, accents: &'a PinAccents) -> Self {
161 Self { owner, accents }
162 }
163
164 pub fn pin(&self, pin: PinId) -> Option<u8> {
165 self.accents.pin(self.owner, pin)
166 }
167
168 pub fn port_pin(&self, pin: PinId) -> Option<u8> {
169 self.accents.port_pin(self.owner, pin)
170 }
171}