Skip to main content

blockworx/widget/
scene.rs

1use std::collections::HashSet;
2
3use blockworx_doc::id::RouteId;
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    /// Route whose label is being edited in place; its label is skipped so the
39    /// in-place editor doesn't draw over (or double up with) the rendered text.
40    suppressed_label: Option<RouteId>,
41    blocks: Option<LayerFn<'p, R>>,
42    pins: Option<LayerFn<'p, R>>,
43    routes: Option<LayerFn<'p, R>>,
44    overlay: Option<LayerFn<'p, R>>,
45}
46
47impl<'p, 'd, R: Renderer> DrawingPasses<'p, 'd, R> {
48    pub fn new(data: &'p Drawing<'d>) -> Self {
49        Self {
50            data,
51            shape_mode: Box::new(|_| RenderMode::Normal),
52            route_mode: Box::new(|_| RouteRenderMode::Normal),
53            suppressed_label: None,
54            blocks: None,
55            pins: None,
56            routes: None,
57            overlay: None,
58        }
59    }
60
61    /// Render mode for each shape (and area), keyed by id. Defaults to
62    /// `Normal` for every shape.
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    pub fn route_mode(mut self, f: impl Fn(RouteId) -> RouteRenderMode + 'p) -> Self {
70        self.route_mode = Box::new(f);
71        self
72    }
73
74    /// Skip drawing this route's label, leaving room for its in-place editor.
75    pub fn suppress_route_label(mut self, route: RouteId) -> Self {
76        self.suppressed_label = Some(route);
77        self
78    }
79
80    /// Replace the block-body pass entirely — e.g. to draw some blocks' bodies
81    /// specially while relocating a pin group (pair with [`Self::pins`]).
82    pub fn blocks(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
83        self.blocks = Some(Box::new(f));
84        self
85    }
86
87    /// Replace the block-pin pass entirely — the layer drawn over block icons —
88    /// e.g. to fade the pins a tool is relocating.
89    pub fn pins(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
90        self.pins = Some(Box::new(f));
91        self
92    }
93
94    /// Replace the routes pass entirely — e.g. to hide routes touching a pin
95    /// that has no valid drop slot.
96    pub fn routes(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
97        self.routes = Some(Box::new(f));
98        self
99    }
100
101    /// Draw on top of the finished scene (marquees, drag ghosts, selection
102    /// frames, waypoint handles, tag overlays).
103    pub fn overlay(mut self, f: impl FnOnce(&mut Style<'_, R>) + 'p) -> Self {
104        self.overlay = Some(Box::new(f));
105        self
106    }
107
108    pub fn draw(self, painter: &mut Style<'_, R>) {
109        let data = self.data;
110        let shape_mode = self.shape_mode;
111        // Viewport culling: on a bounded backend (the on-screen painter), restrict
112        // each layer to the hittables the spatial index reports on screen. `None`
113        // — an offline backend (SVG export) or a Drawing built without an index —
114        // means draw everything, so exports and tests are never culled.
115        let visible: Option<HashSet<HitId>> = painter
116            .visible_world_bounds()
117            .and_then(|viewport| data.visible_ids(viewport));
118        let shape_visible = |id: ShapeId| {
119            visible
120                .as_ref()
121                .is_none_or(|v| v.contains(&HitId::Shape(id)))
122        };
123        let route_visible = |id: RouteId| {
124            visible
125                .as_ref()
126                .is_none_or(|v| v.contains(&HitId::Route(id)))
127        };
128        {
129            let _s = tracing::info_span!("pass_images").entered();
130            for (id, shape) in data.images_layer() {
131                if shape_visible(id) {
132                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
133                }
134            }
135        }
136        {
137            let _s = tracing::info_span!("pass_areas").entered();
138            for (id, area) in data.areas() {
139                if shape_visible(id) {
140                    area.render_ng(data.shape_accents(), shape_mode(id), painter);
141                }
142            }
143        }
144        {
145            let _s = tracing::info_span!("pass_block_bodies").entered();
146            match self.blocks {
147                Some(f) => f(painter),
148                None => {
149                    for (id, shape) in data.blocks_layer() {
150                        if shape_visible(id) {
151                            shape.render_ng(data.shape_accents(), shape_mode(id), painter);
152                        }
153                    }
154                }
155            }
156        }
157        {
158            // Icons sit between the block body and its pins so the pins (and pin
159            // text) read on top of the icon.
160            let _s = tracing::info_span!("pass_icons").entered();
161            for (id, shape) in data.icons() {
162                if shape_visible(id) {
163                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
164                }
165            }
166        }
167        {
168            let _s = tracing::info_span!("pass_block_pins").entered();
169            match self.pins {
170                Some(f) => f(painter),
171                None => {
172                    for (id, shape) in data.blocks_layer() {
173                        if shape_visible(id) {
174                            shape.render_pins_ng(data.shape_accents(), shape_mode(id), painter);
175                        }
176                    }
177                }
178            }
179        }
180        {
181            let _s = tracing::info_span!("pass_ports").entered();
182            for (id, shape) in data.ports_layer() {
183                if shape_visible(id) {
184                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
185                }
186            }
187        }
188        {
189            let _s = tracing::info_span!("pass_routes").entered();
190            match self.routes {
191                Some(f) => f(painter),
192                None => {
193                    for (id, route) in data.auto_routes() {
194                        if route_visible(id) {
195                            // A route with no solved geometry draws nothing.
196                            let Some(geometry) = data.route_geometry(id) else {
197                                continue;
198                            };
199                            let labels = if Some(id) == self.suppressed_label {
200                                LabelPass::Skip
201                            } else {
202                                LabelPass::Draw
203                            };
204                            render_route(painter, &route, geometry, (self.route_mode)(id), labels);
205                        }
206                    }
207                }
208            }
209        }
210        {
211            let _s = tracing::info_span!("pass_texts").entered();
212            for (id, shape) in data.texts_layer() {
213                if shape_visible(id) {
214                    shape.render_ng(data.shape_accents(), shape_mode(id), painter);
215                }
216            }
217        }
218        if let Some(f) = self.overlay {
219            f(painter);
220        }
221    }
222}