Skip to main content

typst_library/visualize/
shape.rs

1use crate::foundations::{Cast, Content, Smart, elem};
2use crate::layout::{Abs, Corners, Length, Point, Rect, Rel, Sides, Size, Sizing};
3use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
4use kurbo::{PathEl, Shape as _};
5
6/// A rectangle with optional content.
7///
8/// = Example <example>
9/// ```example
10/// // Without content.
11/// #rect(width: 35%, height: 30pt)
12///
13/// // With content.
14/// #rect[
15///   Automatically sized \
16///   to fit the content.
17/// ]
18/// ```
19#[elem(title = "Rectangle")]
20pub struct RectElem {
21    /// The rectangle's width, relative to its parent container.
22    pub width: Smart<Rel<Length>>,
23
24    /// The rectangle's height, relative to its parent container.
25    pub height: Sizing,
26
27    /// How to fill the rectangle.
28    ///
29    /// When setting a fill, the default stroke disappears. To create a
30    /// rectangle with both fill and stroke, you have to configure both.
31    ///
32    /// ```example
33    /// #rect(fill: blue)
34    /// ```
35    pub fill: Option<Paint>,
36
37    /// How to stroke the rectangle. This can be:
38    ///
39    /// - `{none}` to disable stroking
40    ///
41    /// - `{auto}` for a stroke of `{1pt + black}` if and only if no fill is
42    ///   given.
43    ///
44    /// - Any kind of @stroke[stroke]
45    ///
46    /// - A dictionary describing the stroke for each side individually. The
47    ///   dictionary can contain the following keys in order of precedence:
48    ///
49    ///   - `top`: The top stroke.
50    ///   - `right`: The right stroke.
51    ///   - `bottom`: The bottom stroke.
52    ///   - `left`: The left stroke.
53    ///   - `x`: The left and right stroke.
54    ///   - `y`: The top and bottom stroke.
55    ///   - `rest`: The stroke on all sides except those for which the
56    ///     dictionary explicitly sets a size.
57    ///
58    ///   All keys are optional; omitted keys will use their previously set
59    ///   value, or the default stroke if never set.
60    ///
61    /// ```example
62    /// #stack(
63    ///   dir: ltr,
64    ///   spacing: 1fr,
65    ///   rect(stroke: red),
66    ///   rect(stroke: 2pt),
67    ///   rect(stroke: 2pt + red),
68    /// )
69    /// ```
70    #[fold]
71    pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
72
73    /// How much to round the rectangle's corners, relative to the minimum of
74    /// the width and height divided by two. This can be:
75    ///
76    /// - A relative length for a uniform corner radius.
77    ///
78    /// - A dictionary: With a dictionary, the stroke for each side can be set
79    ///   individually. The dictionary can contain the following keys in order
80    ///   of precedence:
81    ///   - `top-left`: The top-left corner radius.
82    ///   - `top-right`: The top-right corner radius.
83    ///   - `bottom-right`: The bottom-right corner radius.
84    ///   - `bottom-left`: The bottom-left corner radius.
85    ///   - `left`: The top-left and bottom-left corner radii.
86    ///   - `top`: The top-left and top-right corner radii.
87    ///   - `right`: The top-right and bottom-right corner radii.
88    ///   - `bottom`: The bottom-left and bottom-right corner radii.
89    ///   - `rest`: The radii for all corners except those for which the
90    ///     dictionary explicitly sets a size.
91    ///
92    /// ```example
93    /// #set rect(stroke: 4pt)
94    /// #rect(
95    ///   radius: (
96    ///     left: 5pt,
97    ///     top-right: 20pt,
98    ///     bottom-right: 10pt,
99    ///   ),
100    ///   stroke: (
101    ///     left: red,
102    ///     top: yellow,
103    ///     right: green,
104    ///     bottom: blue,
105    ///   ),
106    /// )
107    /// ```
108    #[fold]
109    pub radius: Corners<Option<Rel<Length>>>,
110
111    /// How much to pad the rectangle's content. See the
112    /// @box.inset[box's documentation] for more details.
113    #[fold]
114    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
115    pub inset: Sides<Option<Rel<Length>>>,
116
117    /// How much to expand the rectangle's size without affecting the layout.
118    /// See the @box.outset[box's documentation] for more details.
119    #[fold]
120    pub outset: Sides<Option<Rel<Length>>>,
121
122    /// The content to place into the rectangle.
123    ///
124    /// When this is omitted, the rectangle takes on a default size of at most
125    /// `{45pt}` by `{30pt}`.
126    #[positional]
127    pub body: Option<Content>,
128}
129
130/// A square with optional content.
131///
132/// = Example <example>
133/// ```example
134/// // Without content.
135/// #square(size: 40pt)
136///
137/// // With content.
138/// #square[
139///   Automatically \
140///   sized to fit.
141/// ]
142/// ```
143#[elem]
144pub struct SquareElem {
145    /// The square's side length. This is mutually exclusive with `width` and
146    /// `height`.
147    #[external]
148    pub size: Smart<Length>,
149
150    /// The square's width. This is mutually exclusive with `size` and `height`.
151    ///
152    /// In contrast to `size`, this can be relative to the parent container's
153    /// width.
154    #[parse(
155        let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
156        match size {
157            None => args.named("width")?,
158            size => size,
159        }
160    )]
161    pub width: Smart<Rel<Length>>,
162
163    /// The square's height. This is mutually exclusive with `size` and `width`.
164    ///
165    /// In contrast to `size`, this can be relative to the parent container's
166    /// height.
167    #[parse(match size {
168        None => args.named("height")?,
169        size => size.map(Into::into),
170    })]
171    pub height: Sizing,
172
173    /// How to fill the square. See the @rect.fill[rectangle's documentation]
174    /// for more details.
175    pub fill: Option<Paint>,
176
177    /// How to stroke the square. See the
178    /// @rect.stroke[rectangle's documentation] for more details.
179    #[fold]
180    pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
181
182    /// How much to round the square's corners. See the
183    /// @rect.radius[rectangle's documentation] for more details.
184    #[fold]
185    pub radius: Corners<Option<Rel<Length>>>,
186
187    /// How much to pad the square's content. See the
188    /// @box.inset[box's documentation] for more details.
189    #[fold]
190    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
191    pub inset: Sides<Option<Rel<Length>>>,
192
193    /// How much to expand the square's size without affecting the layout. See
194    /// the @box.outset[box's documentation] for more details.
195    #[fold]
196    pub outset: Sides<Option<Rel<Length>>>,
197
198    /// The content to place into the square. The square expands to fit this
199    /// content, keeping the 1-1 aspect ratio.
200    ///
201    /// When this is omitted, the square takes on a default size of at most
202    /// `{30pt}`.
203    #[positional]
204    pub body: Option<Content>,
205}
206
207/// An ellipse with optional content.
208///
209/// = Example <example>
210/// ```example
211/// // Without content.
212/// #ellipse(width: 35%, height: 30pt)
213///
214/// // With content.
215/// #ellipse[
216///   #set align(center)
217///   Automatically sized \
218///   to fit the content.
219/// ]
220/// ```
221#[elem]
222pub struct EllipseElem {
223    /// The ellipse's width, relative to its parent container.
224    pub width: Smart<Rel<Length>>,
225
226    /// The ellipse's height, relative to its parent container.
227    pub height: Sizing,
228
229    /// How to fill the ellipse. See the @rect.fill[rectangle's documentation]
230    /// for more details.
231    pub fill: Option<Paint>,
232
233    /// How to stroke the ellipse. See the
234    /// @rect.stroke[rectangle's documentation] for more details.
235    #[fold]
236    pub stroke: Smart<Option<Stroke>>,
237
238    /// How much to pad the ellipse's content. See the
239    /// @box.inset[box's documentation] for more details.
240    #[fold]
241    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
242    pub inset: Sides<Option<Rel<Length>>>,
243
244    /// How much to expand the ellipse's size without affecting the layout. See
245    /// the @box.outset[box's documentation] for more details.
246    #[fold]
247    pub outset: Sides<Option<Rel<Length>>>,
248
249    /// The content to place into the ellipse.
250    ///
251    /// When this is omitted, the ellipse takes on a default size of at most
252    /// `{45pt}` by `{30pt}`.
253    #[positional]
254    pub body: Option<Content>,
255}
256
257/// A circle with optional content.
258///
259/// = Example <example>
260/// ```example
261/// // Without content.
262/// #circle(radius: 25pt)
263///
264/// // With content.
265/// #circle[
266///   #set align(center + horizon)
267///   Automatically \
268///   sized to fit.
269/// ]
270/// ```
271#[elem]
272pub struct CircleElem {
273    /// The circle's radius. This is mutually exclusive with `width` and
274    /// `height`.
275    #[external]
276    pub radius: Length,
277
278    /// The circle's width. This is mutually exclusive with `radius` and
279    /// `height`.
280    ///
281    /// In contrast to `radius`, this can be relative to the parent container's
282    /// width.
283    #[parse(
284        let size = args
285            .named::<Smart<Length>>("radius")?
286            .map(|s| s.map(|r| 2.0 * Rel::from(r)));
287        match size {
288            None => args.named("width")?,
289            size => size,
290        }
291    )]
292    pub width: Smart<Rel<Length>>,
293
294    /// The circle's height. This is mutually exclusive with `radius` and
295    /// `width`.
296    ///
297    /// In contrast to `radius`, this can be relative to the parent container's
298    /// height.
299    #[parse(match size {
300        None => args.named("height")?,
301        size => size.map(Into::into),
302    })]
303    pub height: Sizing,
304
305    /// How to fill the circle. See the @rect.fill[rectangle's documentation]
306    /// for more details.
307    pub fill: Option<Paint>,
308
309    /// How to stroke the circle. See the
310    /// @rect.stroke[rectangle's documentation] for more details.
311    #[fold]
312    #[default(Smart::Auto)]
313    pub stroke: Smart<Option<Stroke>>,
314
315    /// How much to pad the circle's content. See the
316    /// @box.inset[box's documentation] for more details.
317    #[fold]
318    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
319    pub inset: Sides<Option<Rel<Length>>>,
320
321    /// How much to expand the circle's size without affecting the layout. See
322    /// the @box.outset[box's documentation] for more details.
323    #[fold]
324    pub outset: Sides<Option<Rel<Length>>>,
325
326    /// The content to place into the circle. The circle expands to fit this
327    /// content, keeping the 1-1 aspect ratio.
328    #[positional]
329    pub body: Option<Content>,
330}
331
332/// A geometric shape with optional fill and stroke.
333#[derive(Debug, Clone, Eq, PartialEq, Hash)]
334pub struct Shape {
335    /// The shape's geometry.
336    pub geometry: Geometry,
337    /// The shape's background fill.
338    pub fill: Option<Paint>,
339    /// The shape's fill rule.
340    pub fill_rule: FillRule,
341    /// The shape's border stroke.
342    pub stroke: Option<FixedStroke>,
343}
344
345impl Shape {
346    /// The bounding box of the shape,
347    /// optionally taking the stroke into account
348    pub fn bbox(&self, include_stroke: bool) -> Rect {
349        self.geometry
350            .bbox(if include_stroke { self.stroke.as_ref() } else { None })
351    }
352}
353
354/// A fill rule for curve drawing.
355#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
356pub enum FillRule {
357    /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
358    #[default]
359    NonZero,
360    /// Specifies that "inside" is computed by an odd number of edge crossings.
361    EvenOdd,
362}
363
364/// A shape's geometry.
365#[derive(Debug, Clone, Eq, PartialEq, Hash)]
366pub enum Geometry {
367    /// A line to a point (relative to its position).
368    Line(Point),
369    /// A rectangle with its origin in the topleft corner.
370    Rect(Size),
371    /// A curve consisting of movements, lines, and Bézier segments.
372    Curve(Curve),
373}
374
375impl Geometry {
376    /// Fill the geometry without a stroke.
377    pub fn filled(self, fill: impl Into<Paint>) -> Shape {
378        Shape {
379            geometry: self,
380            fill: Some(fill.into()),
381            fill_rule: FillRule::default(),
382            stroke: None,
383        }
384    }
385
386    /// Stroke the geometry without a fill.
387    pub fn stroked(self, stroke: FixedStroke) -> Shape {
388        Shape {
389            geometry: self,
390            fill: None,
391            fill_rule: FillRule::default(),
392            stroke: Some(stroke),
393        }
394    }
395
396    /// Set the geometry's background fill and stroke.
397    pub fn filled_and_stroked(
398        self,
399        fill: impl Into<Paint>,
400        stroke: FixedStroke,
401    ) -> Shape {
402        Shape {
403            geometry: self,
404            fill: Some(fill.into()),
405            fill_rule: FillRule::default(),
406            stroke: Some(stroke),
407        }
408    }
409
410    /// The bounding box of the geometry,
411    /// optionally taking the stroke width of the shape into account
412    pub fn bbox(&self, stroke: Option<&FixedStroke>) -> Rect {
413        match self {
414            Self::Line(end) => {
415                if let Some(stroke) = stroke {
416                    bbox_of_stroked_line(end, stroke)
417                } else {
418                    Rect::new(end.min(Point::zero()), end.max(Point::zero()))
419                }
420            }
421            Self::Rect(size) => {
422                let min = size.to_point().min(Point::zero());
423                let stroke_width = stroke.map(|s| s.thickness).unwrap_or(Abs::zero());
424                Rect::from_pos_size(
425                    min.map(|i| i - 0.5 * stroke_width),
426                    size.map(|i| i.abs() + stroke_width),
427                )
428            }
429            Self::Curve(curve) => curve.bbox(stroke),
430        }
431    }
432}
433
434/// The bounding box of a line including the stroke
435fn bbox_of_stroked_line(end: &Point, stroke: &FixedStroke) -> Rect {
436    let cap = match stroke.cap {
437        super::LineCap::Butt => kurbo::Cap::Butt,
438        super::LineCap::Round => kurbo::Cap::Round,
439        super::LineCap::Square => kurbo::Cap::Square,
440    };
441    let style = kurbo::Stroke::new(stroke.thickness.to_raw()).with_caps(cap);
442    let opts = kurbo::StrokeOpts::default();
443    let tolerance = 0.01;
444    let bbox = kurbo::stroke(
445        [PathEl::LineTo(kurbo::Point::new(end.x.to_raw(), end.y.to_raw()))],
446        &style,
447        &opts,
448        tolerance,
449    )
450    .bounding_box();
451    Rect::new(
452        Point::new(Abs::raw(bbox.x0), Abs::raw(bbox.y0)),
453        Point::new(Abs::raw(bbox.x1), Abs::raw(bbox.y1)),
454    )
455}