Skip to main content

blockworx/canvas/
extent.rs

1//! How big the picture is — measured from what a render actually painted.
2//!
3//! Two answers are derived from a render rather than from the document: the
4//! SVG export's `viewBox` and the editor's fit-to-content framing. Both read
5//! the extent through [`Bounds`], so neither can disagree with the other about
6//! what a primitive occupies, and a shape kind added later joins both the
7//! moment it is drawn.
8//!
9//! [`Extent`] is the standalone half: a [`Renderer`] that draws nothing and
10//! remembers only where. It measures text and images through the backend that
11//! *would* have drawn them, so a fit frames exactly what the canvas paints.
12
13use std::cell::Cell;
14
15use blockworx_doc::block_model::Asset;
16use blockworx_geom::{Align2, Pos2, Rect, Vec2, WorldPx};
17use blockworx_paint::Font;
18
19use crate::canvas::{
20    Renderer,
21    palette::{PaletteStroke, Swatch},
22};
23/// The running world-space union of everything a render has drawn.
24#[derive(Default)]
25pub struct Bounds(Cell<Option<Rect>>);
26
27impl Bounds {
28    /// The union so far, or `None` if nothing has been drawn.
29    pub fn get(&self) -> Option<Rect> {
30        self.0.get()
31    }
32
33    pub fn point(&self, p: Pos2) {
34        self.rect(Rect::from_min_max(p, p));
35    }
36
37    pub fn rect(&self, r: Rect) {
38        self.0.set(Some(match self.0.get() {
39            Some(b) => b.union(r),
40            None => r,
41        }));
42    }
43
44    /// A stroked rect: the outline straddles the edge, so half the width lies
45    /// outside it.
46    pub fn stroked(&self, r: Rect, stroke: &PaletteStroke) {
47        self.rect(r.expand(stroke.width.get() * 0.5));
48    }
49
50    /// A polyline or polygon, by its vertices.
51    pub fn path(&self, points: &[Pos2]) {
52        for p in points {
53            self.point(*p);
54        }
55    }
56
57    pub fn disc(&self, center: Pos2, radius: WorldPx, stroke: &PaletteStroke) {
58        let pad = radius.get() + stroke.width.get() * 0.5;
59        self.rect(Rect::from_center_size(center, Vec2::splat(2.0 * pad)));
60    }
61}
62
63/// A [`Renderer`] that draws nothing and only measures: hand it the same draw
64/// passes the canvas runs and [`Extent::finish`] is the world-space rect they
65/// covered. Text and image sizes come from `inner`, the backend that would have
66/// drawn them.
67pub struct Extent<'r, R: Renderer> {
68    inner: &'r R,
69    bounds: Bounds,
70}
71
72impl<'r, R: Renderer> Extent<'r, R> {
73    pub fn measuring_through(inner: &'r R) -> Self {
74        Self {
75            inner,
76            bounds: Bounds::default(),
77        }
78    }
79
80    /// The union of everything the passes drew, or `None` if they drew nothing.
81    pub fn finish(self) -> Option<Rect> {
82        self.bounds.get()
83    }
84}
85
86impl<R: Renderer> Renderer for Extent<'_, R> {
87    fn rect(
88        &self,
89        rect: Rect,
90        _rounding: WorldPx,
91        _fill: impl Into<Swatch>,
92        stroke: impl Into<PaletteStroke>,
93    ) {
94        self.bounds.stroked(rect, &stroke.into());
95    }
96
97    fn line_segment(&self, points: [Pos2; 2], _stroke: impl Into<PaletteStroke>) {
98        self.bounds.path(&points);
99    }
100
101    fn line(&self, points: Vec<Pos2>, _stroke: impl Into<PaletteStroke>) {
102        self.bounds.path(&points);
103    }
104
105    fn circle(
106        &self,
107        center: Pos2,
108        radius: WorldPx,
109        _fill: impl Into<Swatch>,
110        stroke: impl Into<PaletteStroke>,
111    ) {
112        self.bounds.disc(center, radius, &stroke.into());
113    }
114
115    fn add_convex_polygon(
116        &self,
117        points: Vec<Pos2>,
118        _fill: impl Into<Swatch>,
119        _stroke: impl Into<PaletteStroke>,
120    ) {
121        self.bounds.path(&points);
122    }
123
124    fn text(
125        &self,
126        pos: Pos2,
127        anchor: Align2,
128        text: impl ToString,
129        font: &Font,
130        _color: impl Into<Swatch>,
131    ) -> Rect {
132        self.text_wrapped(
133            pos,
134            anchor,
135            text,
136            font,
137            Swatch::TRANSPARENT,
138            WorldPx::UNBOUNDED,
139        )
140    }
141
142    fn text_wrapped(
143        &self,
144        pos: Pos2,
145        anchor: Align2,
146        text: impl ToString,
147        font: &Font,
148        _color: impl Into<Swatch>,
149        max_width: WorldPx,
150    ) -> Rect {
151        let size = self.inner.text_size_wrapped(text, font, max_width);
152        let rect = anchor.anchor_size(pos, size);
153        if size != Vec2::ZERO {
154            self.bounds.rect(rect);
155        }
156        rect
157    }
158
159    fn rotated_text(
160        &self,
161        pos: Pos2,
162        anchor: Align2,
163        text: impl ToString,
164        font: &Font,
165        _color: impl Into<Swatch>,
166        _angle: f32,
167    ) {
168        let size = self.inner.text_size(text, font);
169        if size != Vec2::ZERO {
170            // A rotated run's exact extent is awkward; the anchor point and the
171            // unrotated rect together bound it well enough — the same claim the
172            // SVG backend makes.
173            self.bounds.point(pos);
174            self.bounds.rect(anchor.anchor_size(pos, size));
175        }
176    }
177
178    fn text_size(&self, text: impl ToString, font: &Font) -> Vec2 {
179        self.inner.text_size(text, font)
180    }
181
182    fn text_size_wrapped(&self, text: impl ToString, font: &Font, max_width: WorldPx) -> Vec2 {
183        self.inner.text_size_wrapped(text, font, max_width)
184    }
185
186    fn draw_image(&self, rect: Rect, _image: &Asset) {
187        self.bounds.rect(rect);
188    }
189
190    fn image_intrinsic_size(&self, image: &Asset) -> Option<Vec2> {
191        self.inner.image_intrinsic_size(image)
192    }
193}