1 | // Copyright 2018 the Kurbo Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | //! Affine transforms. |
5 | |
6 | use core::ops::{Mul, MulAssign}; |
7 | |
8 | use crate::{Point, Rect, Vec2}; |
9 | |
10 | #[cfg (not(feature = "std" ))] |
11 | use crate::common::FloatFuncs; |
12 | |
13 | /// A 2D affine transform. |
14 | #[derive (Clone, Copy, Debug, PartialEq)] |
15 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
16 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
17 | pub struct Affine([f64; 6]); |
18 | |
19 | impl Affine { |
20 | /// The identity transform. |
21 | pub const IDENTITY: Affine = Affine::scale(1.0); |
22 | |
23 | /// A transform that is flipped on the y-axis. Useful for converting between |
24 | /// y-up and y-down spaces. |
25 | pub const FLIP_Y: Affine = Affine::new([1.0, 0., 0., -1.0, 0., 0.]); |
26 | |
27 | /// A transform that is flipped on the x-axis. |
28 | pub const FLIP_X: Affine = Affine::new([-1.0, 0., 0., 1.0, 0., 0.]); |
29 | |
30 | /// Construct an affine transform from coefficients. |
31 | /// |
32 | /// If the coefficients are `(a, b, c, d, e, f)`, then the resulting |
33 | /// transformation represents this augmented matrix: |
34 | /// |
35 | /// ```text |
36 | /// | a c e | |
37 | /// | b d f | |
38 | /// | 0 0 1 | |
39 | /// ``` |
40 | /// |
41 | /// Note that this convention is transposed from PostScript and |
42 | /// Direct2D, but is consistent with the |
43 | /// [Wikipedia](https://en.wikipedia.org/wiki/Affine_transformation) |
44 | /// formulation of affine transformation as augmented matrix. The |
45 | /// idea is that `(A * B) * v == A * (B * v)`, where `*` is the |
46 | /// [`Mul`](std::ops::Mul) trait. |
47 | #[inline ] |
48 | pub const fn new(c: [f64; 6]) -> Affine { |
49 | Affine(c) |
50 | } |
51 | |
52 | /// An affine transform representing uniform scaling. |
53 | #[inline ] |
54 | pub const fn scale(s: f64) -> Affine { |
55 | Affine([s, 0.0, 0.0, s, 0.0, 0.0]) |
56 | } |
57 | |
58 | /// An affine transform representing non-uniform scaling |
59 | /// with different scale values for x and y |
60 | #[inline ] |
61 | pub const fn scale_non_uniform(s_x: f64, s_y: f64) -> Affine { |
62 | Affine([s_x, 0.0, 0.0, s_y, 0.0, 0.0]) |
63 | } |
64 | |
65 | /// An affine transform representing rotation. |
66 | /// |
67 | /// The convention for rotation is that a positive angle rotates a |
68 | /// positive X direction into positive Y. Thus, in a Y-down coordinate |
69 | /// system (as is common for graphics), it is a clockwise rotation, and |
70 | /// in Y-up (traditional for math), it is anti-clockwise. |
71 | /// |
72 | /// The angle, `th`, is expressed in radians. |
73 | #[inline ] |
74 | pub fn rotate(th: f64) -> Affine { |
75 | let (s, c) = th.sin_cos(); |
76 | Affine([c, s, -s, c, 0.0, 0.0]) |
77 | } |
78 | |
79 | /// An affine transform representing a rotation of `th` radians about `center`. |
80 | /// |
81 | /// See [`Affine::rotate()`] for more info. |
82 | #[inline ] |
83 | pub fn rotate_about(th: f64, center: Point) -> Affine { |
84 | let center = center.to_vec2(); |
85 | Self::translate(-center) |
86 | .then_rotate(th) |
87 | .then_translate(center) |
88 | } |
89 | |
90 | /// An affine transform representing translation. |
91 | #[inline ] |
92 | pub fn translate<V: Into<Vec2>>(p: V) -> Affine { |
93 | let p = p.into(); |
94 | Affine([1.0, 0.0, 0.0, 1.0, p.x, p.y]) |
95 | } |
96 | |
97 | /// An affine transformation representing a skew. |
98 | /// |
99 | /// The `skew_x` and `skew_y` parameters represent skew factors for the |
100 | /// horizontal and vertical directions, respectively. |
101 | /// |
102 | /// This is commonly used to generate a faux oblique transform for |
103 | /// font rendering. In this case, you can slant the glyph 20 degrees |
104 | /// clockwise in the horizontal direction (assuming a Y-up coordinate |
105 | /// system): |
106 | /// |
107 | /// ``` |
108 | /// let oblique_transform = kurbo::Affine::skew(20f64.to_radians().tan(), 0.0); |
109 | /// ``` |
110 | #[inline ] |
111 | pub fn skew(skew_x: f64, skew_y: f64) -> Affine { |
112 | Affine([1.0, skew_y, skew_x, 1.0, 0.0, 0.0]) |
113 | } |
114 | |
115 | /// Create an affine transform that represents reflection about the line `point + direction * t, t in (-infty, infty)` |
116 | /// |
117 | /// # Examples |
118 | /// |
119 | /// ``` |
120 | /// # use kurbo::{Point, Vec2, Affine}; |
121 | /// # fn assert_near(p0: Point, p1: Point) { |
122 | /// # assert!((p1 - p0).hypot() < 1e-9, "{p0:?} != {p1:?}" ); |
123 | /// # } |
124 | /// let point = Point::new(1., 0.); |
125 | /// let vec = Vec2::new(1., 1.); |
126 | /// let map = Affine::reflect(point, vec); |
127 | /// assert_near(map * Point::new(1., 0.), Point::new(1., 0.)); |
128 | /// assert_near(map * Point::new(2., 1.), Point::new(2., 1.)); |
129 | /// assert_near(map * Point::new(2., 2.), Point::new(3., 1.)); |
130 | /// ``` |
131 | #[inline ] |
132 | #[must_use ] |
133 | pub fn reflect(point: impl Into<Point>, direction: impl Into<Vec2>) -> Self { |
134 | let point = point.into(); |
135 | let direction = direction.into(); |
136 | |
137 | let n = Vec2 { |
138 | x: direction.y, |
139 | y: -direction.x, |
140 | } |
141 | .normalize(); |
142 | |
143 | // Compute Householder reflection matrix |
144 | let x2 = n.x * n.x; |
145 | let xy = n.x * n.y; |
146 | let y2 = n.y * n.y; |
147 | // Here we also add in the post translation, because it doesn't require any further calc. |
148 | let aff = Affine::new([ |
149 | 1. - 2. * x2, |
150 | -2. * xy, |
151 | -2. * xy, |
152 | 1. - 2. * y2, |
153 | point.x, |
154 | point.y, |
155 | ]); |
156 | aff.pre_translate(-point.to_vec2()) |
157 | } |
158 | |
159 | /// A [rotation] by `th` followed by `self`. |
160 | /// |
161 | /// Equivalent to `self * Affine::rotate(th)` |
162 | /// |
163 | /// [rotation]: Affine::rotate |
164 | #[inline ] |
165 | #[must_use ] |
166 | pub fn pre_rotate(self, th: f64) -> Self { |
167 | self * Affine::rotate(th) |
168 | } |
169 | |
170 | /// A [rotation] by `th` about `center` followed by `self`. |
171 | /// |
172 | /// Equivalent to `self * Affine::rotate_about(th, center)` |
173 | /// |
174 | /// [rotation]: Affine::rotate_about |
175 | #[inline ] |
176 | #[must_use ] |
177 | pub fn pre_rotate_about(self, th: f64, center: Point) -> Self { |
178 | Affine::rotate_about(th, center) * self |
179 | } |
180 | |
181 | /// A [scale] by `scale` followed by `self`. |
182 | /// |
183 | /// Equivalent to `self * Affine::scale(scale)` |
184 | /// |
185 | /// [scale]: Affine::scale |
186 | #[inline ] |
187 | #[must_use ] |
188 | pub fn pre_scale(self, scale: f64) -> Self { |
189 | self * Affine::scale(scale) |
190 | } |
191 | |
192 | /// A [scale] by `(scale_x, scale_y)` followed by `self`. |
193 | /// |
194 | /// Equivalent to `self * Affine::scale_non_uniform(scale_x, scale_y)` |
195 | /// |
196 | /// [scale]: Affine::scale_non_uniform |
197 | #[inline ] |
198 | #[must_use ] |
199 | pub fn pre_scale_non_uniform(self, scale_x: f64, scale_y: f64) -> Self { |
200 | self * Affine::scale_non_uniform(scale_x, scale_y) |
201 | } |
202 | |
203 | /// A [translation] of `trans` followed by `self`. |
204 | /// |
205 | /// Equivalent to `self * Affine::translate(trans)` |
206 | /// |
207 | /// [translation]: Affine::translate |
208 | #[inline ] |
209 | #[must_use ] |
210 | pub fn pre_translate(self, trans: Vec2) -> Self { |
211 | self * Affine::translate(trans) |
212 | } |
213 | |
214 | /// `self` followed by a [rotation] of `th`. |
215 | /// |
216 | /// Equivalent to `Affine::rotate(th) * self` |
217 | /// |
218 | /// [rotation]: Affine::rotate |
219 | #[inline ] |
220 | #[must_use ] |
221 | pub fn then_rotate(self, th: f64) -> Self { |
222 | Affine::rotate(th) * self |
223 | } |
224 | |
225 | /// `self` followed by a [rotation] of `th` about `center`. |
226 | /// |
227 | /// Equivalent to `Affine::rotate_about(th, center) * self` |
228 | /// |
229 | /// [rotation]: Affine::rotate_about |
230 | #[inline ] |
231 | #[must_use ] |
232 | pub fn then_rotate_about(self, th: f64, center: Point) -> Self { |
233 | Affine::rotate_about(th, center) * self |
234 | } |
235 | |
236 | /// `self` followed by a [scale] of `scale`. |
237 | /// |
238 | /// Equivalent to `Affine::scale(scale) * self` |
239 | /// |
240 | /// [scale]: Affine::scale |
241 | #[inline ] |
242 | #[must_use ] |
243 | pub fn then_scale(self, scale: f64) -> Self { |
244 | Affine::scale(scale) * self |
245 | } |
246 | |
247 | /// `self` followed by a [scale] of `(scale_x, scale_y)`. |
248 | /// |
249 | /// Equivalent to `Affine::scale_non_uniform(scale_x, scale_y) * self` |
250 | /// |
251 | /// [scale]: Affine::scale_non_uniform |
252 | #[inline ] |
253 | #[must_use ] |
254 | pub fn then_scale_non_uniform(self, scale_x: f64, scale_y: f64) -> Self { |
255 | Affine::scale_non_uniform(scale_x, scale_y) * self |
256 | } |
257 | |
258 | /// `self` followed by a translation of `trans`. |
259 | /// |
260 | /// Equivalent to `Affine::translate(trans) * self` |
261 | /// |
262 | /// [translation]: Affine::translate |
263 | #[inline ] |
264 | #[must_use ] |
265 | pub fn then_translate(mut self, trans: Vec2) -> Self { |
266 | self.0[4] += trans.x; |
267 | self.0[5] += trans.y; |
268 | self |
269 | } |
270 | |
271 | /// Creates an affine transformation that takes the unit square to the given rectangle. |
272 | /// |
273 | /// Useful when you want to draw into the unit square but have your output fill any rectangle. |
274 | /// In this case push the `Affine` onto the transform stack. |
275 | pub fn map_unit_square(rect: Rect) -> Affine { |
276 | Affine([rect.width(), 0., 0., rect.height(), rect.x0, rect.y0]) |
277 | } |
278 | |
279 | /// Get the coefficients of the transform. |
280 | #[inline ] |
281 | pub fn as_coeffs(self) -> [f64; 6] { |
282 | self.0 |
283 | } |
284 | |
285 | /// Compute the determinant of this transform. |
286 | pub fn determinant(self) -> f64 { |
287 | self.0[0] * self.0[3] - self.0[1] * self.0[2] |
288 | } |
289 | |
290 | /// Compute the inverse transform. |
291 | /// |
292 | /// Produces NaN values when the determinant is zero. |
293 | pub fn inverse(self) -> Affine { |
294 | let inv_det = self.determinant().recip(); |
295 | Affine([ |
296 | inv_det * self.0[3], |
297 | -inv_det * self.0[1], |
298 | -inv_det * self.0[2], |
299 | inv_det * self.0[0], |
300 | inv_det * (self.0[2] * self.0[5] - self.0[3] * self.0[4]), |
301 | inv_det * (self.0[1] * self.0[4] - self.0[0] * self.0[5]), |
302 | ]) |
303 | } |
304 | |
305 | /// Compute the bounding box of a transformed rectangle. |
306 | /// |
307 | /// Returns the minimal `Rect` that encloses the given `Rect` after affine transformation. |
308 | /// If the transform is axis-aligned, then this bounding box is "tight", in other words the |
309 | /// returned `Rect` is the transformed rectangle. |
310 | /// |
311 | /// The returned rectangle always has non-negative width and height. |
312 | pub fn transform_rect_bbox(self, rect: Rect) -> Rect { |
313 | let p00 = self * Point::new(rect.x0, rect.y0); |
314 | let p01 = self * Point::new(rect.x0, rect.y1); |
315 | let p10 = self * Point::new(rect.x1, rect.y0); |
316 | let p11 = self * Point::new(rect.x1, rect.y1); |
317 | Rect::from_points(p00, p01).union(Rect::from_points(p10, p11)) |
318 | } |
319 | |
320 | /// Is this map [finite]? |
321 | /// |
322 | /// [finite]: f64::is_finite |
323 | #[inline ] |
324 | pub fn is_finite(&self) -> bool { |
325 | self.0[0].is_finite() |
326 | && self.0[1].is_finite() |
327 | && self.0[2].is_finite() |
328 | && self.0[3].is_finite() |
329 | && self.0[4].is_finite() |
330 | && self.0[5].is_finite() |
331 | } |
332 | |
333 | /// Is this map [NaN]? |
334 | /// |
335 | /// [NaN]: f64::is_nan |
336 | #[inline ] |
337 | pub fn is_nan(&self) -> bool { |
338 | self.0[0].is_nan() |
339 | || self.0[1].is_nan() |
340 | || self.0[2].is_nan() |
341 | || self.0[3].is_nan() |
342 | || self.0[4].is_nan() |
343 | || self.0[5].is_nan() |
344 | } |
345 | |
346 | /// Compute the singular value decomposition of the linear transformation (ignoring the |
347 | /// translation). |
348 | /// |
349 | /// All non-degenerate linear transformations can be represented as |
350 | /// |
351 | /// 1. a rotation about the origin. |
352 | /// 2. a scaling along the x and y axes |
353 | /// 3. another rotation about the origin |
354 | /// |
355 | /// composed together. Decomposing a 2x2 matrix in this way is called a "singular value |
356 | /// decomposition" and is written `U Σ V^T`, where U and V^T are orthogonal (rotations) and Σ |
357 | /// is a diagonal matrix (a scaling). |
358 | /// |
359 | /// Since currently this function is used to calculate ellipse radii and rotation from an |
360 | /// affine map on the unit circle, we don't calculate V^T, since a rotation of the unit (or |
361 | /// any) circle about its center always results in the same circle. This is the reason that an |
362 | /// ellipse mapped using an affine map is always an ellipse. |
363 | /// |
364 | /// Will return NaNs if the matrix (or equivalently the linear map) is singular. |
365 | /// |
366 | /// First part of the return tuple is the scaling, second part is the angle of rotation (in |
367 | /// radians) |
368 | #[inline ] |
369 | pub(crate) fn svd(self) -> (Vec2, f64) { |
370 | let a = self.0[0]; |
371 | let a2 = a * a; |
372 | let b = self.0[1]; |
373 | let b2 = b * b; |
374 | let c = self.0[2]; |
375 | let c2 = c * c; |
376 | let d = self.0[3]; |
377 | let d2 = d * d; |
378 | let ab = a * b; |
379 | let cd = c * d; |
380 | let angle = 0.5 * (2.0 * (ab + cd)).atan2(a2 - b2 + c2 - d2); |
381 | let s1 = a2 + b2 + c2 + d2; |
382 | let s2 = ((a2 - b2 + c2 - d2).powi(2) + 4.0 * (ab + cd).powi(2)).sqrt(); |
383 | ( |
384 | Vec2 { |
385 | x: (0.5 * (s1 + s2)).sqrt(), |
386 | y: (0.5 * (s1 - s2)).sqrt(), |
387 | }, |
388 | angle, |
389 | ) |
390 | } |
391 | |
392 | /// Returns the translation part of this affine map (`(self.0[4], self.0[5])`). |
393 | #[inline ] |
394 | pub fn translation(self) -> Vec2 { |
395 | Vec2 { |
396 | x: self.0[4], |
397 | y: self.0[5], |
398 | } |
399 | } |
400 | |
401 | /// Replaces the translation portion of this affine map |
402 | /// |
403 | /// The translation can be seen as being applied after the linear part of the map. |
404 | #[must_use ] |
405 | #[inline ] |
406 | pub fn with_translation(mut self, trans: Vec2) -> Affine { |
407 | self.0[4] = trans.x; |
408 | self.0[5] = trans.y; |
409 | self |
410 | } |
411 | } |
412 | |
413 | impl Default for Affine { |
414 | #[inline ] |
415 | fn default() -> Affine { |
416 | Affine::IDENTITY |
417 | } |
418 | } |
419 | |
420 | impl Mul<Point> for Affine { |
421 | type Output = Point; |
422 | |
423 | #[inline ] |
424 | fn mul(self, other: Point) -> Point { |
425 | Point::new( |
426 | self.0[0] * other.x + self.0[2] * other.y + self.0[4], |
427 | self.0[1] * other.x + self.0[3] * other.y + self.0[5], |
428 | ) |
429 | } |
430 | } |
431 | |
432 | impl Mul for Affine { |
433 | type Output = Affine; |
434 | |
435 | #[inline ] |
436 | fn mul(self, other: Affine) -> Affine { |
437 | Affine([ |
438 | self.0[0] * other.0[0] + self.0[2] * other.0[1], |
439 | self.0[1] * other.0[0] + self.0[3] * other.0[1], |
440 | self.0[0] * other.0[2] + self.0[2] * other.0[3], |
441 | self.0[1] * other.0[2] + self.0[3] * other.0[3], |
442 | self.0[0] * other.0[4] + self.0[2] * other.0[5] + self.0[4], |
443 | self.0[1] * other.0[4] + self.0[3] * other.0[5] + self.0[5], |
444 | ]) |
445 | } |
446 | } |
447 | |
448 | impl MulAssign for Affine { |
449 | #[inline ] |
450 | fn mul_assign(&mut self, other: Affine) { |
451 | *self = self.mul(other); |
452 | } |
453 | } |
454 | |
455 | impl Mul<Affine> for f64 { |
456 | type Output = Affine; |
457 | |
458 | #[inline ] |
459 | fn mul(self, other: Affine) -> Affine { |
460 | Affine([ |
461 | self * other.0[0], |
462 | self * other.0[1], |
463 | self * other.0[2], |
464 | self * other.0[3], |
465 | self * other.0[4], |
466 | self * other.0[5], |
467 | ]) |
468 | } |
469 | } |
470 | |
471 | // Conversions to and from mint |
472 | #[cfg (feature = "mint" )] |
473 | impl From<Affine> for mint::ColumnMatrix2x3<f64> { |
474 | #[inline ] |
475 | fn from(a: Affine) -> mint::ColumnMatrix2x3<f64> { |
476 | mint::ColumnMatrix2x3 { |
477 | x: mint::Vector2 { |
478 | x: a.0[0], |
479 | y: a.0[1], |
480 | }, |
481 | y: mint::Vector2 { |
482 | x: a.0[2], |
483 | y: a.0[3], |
484 | }, |
485 | z: mint::Vector2 { |
486 | x: a.0[4], |
487 | y: a.0[5], |
488 | }, |
489 | } |
490 | } |
491 | } |
492 | |
493 | #[cfg (feature = "mint" )] |
494 | impl From<mint::ColumnMatrix2x3<f64>> for Affine { |
495 | #[inline ] |
496 | fn from(m: mint::ColumnMatrix2x3<f64>) -> Affine { |
497 | Affine([m.x.x, m.x.y, m.y.x, m.y.y, m.z.x, m.z.y]) |
498 | } |
499 | } |
500 | |
501 | #[cfg (test)] |
502 | mod tests { |
503 | use crate::{Affine, Point, Vec2}; |
504 | use std::f64::consts::PI; |
505 | |
506 | fn assert_near(p0: Point, p1: Point) { |
507 | assert!((p1 - p0).hypot() < 1e-9, "{p0:?} != {p1:?}" ); |
508 | } |
509 | |
510 | fn affine_assert_near(a0: Affine, a1: Affine) { |
511 | for i in 0..6 { |
512 | assert!((a0.0[i] - a1.0[i]).abs() < 1e-9, "{a0:?} != {a1:?}" ); |
513 | } |
514 | } |
515 | |
516 | #[test ] |
517 | fn affine_basic() { |
518 | let p = Point::new(3.0, 4.0); |
519 | |
520 | assert_near(Affine::default() * p, p); |
521 | assert_near(Affine::scale(2.0) * p, Point::new(6.0, 8.0)); |
522 | assert_near(Affine::rotate(0.0) * p, p); |
523 | assert_near(Affine::rotate(PI / 2.0) * p, Point::new(-4.0, 3.0)); |
524 | assert_near(Affine::translate((5.0, 6.0)) * p, Point::new(8.0, 10.0)); |
525 | assert_near(Affine::skew(0.0, 0.0) * p, p); |
526 | assert_near(Affine::skew(2.0, 4.0) * p, Point::new(11.0, 16.0)); |
527 | } |
528 | |
529 | #[test ] |
530 | fn affine_mul() { |
531 | let a1 = Affine::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); |
532 | let a2 = Affine::new([0.1, 1.2, 2.3, 3.4, 4.5, 5.6]); |
533 | |
534 | let px = Point::new(1.0, 0.0); |
535 | let py = Point::new(0.0, 1.0); |
536 | let pxy = Point::new(1.0, 1.0); |
537 | assert_near(a1 * (a2 * px), (a1 * a2) * px); |
538 | assert_near(a1 * (a2 * py), (a1 * a2) * py); |
539 | assert_near(a1 * (a2 * pxy), (a1 * a2) * pxy); |
540 | } |
541 | |
542 | #[test ] |
543 | fn affine_inv() { |
544 | let a = Affine::new([0.1, 1.2, 2.3, 3.4, 4.5, 5.6]); |
545 | let a_inv = a.inverse(); |
546 | |
547 | let px = Point::new(1.0, 0.0); |
548 | let py = Point::new(0.0, 1.0); |
549 | let pxy = Point::new(1.0, 1.0); |
550 | assert_near(a * (a_inv * px), px); |
551 | assert_near(a * (a_inv * py), py); |
552 | assert_near(a * (a_inv * pxy), pxy); |
553 | assert_near(a_inv * (a * px), px); |
554 | assert_near(a_inv * (a * py), py); |
555 | assert_near(a_inv * (a * pxy), pxy); |
556 | } |
557 | |
558 | #[test ] |
559 | fn reflection() { |
560 | affine_assert_near( |
561 | Affine::reflect(Point::ZERO, (1., 0.)), |
562 | Affine::new([1., 0., 0., -1., 0., 0.]), |
563 | ); |
564 | affine_assert_near( |
565 | Affine::reflect(Point::ZERO, (0., 1.)), |
566 | Affine::new([-1., 0., 0., 1., 0., 0.]), |
567 | ); |
568 | // y = x |
569 | affine_assert_near( |
570 | Affine::reflect(Point::ZERO, (1., 1.)), |
571 | Affine::new([0., 1., 1., 0., 0., 0.]), |
572 | ); |
573 | |
574 | // no translate |
575 | let point = Point::new(0., 0.); |
576 | let vec = Vec2::new(1., 1.); |
577 | let map = Affine::reflect(point, vec); |
578 | assert_near(map * Point::new(0., 0.), Point::new(0., 0.)); |
579 | assert_near(map * Point::new(1., 1.), Point::new(1., 1.)); |
580 | assert_near(map * Point::new(1., 2.), Point::new(2., 1.)); |
581 | |
582 | // with translate |
583 | let point = Point::new(1., 0.); |
584 | let vec = Vec2::new(1., 1.); |
585 | let map = Affine::reflect(point, vec); |
586 | assert_near(map * Point::new(1., 0.), Point::new(1., 0.)); |
587 | assert_near(map * Point::new(2., 1.), Point::new(2., 1.)); |
588 | assert_near(map * Point::new(2., 2.), Point::new(3., 1.)); |
589 | } |
590 | } |
591 | |