dessin/
shapes.rs

1//! Basic building blocks of dessin
2//!
3//! ## Usage
4//!
5//! ### Ellipse
6//!
7//! ```
8//! # use dessin::prelude::*;
9//! dessin!(
10//! 	Ellipse()
11//! );
12//! ```
13//!
14//! ### Text
15//!
16//! ```
17//! # use dessin::prelude::*;
18//! dessin!(
19//! 	Text()
20//! );
21//! ```
22//!
23//! ### Curve
24//!
25//! ```
26//! # use dessin::prelude::*;
27//! dessin!(
28//! 	Curve()
29//! );
30//! ```
31//!
32//! ### Image
33//!
34//! ```
35//! # use dessin::prelude::*;
36//! dessin!(
37//! 	Image()
38//! );
39//! ```
40//!
41//! ### Group
42//!
43//! ```
44//! # use dessin::prelude::*;
45//! dessin!(
46//! 	[]
47//! );
48//! ```
49//!
50//! ### Dynamic
51//!
52//! ```
53//! # use dessin::prelude::*;
54//!
55//! let ellipse_ref /* : Arc<RwLock<Ellipse>> */ = Default::default();
56//!
57//! dessin!(
58//! 	Dynamic<Ellipse>(
59//! 		_ref = &ellipse_ref,
60//! 		semi_major_axis = 2.
61//! 	)
62//! );
63//!
64//! // Later in code
65//!
66//! ellipse_ref.write().unwrap().semi_major_axis(0.5);
67//! ```
68//!
69//! ## Details
70
71pub(crate) mod curve;
72pub(crate) mod dynamic;
73pub(crate) mod ellipse;
74pub(crate) mod image;
75pub(crate) mod text;
76
77pub use self::image::*;
78pub use curve::*;
79pub use dynamic::*;
80pub use ellipse::*;
81use na::{Point2, Rotation2, Scale2, Vector2};
82use nalgebra::{self as na, Transform2, Translation2};
83use std::{fmt, marker::PhantomData, sync::Arc};
84pub use text::*;
85
86/// Transforming operation on shapes such as:
87/// - a translation with [`translate`][ShapeOp::translate]
88/// - a scale with [`scale`][ShapeOp::scale]
89/// - a rotation with [`rotate`][ShapeOp::rotate]
90/// - any other transform with [`transform`][ShapeOp::transform]
91pub trait ShapeOp {
92	/// Apply an ordinary transform.
93	/// You don't need to implement [`translate`][ShapeOp::translate], [`scale`][ShapeOp::scale] or [`rotate`][ShapeOp::rotate]
94	/// yourself as a blanket implementation is given with this transform.
95	fn transform(&mut self, transform_matrix: Transform2<f32>) -> &mut Self;
96
97	/// Translation
98	#[inline]
99	fn translate<T: Into<Translation2<f32>>>(&mut self, translation: T) -> &mut Self {
100		self.transform(na::convert::<_, Transform2<f32>>(translation.into()));
101		self
102	}
103	/// Scale
104	#[inline]
105	fn scale<S: Into<Scale2<f32>>>(&mut self, scale: S) -> &mut Self {
106		self.transform(na::convert::<_, Transform2<f32>>(scale.into()));
107		self
108	}
109	/// Rotation
110	#[inline]
111	fn rotate<R: Into<Rotation2<f32>>>(&mut self, rotation: R) -> &mut Self {
112		self.transform(na::convert::<_, Transform2<f32>>(rotation.into()));
113		self
114	}
115
116	/// Get own local transform.
117	/// Required for the blanket implementation of [`global_transform`][ShapeOp::global_transform].
118	fn local_transform(&self) -> &Transform2<f32>;
119	/// Absolute transform given the parent transform
120	#[inline]
121	fn global_transform(&self, parent_transform: &Transform2<f32>) -> Transform2<f32> {
122		parent_transform * self.local_transform()
123	}
124}
125
126/// Same as [`ShapeOp`] but for chaining methods.
127/// All shapes that implement [`ShapeOp`] also implement [`ShapeOpWith`] for free.
128pub trait ShapeOpWith: ShapeOp + Sized {
129	/// Transform
130	#[inline]
131	fn with_transform(mut self, transform_matrix: Transform2<f32>) -> Self {
132		self.transform(transform_matrix);
133		self
134	}
135
136	/// Translate
137	#[inline]
138	fn with_translate<T: Into<Translation2<f32>>>(mut self, translation: T) -> Self {
139		self.translate(translation);
140		self
141	}
142	/// Resize
143	#[inline]
144	fn with_resize<S: Into<Scale2<f32>>>(mut self, scale: S) -> Self {
145		self.scale(scale);
146		self
147	}
148	/// Rotate
149	#[inline]
150	fn with_rotate<R: Into<Rotation2<f32>>>(mut self, rotation: R) -> Self {
151		self.rotate(rotation);
152		self
153	}
154}
155impl<T: ShapeOp> ShapeOpWith for T {}
156
157/// Marker discribing the state of a bounding box.
158/// With this marker, the bounding box may be skew or rotated.
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct UnParticular;
161/// Marker discribing the state of a bounding box.
162/// With this marker, the sides of the bounding box are guaranteed to be aligned with the X and Y axis.
163#[derive(Debug, Clone, Copy, PartialEq)]
164pub struct Straight;
165
166/// Bounding box used to describe max bound of an shape.
167/// Usefull to find the max size of shapes as multiple [`BoundingBox`] can be join together.
168#[derive(Debug, Clone, Copy, PartialEq)]
169pub struct BoundingBox<Type> {
170	_ty: PhantomData<Type>,
171	top_left: Point2<f32>,
172	top_right: Point2<f32>,
173	bottom_right: Point2<f32>,
174	bottom_left: Point2<f32>,
175}
176impl<T> BoundingBox<T> {
177	/// Top border
178	///
179	/// ⚠️ There is no guarantee that this is actually the most top border.
180	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
181	#[inline]
182	pub fn top(&self) -> f32 {
183		self.top_left.y
184	}
185
186	/// Bottom border
187	///
188	/// ⚠️ There is no guarantee that this is actually the most bottom border.
189	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
190	#[inline]
191	pub fn bottom(&self) -> f32 {
192		self.bottom_left.y
193	}
194
195	/// Left border
196	///
197	/// ⚠️ There is no guarantee that this is actually the most left border.
198	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
199	#[inline]
200	pub fn left(&self) -> f32 {
201		self.bottom_left.x
202	}
203
204	/// Right border
205	///
206	/// ⚠️ There is no guarantee that this is actually the most right border.
207	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
208	#[inline]
209	pub fn right(&self) -> f32 {
210		self.bottom_right.x
211	}
212
213	/// Top left corner
214	///
215	/// ⚠️ There is no guarantee that this is actually the most top and left corner.
216	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
217	#[inline]
218	pub fn top_left(&self) -> Point2<f32> {
219		self.top_left
220	}
221
222	/// Top right corner
223	///
224	/// ⚠️ There is no guarantee that this is actually the most top and right corner.
225	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
226	#[inline]
227	pub fn top_right(&self) -> Point2<f32> {
228		self.top_right
229	}
230
231	/// bottom right corner
232	///
233	/// ⚠️ There is no guarantee that this is actually the most bottom and right corner.
234	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
235	#[inline]
236	pub fn bottom_right(&self) -> Point2<f32> {
237		self.bottom_right
238	}
239
240	/// Bottom left corner
241	///
242	/// ⚠️ There is no guarantee that this is actually the most bottom and left corner.
243	/// [`straigthen`][BoundingBox::straigthen] the [`BoundingBox`] first for this guarantee.
244	#[inline]
245	pub fn bottom_left(&self) -> Point2<f32> {
246		self.bottom_left
247	}
248
249	/// Apply a transform to a [`BoundingBox`]
250	pub fn transform(self, transform: &Transform2<f32>) -> BoundingBox<UnParticular> {
251		BoundingBox {
252			_ty: PhantomData,
253			top_left: transform * self.top_left,
254			top_right: transform * self.top_right,
255			bottom_right: transform * self.bottom_right,
256			bottom_left: transform * self.bottom_left,
257		}
258	}
259
260	/// Width
261	pub fn width(&self) -> f32 {
262		(self.top_right - self.top_left).magnitude()
263	}
264
265	/// Height
266	pub fn height(&self) -> f32 {
267		(self.top_right - self.bottom_right).magnitude()
268	}
269}
270
271impl BoundingBox<UnParticular> {
272	/// Create a [`BoundingBox`] from each corner
273	pub fn new(
274		top_left: Point2<f32>,
275		top_right: Point2<f32>,
276		bottom_right: Point2<f32>,
277		bottom_left: Point2<f32>,
278	) -> Self {
279		BoundingBox {
280			_ty: PhantomData,
281			top_left,
282			top_right,
283			bottom_right,
284			bottom_left,
285		}
286	}
287
288	/// Center of a [`BoundingBox`].
289	/// If you need a [`BoundingBox<Straight>`], you should straighten it first and then call center on it.
290	pub fn center(&self) -> Point2<f32> {
291		let x =
292			(self.bottom_left.x + self.bottom_right.x + self.top_left.x + self.top_right.x) / 4.;
293		let y =
294			(self.bottom_left.y + self.bottom_right.y + self.top_left.y + self.top_right.y) / 4.;
295
296		Point2::new(x, y)
297	}
298
299	/// Straighen a [`BoundingBox`] and guarantee that the sides of the bounding box are aligns with the X and Y axis.
300	pub fn straigthen(&self) -> BoundingBox<Straight> {
301		let top = self
302			.top_left
303			.y
304			.max(self.top_right.y)
305			.max(self.bottom_left.y)
306			.max(self.bottom_right.y);
307		let bottom = self
308			.top_left
309			.y
310			.min(self.top_right.y)
311			.min(self.bottom_left.y)
312			.min(self.bottom_right.y);
313
314		let right = self
315			.top_left
316			.x
317			.max(self.top_right.x)
318			.max(self.bottom_left.x)
319			.max(self.bottom_right.x);
320		let left = self
321			.top_left
322			.x
323			.min(self.top_right.x)
324			.min(self.bottom_left.x)
325			.min(self.bottom_right.x);
326
327		BoundingBox {
328			_ty: PhantomData,
329			top_left: Point2::new(left, top),
330			top_right: Point2::new(right, top),
331			bottom_right: Point2::new(right, bottom),
332			bottom_left: Point2::new(left, bottom),
333		}
334	}
335
336	/// Same as [`straighten`] but for chaining
337	#[inline]
338	pub fn into_straight(self) -> BoundingBox<Straight> {
339		self.straigthen()
340	}
341}
342
343impl BoundingBox<Straight> {
344	/// [`BoundingBox`] center at the origin
345	pub fn zero() -> Self {
346		BoundingBox {
347			_ty: PhantomData,
348			top_left: Point2::origin(),
349			top_right: Point2::origin(),
350			bottom_right: Point2::origin(),
351			bottom_left: Point2::origin(),
352		}
353	}
354
355	/// [`BoundingBox`] center at the given point
356	pub fn at<P: Into<Point2<f32>>>(p: P) -> Self {
357		let p = p.into();
358		BoundingBox {
359			_ty: PhantomData,
360			top_left: p,
361			top_right: p,
362			bottom_right: p,
363			bottom_left: p,
364		}
365	}
366
367	/// [`BoundingBox`] from mins and maxs
368	pub fn mins_maxs(min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Self {
369		BoundingBox {
370			_ty: PhantomData,
371			top_left: [min_x, max_y].into(),
372			top_right: [max_x, max_y].into(),
373			bottom_right: [max_x, min_y].into(),
374			bottom_left: [min_x, min_y].into(),
375		}
376	}
377
378	/// [`BoundingBox`] centered at (0,0) with a given size
379	pub fn centered<V: Into<Vector2<f32>>>(size: V) -> Self {
380		let size = size.into() / 2.;
381		BoundingBox {
382			_ty: PhantomData,
383			top_left: Point2::new(-size.x, size.y),
384			top_right: Point2::new(size.x, size.y),
385			bottom_right: Point2::new(size.x, -size.y),
386			bottom_left: Point2::new(-size.x, -size.y),
387		}
388	}
389
390	/// Convert the [`BoundingBox`] to [`UnParticular`].
391	#[inline]
392	pub fn as_unparticular(self) -> BoundingBox<UnParticular> {
393		BoundingBox {
394			_ty: PhantomData,
395			top_left: self.top_left,
396			top_right: self.top_right,
397			bottom_right: self.bottom_right,
398			bottom_left: self.bottom_left,
399		}
400	}
401
402	/// Straighen a [`BoundingBox`] and guarantee that the sides of the bounding box are aligns with the X and Y axis.
403	pub fn straigthen(&self) -> BoundingBox<Straight> {
404		*self
405	}
406
407	/// Same as [`straighten`] but for chaining
408	#[inline]
409	pub fn into_straight(self) -> BoundingBox<Straight> {
410		self
411	}
412
413	/// Scale difference from self to other
414	#[inline]
415	pub fn scale_difference(&self, other: &BoundingBox<Straight>) -> Vector2<f32> {
416		Vector2::new(other.width() / self.width(), other.height() / self.height())
417	}
418
419	/// A u B
420	///
421	/// Creates a bigger [`BoundingBox`] from the union of the two.
422	pub fn join(mut self, other: BoundingBox<Straight>) -> BoundingBox<Straight> {
423		let min_x = self.bottom_left.x.min(other.bottom_left.x);
424		let min_y = self.bottom_left.y.min(other.bottom_left.y);
425		let max_x = self.top_right.x.max(other.top_right.x);
426		let max_y = self.top_right.y.max(other.top_right.y);
427
428		self.top_left.x = min_x;
429		self.top_left.y = max_y;
430
431		self.top_right.x = max_x;
432		self.top_right.y = max_y;
433
434		self.bottom_right.x = max_x;
435		self.bottom_right.y = min_y;
436
437		self.bottom_left.x = min_x;
438		self.bottom_left.y = min_y;
439
440		self
441	}
442
443	/// A n B
444	///
445	/// Creates a smaller [`BoundingBox`] from the intersection of the two.
446	pub fn intersect(mut self, other: BoundingBox<Straight>) -> BoundingBox<Straight> {
447		let (min_x, max_x) = if self.bottom_right.x <= other.bottom_left.x
448			|| self.bottom_left.x >= other.bottom_right.x
449		{
450			(0., 0.)
451		} else {
452			(
453				self.bottom_left.x.max(other.bottom_left.x),
454				self.top_right.x.min(other.top_right.x),
455			)
456		};
457
458		let (min_y, max_y) =
459			if self.top_left.y <= other.bottom_left.y || self.bottom_right.y >= other.top_left.y {
460				(0., 0.)
461			} else {
462				(
463					self.bottom_left.y.max(other.bottom_left.y),
464					self.top_right.y.min(other.top_right.y),
465				)
466			};
467
468		self.top_left.x = min_x;
469		self.top_left.y = max_y;
470		self.top_right.x = max_x;
471		self.top_right.y = max_y;
472		self.bottom_right.x = max_x;
473		self.bottom_right.y = min_y;
474		self.bottom_left.x = min_x;
475		self.bottom_left.y = min_y;
476
477		self
478	}
479
480	/// Center of a [`BoundingBox`].
481	pub fn center(&self) -> Point2<f32> {
482		let x = (self.bottom_left.x + self.top_right.x) / 2.;
483		let y = (self.bottom_left.y + self.top_right.y) / 2.;
484
485		Point2::new(x, y)
486	}
487}
488
489/// Traits that defined whether a [`Shape`] can be bound by a [`BoundingBox`]
490pub trait ShapeBoundingBox {
491	/// [`BoundingBox`] of a [`Shape`]
492	fn local_bounding_box(&self) -> BoundingBox<UnParticular>;
493	/// Absolute [`BoundingBox`] from a transform
494	fn global_bounding_box(&self, parent_transform: &Transform2<f32>) -> BoundingBox<UnParticular> {
495		self.local_bounding_box().transform(parent_transform)
496	}
497}
498
499/// A group of [`Shape`], locally positionned by a transform
500#[derive(Default, Debug, Clone, PartialEq)]
501pub struct Group {
502	/// Transform of the whole group
503	pub local_transform: Transform2<f32>,
504	/// List of shapes
505	pub shapes: Vec<Shape>,
506	/// Metadata
507	pub metadata: Vec<(String, String)>,
508}
509
510/// Building block of a dessin
511///
512/// Every complex shape should boil down to these.
513#[derive(Clone)]
514pub enum Shape {
515	/// A group of [`Shape`], locally positionned by a transform
516	Group(Group),
517	/// Block of style
518	Style {
519		/// Fill
520		fill: Option<crate::style::Fill>,
521		/// Stroke
522		stroke: Option<crate::style::Stroke>,
523		/// Styled shape. (Or Shapes if it is a [`Groupe`][Shape::Group])
524		shape: Box<Shape>,
525	},
526	/// Ellipse
527	Ellipse(Ellipse),
528	/// Image
529	Image(Image),
530	/// Text
531	Text(Text),
532	/// Curve
533	Curve(Curve),
534	/// Shape whose body is generated only during export.
535	///
536	/// Enables chirurgical changes of the shape.
537	///
538	/// See [`Dynamic`] for more details.
539	Dynamic {
540		/// Transform of the whole group
541		local_transform: Transform2<f32>,
542		/// The shape at run time
543		shaper: Arc<Shaper>,
544	},
545}
546impl PartialEq for Shape {
547	fn eq(&self, other: &Self) -> bool {
548		match (self, other) {
549			(Shape::Group(g1), Shape::Group(g2)) => g1 == g2,
550			(
551				Shape::Style {
552					fill: fill1,
553					stroke: stroke1,
554					shape: shape1,
555				},
556				Shape::Style {
557					fill: fill2,
558					stroke: stroke2,
559					shape: shape2,
560				},
561			) => fill1 == fill2 && stroke1 == stroke2 && shape1 == shape2,
562			(Shape::Ellipse(e1), Shape::Ellipse(e2)) => e1 == e2,
563			(Shape::Image(i1), Shape::Image(i2)) => i1 == i2,
564			(Shape::Text(t1), Shape::Text(t2)) => t1 == t2,
565			(Shape::Curve(c1), Shape::Curve(c2)) => c1 == c2,
566			(
567				Shape::Dynamic {
568					local_transform: local_transform1,
569					shaper: shaper1,
570				},
571				Shape::Dynamic {
572					local_transform: local_transform2,
573					shaper: shaper2,
574				},
575			) => local_transform1 == local_transform2 && shaper1() == shaper2(),
576			_ => false,
577		}
578	}
579}
580
581impl Shape {
582	/// Get current shape as group
583	/// If current shape isn't a group, morph it into a group
584	pub fn get_or_mutate_as_group(&mut self) -> &mut Group {
585		if let Shape::Group(g) = self {
586			g
587		} else {
588			let mut dummy = Shape::Group(Group {
589				local_transform: Default::default(),
590				shapes: Default::default(),
591				metadata: Default::default(),
592			});
593
594			std::mem::swap(self, &mut dummy);
595
596			let mut group = Shape::Group(Group {
597				local_transform: Default::default(),
598				shapes: vec![dummy],
599				metadata: vec![],
600			});
601
602			std::mem::swap(self, &mut group);
603			self.get_or_mutate_as_group()
604		}
605	}
606
607	/// Add some metadata
608	pub fn extend_metadata<K: ToString, V: ToString, E: IntoIterator<Item = (K, V)>>(
609		&mut self,
610		extend: E,
611	) {
612		self.get_or_mutate_as_group().metadata.extend(
613			extend
614				.into_iter()
615				.map(|(k, v)| (k.to_string(), v.to_string())),
616		);
617	}
618
619	/// Add some metadata
620	pub fn add_metadata<K: ToString, V: ToString>(&mut self, (key, value): (K, V)) {
621		let key = key.to_string();
622		let value = value.to_string();
623
624		self.get_or_mutate_as_group().metadata.push((key, value));
625	}
626}
627
628impl fmt::Debug for Shape {
629	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
630		match self {
631			Self::Group(Group {
632				local_transform,
633				shapes,
634				metadata,
635			}) => f
636				.debug_struct("Group")
637				.field("local_transform", local_transform)
638				.field("shapes", shapes)
639				.field("metadata", metadata)
640				.finish(),
641			Self::Style {
642				fill,
643				stroke,
644				shape,
645			} => f
646				.debug_struct("Style")
647				.field("fill", fill)
648				.field("stroke", stroke)
649				.field("shape", shape)
650				.finish(),
651			Self::Ellipse(arg0) => f.debug_tuple("Ellipse").field(arg0).finish(),
652			Self::Image(arg0) => f.debug_tuple("Image").field(arg0).finish(),
653			Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(),
654			Self::Curve(arg0) => f.debug_tuple("Curve").field(arg0).finish(),
655			Self::Dynamic {
656				local_transform,
657				shaper: _,
658			} => f
659				.debug_struct("Dynamic")
660				.field("local_transform", local_transform)
661				.field("shaper", &"Arc<Fn() -> Shape>")
662				.finish(),
663		}
664	}
665}
666
667impl Default for Shape {
668	fn default() -> Self {
669		Shape::Group(Group {
670			local_transform: Transform2::default(),
671			shapes: vec![],
672			metadata: vec![],
673		})
674	}
675}
676
677impl ShapeOp for Shape {
678	fn transform(&mut self, transform_matrix: Transform2<f32>) -> &mut Self {
679		match self {
680			Shape::Group(Group {
681				local_transform, ..
682			}) => {
683				*local_transform = transform_matrix * *local_transform;
684			}
685			Shape::Style { shape, .. } => {
686				shape.transform(transform_matrix);
687			}
688			Shape::Ellipse(v) => {
689				v.transform(transform_matrix);
690			}
691			Shape::Image(v) => {
692				v.transform(transform_matrix);
693			}
694			Shape::Text(v) => {
695				v.transform(transform_matrix);
696			}
697			Shape::Curve(v) => {
698				v.transform(transform_matrix);
699			}
700			Shape::Dynamic {
701				local_transform, ..
702			} => {
703				*local_transform = transform_matrix * *local_transform;
704			}
705		};
706
707		self
708	}
709
710	#[inline]
711	fn local_transform(&self) -> &Transform2<f32> {
712		match self {
713			Shape::Group(Group {
714				local_transform, ..
715			}) => local_transform,
716			Shape::Style { shape, .. } => shape.local_transform(),
717			Shape::Ellipse(v) => v.local_transform(),
718			Shape::Image(v) => v.local_transform(),
719			Shape::Text(v) => v.local_transform(),
720			Shape::Curve(v) => v.local_transform(),
721			Shape::Dynamic {
722				local_transform, ..
723			} => local_transform,
724		}
725	}
726}
727
728impl ShapeBoundingBox for Shape {
729	fn local_bounding_box(&self) -> BoundingBox<UnParticular> {
730		match self {
731			Shape::Group(Group {
732				local_transform,
733				shapes,
734				..
735			}) => shapes
736				.iter()
737				.map(|v| v.global_bounding_box(local_transform).straigthen())
738				.reduce(BoundingBox::join)
739				.unwrap_or_else(|| BoundingBox::zero())
740				.as_unparticular(),
741			Shape::Style { shape, .. } => shape.local_bounding_box(),
742			Shape::Ellipse(e) => e.local_bounding_box(),
743			Shape::Image(i) => i.local_bounding_box(),
744			Shape::Text(t) => t.local_bounding_box(),
745			Shape::Curve(c) => c.local_bounding_box(),
746			Shape::Dynamic {
747				local_transform,
748				shaper,
749			} => shaper().local_bounding_box().transform(local_transform),
750		}
751	}
752}
753
754#[cfg(test)]
755mod tests {
756	use crate::prelude::*;
757	use nalgebra::{Point2, Rotation2, Transform2};
758	use std::f32::consts::FRAC_PI_2;
759
760	const EPS: f32 = 10e-6;
761
762	#[test]
763	fn parent_rotate_child_scale() {
764		let base = dessin!(Image(scale = [2., 4.], translate = [1., 2.]));
765
766		let base_position = base.position(&Transform2::default());
767		assert!(
768			(base_position.bottom_left - Point2::new(0., 0.)).magnitude() < EPS,
769			"left = {}, right = [0., 0.]",
770			base_position.bottom_left,
771		);
772		assert!(
773			(base_position.top_left - Point2::new(0., 4.)).magnitude() < EPS,
774			"left = {}, right = [0., 4.]",
775			base_position.top_left,
776		);
777		assert!(
778			(base_position.top_right - Point2::new(2., 4.)).magnitude() < EPS,
779			"left = {}, right = [2., 4.]",
780			base_position.top_right,
781		);
782
783		let transform = nalgebra::convert(Rotation2::new(FRAC_PI_2));
784		let transform_position = base.position(&transform);
785		assert!(
786			(transform_position.bottom_left - Point2::new(0., 0.)).magnitude() < EPS,
787			"left = {}, right = [0., 0.]",
788			transform_position.bottom_left,
789		);
790		assert!(
791			(transform_position.top_left - Point2::new(-4., 0.)).magnitude() < EPS,
792			"left = {}, right = [-4., 0.]",
793			transform_position.top_left,
794		);
795		assert!(
796			(transform_position.top_right - Point2::new(-4., 2.)).magnitude() < EPS,
797			"left = {}, right = [-4., 2.]",
798			transform_position.top_right,
799		);
800	}
801}