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}