Skip to main content

blockworx_editor/widget/
scene.rs

1use std::collections::HashSet;
2
3use blockworx_doc::id::{RouteId, RouteLabelId};
4
5use crate::{
6    render::{LabelPass, RouteRenderMode, render_route},
7    shape::ShapeId,
8    state::RenderMode,
9    theme::Style,
10    widget::{drawing::Drawing, spatial::HitId},
11};
12use blockworx_paint::Renderer;
13
14type LayerFn<'p, R> = Box<dyn FnOnce(&mut Style<'_, R>) + 'p>;
15
16/// Centralized scene renderer and the single source of truth for paint order.
17///
18/// `draw` lays the scene down in one fixed order — background images, areas,
19/// block bodies, block icons, block pins, ports, routes, text boxes, then a
20/// foreground overlay. A block is drawn in two passes (body then pins) with the
21/// icon layer between, so a block's pins and pin text read on top of its icon;
22/// routes pass over the block cluster but under the text-box layer. The same
23/// passes drive the static view, the SVG export, and every tool's interactive
24/// preview.
25///
26/// Callers vary only what differs from a plain render:
27/// - [`shape_mode`](Self::shape_mode): a per-shape [`RenderMode`] (also applied
28///   to areas), e.g. to preview a dragged or selected shape.
29/// - [`route_mode`](Self::route_mode): a per-route [`RouteRenderMode`].
30/// - [`blocks`](Self::blocks) / [`pins`](Self::pins) / [`routes`](Self::routes):
31///   replace a whole pass when a closure can't express the need (hand-drawn
32///   pins, hidden routes).
33/// - [`overlay`](Self::overlay): draw on top once the scene is complete.
34pub struct DrawingPasses<'p, 'd, R: Renderer> {
35    data: &'p Drawing<'d>,
36    shape_mode: Box<dyn Fn(ShapeId) -> RenderMode + 'p>,
37    route_mode: Box<dyn Fn(RouteId) -> RouteRenderMode + 'p>,
38    /// The route whose labels a tool draws itself, and which of them.
39    suppressed: Option<(RouteId, LabelPass)>,
40    blocks: Option<LayerFn<'p, R>>,
41    pins: Option<LayerFn<'p, R>>,
42    routes: Option<LayerFn<'p, R>>,
43    overlay: Option<LayerFn<'p, R>>,
44}
45
46impl<'p, 'd, R: Renderer> DrawingPasses<'p, 'd, R> {
47    pub fn new(data: &'p Drawing<'d>) -> Self {
48        Self {
49            data,
50            shape_mode: Box::new(|_| RenderMode::Normal),
51            route_mode: Box::new(|_| RouteRenderMode::Normal),
52            suppressed: None,
53            blocks: None,
54            pins: None,
55            routes: None,
56            overlay: None,
57        }
58    }
59
60    /// Render mode for each shape (and area), keyed by id. Defaults to
61    /// `Normal` for every shape.
62    #[must_use]
63    pub fn shape_mode(mut self, f: impl Fn(ShapeId) -> RenderMode + 'p) -> Self {
64        self.shape_mode = Box::new(f);
65        self
66    }
67
68    /// Render mode for each route, keyed by id. Defaults to `Normal`.
69    #[must_use]
70    pub fn route_mode(mut self, f: impl Fn(RouteId) -> RouteRenderMode + 'p) -> Self {
71        self.route_mode = Box::new(f);
72        self
73    }
74
75    /// Skip drawing every label on `route`, for a tool that draws them all
76    /// itself.
77    #[must_use]
78    pub fn suppress_route_labels(mut self, route: RouteId) -> Self {
79        self.suppressed = Some((route, LabelPass::Skip));
80        self
81    }
82
83    /// Skip drawing one label on `route` — the one a tool drags or edits in
84    /// place — and draw its siblings as they stand.
85    #[must_use]
86    pub fn suppress_route_label(mut self, route: RouteId, label: RouteLabelId) -> Self {
87        self.suppressed = Some((route, LabelPass::Omit(label)));
88        self
89    }
90
91    /// Replace the block-body pass entirely — e.g. to draw some blocks' bodies
92    /// specially while relocating a pin group (pair with [`Self::pins`]).
93    #[must_use]
94    pub fn blocks(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
95        self.blocks = Some(Box::new(f));
96        self
97    }
98
99    /// Replace the block-pin pass entirely — the layer drawn over block icons —
100    /// e.g. to fade the pins a tool is relocating.
101    #[must_use]
102    pub fn pins(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
103        self.pins = Some(Box::new(f));
104        self
105    }
106
107    /// Replace the routes pass entirely — e.g. to hide routes touching a pin
108    /// that has no valid drop slot.
109    #[must_use]
110    pub fn routes(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
111        self.routes = Some(Box::new(f));
112        self
113    }
114
115    /// Draw on top of the finished scene (marquees, drag ghosts, selection
116    /// frames, waypoint handles, tag overlays).
117    #[must_use]
118    pub fn overlay(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
119        self.overlay = Some(Box::new(f));
120        self
121    }
122
123    pub fn draw(self, painter: &mut Style<'_, R>) {
124        let _s = tracing::info_span!("scene").entered();
125        let data = self.data;
126        let shape_mode = self.shape_mode;
127        // Viewport culling: on a bounded backend (the on-screen painter), restrict
128        // each layer to the hittables the spatial index reports on screen. `None`
129        // — an offline backend (SVG export) or a Drawing built without an index —
130        // means draw everything, so exports and tests are never culled.
131        let visible: Option<HashSet<HitId>> = painter
132            .visible_world_bounds()
133            .and_then(|viewport| data.visible_ids(viewport));
134        let shape_visible = |id: ShapeId| {
135            visible
136                .as_ref()
137                .is_none_or(|v| v.contains(&HitId::Shape(id)))
138        };
139        let route_visible = |id: RouteId| {
140            visible
141                .as_ref()
142                .is_none_or(|v| v.contains(&HitId::Route(id)))
143        };
144        // In draw order, once: the scope's order costs a walk of every wire
145        // to establish.
146        let visible_routes: Vec<RouteId> = data
147            .scope_route_ids()
148            .into_iter()
149            .filter(|&id| route_visible(id))
150            .collect();
151        {
152            let _s = tracing::info_span!("pass_images").entered();
153            for (id, shape) in data.images_layer() {
154                if shape_visible(id) {
155                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
156                }
157            }
158        }
159        {
160            let _s = tracing::info_span!("pass_areas").entered();
161            for (id, area) in data.areas() {
162                if shape_visible(id) {
163                    area.render_ng(data.shape_accents(), shape_mode(id), painter);
164                }
165            }
166        }
167        {
168            let _s = tracing::info_span!("pass_block_bodies").entered();
169            match self.blocks {
170                Some(f) => f(painter),
171                None => {
172                    for (id, shape) in data.blocks_layer() {
173                        if shape_visible(id) {
174                            shape.render_ng(data.shape_accents(), shape_mode(id), painter);
175                        }
176                    }
177                }
178            }
179        }
180        {
181            // Icons sit between the block body and its pins so the pins (and pin
182            // text) read on top of the icon.
183            let _s = tracing::info_span!("pass_icons").entered();
184            for (id, shape) in data.icons() {
185                if shape_visible(id) {
186                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
187                }
188            }
189        }
190        {
191            let _s = tracing::info_span!("pass_block_pins").entered();
192            match self.pins {
193                Some(f) => f(painter),
194                None => {
195                    for (id, shape) in data.blocks_layer() {
196                        if shape_visible(id) {
197                            shape.render_pins_ng(data.shape_accents(), shape_mode(id), painter);
198                        }
199                    }
200                }
201            }
202        }
203        {
204            let _s = tracing::info_span!("pass_ports").entered();
205            for (id, shape) in data.ports_layer() {
206                if shape_visible(id) {
207                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
208                }
209            }
210        }
211        {
212            let _s = tracing::info_span!("pass_routes").entered();
213            if let Some(f) = self.routes {
214                f(painter);
215            } else {
216                let hops = data.hops(&visible_routes);
217                for &id in &visible_routes {
218                    let Some(route) = data.auto_route(id) else {
219                        continue;
220                    };
221                    let Some(course) = data.course(id, hops.of(id)) else {
222                        continue;
223                    };
224                    let labels = match self.suppressed {
225                        Some((route, pass)) if route == id => pass,
226                        _ => LabelPass::Draw,
227                    };
228                    render_route(painter, &route, course, (self.route_mode)(id), labels);
229                }
230            }
231        }
232        {
233            let _s = tracing::info_span!("pass_texts").entered();
234            for (id, shape) in data.texts_layer() {
235                if shape_visible(id) {
236                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
237                }
238            }
239        }
240        {
241            // Over the shapes, not under them: a block's fill is opaque, so a
242            // mark drawn beneath one is no mark at all.
243            //
244            // An overlap the document already holds, which nothing refuses —
245            // a paste has no overlap check — and the router has to work
246            // around. Only what is on screen: that is what can be marked.
247            //
248            // So is a wire that settled on its fallback L, where it runs
249            // through one of them.
250            let _s = tracing::info_span!("pass_standing_conflicts").entered();
251            let shapes: Vec<ShapeId> = data
252                .child_blocks()
253                .into_iter()
254                .map(|(id, _)| ShapeId::Rect(id))
255                .chain(
256                    data.block_pins(data.current_scope())
257                        .into_iter()
258                        .map(|(id, _)| ShapeId::Port(id)),
259                )
260                .filter(|&id| shape_visible(id))
261                .collect();
262            let conflicts = data.standing_conflicts(&shapes);
263            for mark in conflicts.overlaps.into_iter().chain(conflicts.crossings) {
264                crate::render::draw_standing_conflict(mark, painter);
265            }
266        }
267        if let Some(f) = self.overlay {
268            f(painter);
269        }
270    }
271}