Skip to main content

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}