arcs/window/
window.rs

1use crate::{
2    algorithms::Bounded,
3    components::{
4        BoundingBox, DrawingObject, Geometry, Layer, LineStyle, PointStyle,
5        Viewport, WindowStyle,
6    },
7    CanvasSpace, DrawingSpace, Line, Point,
8};
9use euclid::{Point2D, Scale, Size2D};
10use kurbo::Circle;
11use piet::RenderContext;
12use shred_derive::SystemData;
13use specs::{join::MaybeJoin, prelude::*};
14use std::{cmp::Reverse, collections::BTreeMap};
15
16/// A wrapper around the "window" object.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Window(pub Entity);
19
20impl Window {
21    /// Creates a new [`Window`] entity populated with all default components.
22    pub fn create(world: &mut World) -> Self {
23        let ent = world
24            .create_entity()
25            .with(Viewport {
26                centre: Point::zero(),
27                pixels_per_drawing_unit: Scale::new(1.0),
28            })
29            .with(LineStyle::default())
30            .with(PointStyle::default())
31            .with(WindowStyle::default())
32            .build();
33
34        Window(ent)
35    }
36
37    /// Get a [`System`] which will render using a particular [`RenderContext`].
38    ///
39    /// # Note
40    ///
41    /// This snapshots the window's state and styling (e.g. [`Viewport`] and
42    /// [`WindowStyle`]) so you shouldn't keep this system around for any length
43    /// of time.
44    pub fn render_system<'a, R>(
45        &'a self,
46        backend: R,
47        window_size: Size2D<f64, CanvasSpace>,
48    ) -> impl System<'a> + 'a
49    where
50        R: RenderContext + 'a,
51    {
52        RenderSystem {
53            backend,
54            window_size,
55            window: self,
56        }
57    }
58}
59
60macro_rules! components {
61    ($( $get:ident, $get_mut:ident, $component_name:expr => $component_type:ty ),* $(,)?) => {
62        $(
63            #[doc = "Get a reference to the [`Window`]'s [`"]
64            #[doc = $component_name]
65            #[doc = "`] component."]
66            pub fn $get<'a>(&self, storage: &'a ReadStorage<'a, $component_type>) -> &'a $component_type
67            {
68                storage
69                    .get(self.0)
70                    .expect(concat!("The window should always have a ", stringify!($component_type), " component"))
71            }
72
73            #[doc = "Get a mutable reference to the [`Window`]'s [`"]
74            #[doc = $component_name]
75            #[doc = "`] component."]
76            pub fn $get_mut<'a, 'world: 'a>(&self, storage: &'a mut WriteStorage<'world, $component_type>) -> &'a mut $component_type
77            {
78                storage
79                    .get_mut(self.0)
80                    .expect(concat!("The window should always have a ", stringify!($component_type), " component"))
81            }
82        )*
83    };
84}
85
86/// Accessors for the various components attached to this [`Window`].
87impl Window {
88    components! {
89        viewport, viewport_mut, stringify!(Viewport) => Viewport,
90        default_point_style, default_point_style_mut, stringify!(PointStyle) => PointStyle,
91        default_line_style, default_line_style_mut, stringify!(LineStyle) => LineStyle,
92        style, style_mut, stringify!(WindowStyle) => WindowStyle,
93    }
94}
95
96/// The [`System`] which actually renders things.
97///
98/// This is a temporary object "closing over" the [`Window`] and some
99/// [`RenderContext`].
100#[derive(Debug)]
101struct RenderSystem<'window, B> {
102    backend: B,
103    window_size: Size2D<f64, CanvasSpace>,
104    window: &'window Window,
105}
106
107impl<'window, B> RenderSystem<'window, B> {
108    /// Calculate the area of the drawing displayed by the viewport.
109    fn viewport_dimensions(&self, viewport: &Viewport) -> BoundingBox {
110        let window_size = viewport
111            .pixels_per_drawing_unit
112            .inv()
113            .transform_size(self.window_size);
114
115        BoundingBox::from_centre_and_size(viewport.centre, window_size)
116    }
117}
118
119impl<'window, B: RenderContext> RenderSystem<'window, B> {
120    fn render(
121        &mut self,
122        ent: Entity,
123        drawing_object: &DrawingObject,
124        styles: &Styling,
125        viewport: &Viewport,
126    ) {
127        match drawing_object.geometry {
128            Geometry::Point(point) => {
129                self.render_point(
130                    ent,
131                    point,
132                    drawing_object.layer,
133                    styles,
134                    viewport,
135                );
136            },
137            Geometry::Line(ref line) => {
138                self.render_line(
139                    ent,
140                    line,
141                    drawing_object.layer,
142                    styles,
143                    viewport,
144                );
145            },
146            _ => unimplemented!(),
147        }
148    }
149
150    /// Draw a [`Point`] as a circle on the canvas.
151    fn render_point(
152        &mut self,
153        entity: Entity,
154        point: Point,
155        layer: Entity,
156        styles: &Styling,
157        viewport: &Viewport,
158    ) {
159        let style = resolve_point_style(styles, self.window, entity, layer);
160
161        let centre = self.to_canvas_coordinates(point, viewport);
162        let shape = Circle {
163            center: kurbo::Point::new(centre.x, centre.y),
164            radius: style.radius.in_pixels(viewport.pixels_per_drawing_unit),
165        };
166        log::trace!("Drawing {:?} as {:?} using {:?}", point, shape, style);
167
168        self.backend.fill(shape, &style.colour);
169    }
170
171    fn render_line(
172        &mut self,
173        entity: Entity,
174        line: &Line,
175        layer: Entity,
176        styles: &Styling,
177        viewport: &Viewport,
178    ) {
179        let style = resolve_line_style(styles, self.window, entity, layer);
180
181        let start = self.to_canvas_coordinates(line.start, viewport);
182        let end = self.to_canvas_coordinates(line.end, viewport);
183        let shape = kurbo::Line::new(start.to_tuple(), end.to_tuple());
184        let stroke_width =
185            style.width.in_pixels(viewport.pixels_per_drawing_unit);
186        log::trace!("Drawing {:?} as {:?} using {:?}", line, shape, style);
187
188        self.backend.stroke(shape, &style.stroke, stroke_width);
189    }
190
191    /// Translates a [`crate::Point`] from drawing space to a location in
192    /// [`CanvasSpace`].
193    fn to_canvas_coordinates(
194        &self,
195        point: Point2D<f64, DrawingSpace>,
196        viewport: &Viewport,
197    ) -> Point2D<f64, CanvasSpace> {
198        super::to_canvas_coordinates(point, viewport, self.window_size)
199    }
200}
201
202impl<'window, 'world, B: RenderContext> System<'world>
203    for RenderSystem<'window, B>
204{
205    type SystemData = (
206        DrawOrder<'world>,
207        Styling<'world>,
208        ReadStorage<'world, Viewport>,
209    );
210
211    fn run(&mut self, data: Self::SystemData) {
212        let (draw_order, styling, viewports) = data;
213
214        let window_style = self.window.style(&styling.window_styles);
215        let viewport = self.window.viewport(&viewports);
216
217        // make sure we're working with a blank screen
218        self.backend.clear(window_style.background_colour.clone());
219
220        let viewport_dimensions = self.viewport_dimensions(&viewport);
221
222        for (ent, obj) in draw_order.calculate(viewport_dimensions) {
223            self.render(ent, obj, &styling, viewport);
224        }
225    }
226}
227
228/// Styling information.
229#[derive(SystemData)]
230struct Styling<'world> {
231    point_styles: ReadStorage<'world, PointStyle>,
232    line_styles: ReadStorage<'world, LineStyle>,
233    window_styles: ReadStorage<'world, WindowStyle>,
234}
235
236fn resolve_point_style<'a>(
237    styling: &'a Styling,
238    window: &'a Window,
239    point: Entity,
240    layer: Entity,
241) -> &'a PointStyle {
242    styling
243            .point_styles
244            // the style for this point may have been overridden explicitly
245            .get(point)
246            // otherwise fall back to the layer's PointStyle
247            .or_else(|| styling.point_styles.get(layer))
248            // fall back to the window's default if the layer didn't specify one
249            .unwrap_or_else(|| window.default_point_style(&styling.point_styles))
250}
251
252fn resolve_line_style<'a>(
253    styling: &'a Styling,
254    window: &'a Window,
255    line: Entity,
256    layer: Entity,
257) -> &'a LineStyle {
258    styling
259        .line_styles
260        .get(line)
261        .or_else(|| styling.line_styles.get(layer))
262        .unwrap_or_else(|| window.default_line_style(&styling.line_styles))
263}
264
265/// The state needed when calculating which order to draw things in so z-levels
266/// are implemented correctly.
267#[derive(SystemData)]
268struct DrawOrder<'world> {
269    entities: Entities<'world>,
270    drawing_objects: ReadStorage<'world, DrawingObject>,
271    layers: ReadStorage<'world, Layer>,
272    bounding_boxes: ReadStorage<'world, BoundingBox>,
273}
274
275impl<'world> DrawOrder<'world> {
276    fn calculate(
277        &self,
278        viewport_dimensions: BoundingBox,
279    ) -> impl Iterator<Item = (Entity, &'_ DrawingObject)> + '_ {
280        type EntitiesByZLevel<'a> =
281            BTreeMap<Reverse<usize>, Vec<(Entity, &'a DrawingObject)>>;
282
283        // Iterate through all drawing objects, grouping them by the parent
284        // layer's z-level in reverse order (we want to yield higher z-levels
285        // first)
286        let mut drawing_objects = EntitiesByZLevel::new();
287
288        // PERF: This function has a massive impact on render times
289        // Some ideas:
290        //   - Use a pre-calculated quad-tree so we just need to check items
291        //     within the viewport bounds
292        //   - use a entities-to-layers cache so we can skip checking whether to
293        //     draw an object on a hidden layer
294
295        for (ent, obj, bounds) in (
296            &self.entities,
297            &self.drawing_objects,
298            MaybeJoin(&self.bounding_boxes),
299        )
300            .join()
301        {
302            let Layer { z_level, visible } = self
303                .layers
304                .get(obj.layer)
305                .expect("The object's layer was deleted");
306
307            // try to use the cached bounds, otherwise re-calculate them
308            let bounds = bounds
309                .copied()
310                .unwrap_or_else(|| obj.geometry.bounding_box());
311
312            if *visible && viewport_dimensions.intersects_with(bounds) {
313                drawing_objects
314                    .entry(Reverse(*z_level))
315                    .or_default()
316                    .push((ent, obj));
317            }
318        }
319
320        drawing_objects.into_iter().flat_map(|(_, items)| items)
321    }
322}