1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Point {
11 pub x: i32,
12 pub y: i32,
13}
14
15impl Point {
16 pub const fn new(x: i32, y: i32) -> Self {
17 Self { x, y }
18 }
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub struct Size {
23 pub w: i32,
24 pub h: i32,
25}
26
27impl Size {
28 pub const fn new(w: i32, h: i32) -> Self {
29 Self { w, h }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Rect {
35 pub x: i32,
36 pub y: i32,
37 pub w: i32,
38 pub h: i32,
39}
40
41impl Rect {
42 pub const fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
43 Self { x, y, w, h }
44 }
45
46 pub const fn contains(&self, p: Point) -> bool {
47 p.x >= self.x && p.y >= self.y && p.x < self.x + self.w && p.y < self.y + self.h
48 }
49
50 #[must_use]
56 pub fn clamp_point(&self, p: Point) -> Point {
57 Point::new(
58 p.x.clamp(self.x, self.x + (self.w - 1).max(0)),
59 p.y.clamp(self.y, self.y + (self.h - 1).max(0)),
60 )
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum ToolKind {
67 Rect,
68 Circle,
71 Ellipse,
72 Triangle,
73 Polygon,
75 Freehand,
77 Measure,
81 Poly,
84}
85
86impl ToolKind {
87 #[must_use]
88 pub const fn next(self) -> Self {
89 match self {
90 Self::Rect => Self::Ellipse,
91 Self::Circle | Self::Ellipse => Self::Triangle,
92 Self::Triangle => Self::Polygon,
93 Self::Polygon => Self::Freehand,
94 Self::Freehand => Self::Measure,
95 Self::Measure | Self::Poly => Self::Rect,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ResizeHandle {
103 CircleRadius,
105 RectEdges {
107 left: bool,
108 right: bool,
109 top: bool,
110 bottom: bool,
111 },
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(untagged)]
124pub enum Shape {
125 Rect(Rect),
126 Circle {
127 cx: i32,
128 cy: i32,
129 r: i32,
130 },
131 Ellipse {
135 cx: i32,
136 cy: i32,
137 rx: i32,
138 ry: i32,
139 },
140 Triangle {
141 ax: i32,
142 ay: i32,
143 bx: i32,
144 by: i32,
145 cx: i32,
146 cy: i32,
147 },
148 Poly {
152 points: Vec<Point>,
153 },
154}
155
156impl Shape {
157 pub fn compute_preview(
163 tool: ToolKind,
164 start: Point,
165 current: Point,
166 region: Rect,
167 lock: bool,
168 ) -> Option<Self> {
169 let cx = current.x.clamp(region.x, region.x + region.w - 1);
174 let cy = current.y.clamp(region.y, region.y + region.h - 1);
175 match tool {
176 ToolKind::Rect | ToolKind::Triangle | ToolKind::Ellipse => {
177 let x = start.x.min(cx);
178 let y = start.y.min(cy);
179 let w = (start.x - cx).abs();
180 let h = (start.y - cy).abs();
181 if w <= 1 || h <= 1 {
182 return None;
183 }
184 let bbox = Rect::new(x, y, w, h);
185 Some(match tool {
186 ToolKind::Rect => Self::Rect(bbox),
187 ToolKind::Ellipse => ellipse_in_box(bbox, lock),
188 _ => triangle_in_box(bbox),
189 })
190 }
191 ToolKind::Circle => {
192 let dx = f64::from(start.x - cx);
193 let dy = f64::from(start.y - cy);
194 let r = dx.hypot(dy) as i32;
195 if r <= 0 {
196 return None;
197 }
198 Some(Self::Circle {
199 cx: start.x,
200 cy: start.y,
201 r,
202 })
203 }
204 ToolKind::Polygon | ToolKind::Freehand | ToolKind::Poly | ToolKind::Measure => None,
210 }
211 }
212
213 pub const fn kind(&self) -> ToolKind {
214 match self {
215 Self::Rect(_) => ToolKind::Rect,
216 Self::Circle { .. } => ToolKind::Circle,
217 Self::Ellipse { .. } => ToolKind::Ellipse,
218 Self::Triangle { .. } => ToolKind::Triangle,
219 Self::Poly { .. } => ToolKind::Poly,
220 }
221 }
222
223 pub fn bbox(&self) -> Rect {
226 match *self {
227 Self::Poly { ref points } => {
228 let mut x0 = i32::MAX;
229 let mut y0 = i32::MAX;
230 let mut x1 = i32::MIN;
231 let mut y1 = i32::MIN;
232 for p in points {
233 x0 = x0.min(p.x);
234 y0 = y0.min(p.y);
235 x1 = x1.max(p.x);
236 y1 = y1.max(p.y);
237 }
238 if points.is_empty() {
239 return Rect::new(0, 0, 0, 0);
240 }
241 Rect::new(x0, y0, x1.saturating_sub(x0), y1.saturating_sub(y0))
242 }
243 Self::Rect(r) => r,
244 Self::Ellipse { cx, cy, rx, ry } => Rect::new(
245 cx.saturating_sub(rx),
246 cy.saturating_sub(ry),
247 rx.saturating_mul(2),
248 ry.saturating_mul(2),
249 ),
250 Self::Circle { cx, cy, r } => Rect::new(
251 cx.saturating_sub(r),
252 cy.saturating_sub(r),
253 r.saturating_mul(2),
254 r.saturating_mul(2),
255 ),
256 Self::Triangle {
257 ax,
258 ay,
259 bx,
260 by,
261 cx,
262 cy,
263 } => {
264 let x0 = min3(ax, bx, cx);
265 let y0 = min3(ay, by, cy);
266 Rect::new(
267 x0,
268 y0,
269 max3(ax, bx, cx).saturating_sub(x0),
270 max3(ay, by, cy).saturating_sub(y0),
271 )
272 }
273 }
274 }
275
276 pub fn hit_test(&self, p: Point) -> bool {
279 match *self {
280 Self::Poly { ref points } => point_in_poly(points, p),
281 Self::Rect(r) => r.contains(p),
282 Self::Ellipse { cx, cy, rx, ry } => {
283 let dx = i128::from(p.x - cx);
286 let dy = i128::from(p.y - cy);
287 let rx = i128::from(rx);
288 let ry = i128::from(ry);
289 dx * dx * ry * ry + dy * dy * rx * rx <= rx * rx * ry * ry
290 }
291 Self::Circle { cx, cy, r } => {
292 let dx = i64::from(p.x - cx);
293 let dy = i64::from(p.y - cy);
294 dx * dx + dy * dy <= i64::from(r) * i64::from(r)
295 }
296 Self::Triangle {
297 ax,
298 ay,
299 bx,
300 by,
301 cx,
302 cy,
303 } => {
304 if cross(cx, cy, ax, ay, bx, by) == 0 {
307 return false;
308 }
309 let d1 = cross(p.x, p.y, ax, ay, bx, by);
312 let d2 = cross(p.x, p.y, bx, by, cx, cy);
313 let d3 = cross(p.x, p.y, cx, cy, ax, ay);
314 let has_neg = d1 < 0 || d2 < 0 || d3 < 0;
315 let has_pos = d1 > 0 || d2 > 0 || d3 > 0;
316 !(has_neg && has_pos)
317 }
318 }
319 }
320
321 pub fn covers(&self, x: i32, y: i32) -> bool {
324 self.hit_test(Point::new(x, y))
325 }
326
327 pub fn click_point(&self) -> Point {
333 match *self {
334 Self::Poly { ref points } => poly_interior_point(points),
335 Self::Rect(_) => self.pivot(),
336 Self::Circle { cx, cy, .. } | Self::Ellipse { cx, cy, .. } => Point::new(cx, cy),
337 Self::Triangle {
338 ax,
339 ay,
340 bx,
341 by,
342 cx,
343 cy,
344 } => Point::new(
345 ((i64::from(ax) + i64::from(bx) + i64::from(cx)) / 3) as i32,
346 ((i64::from(ay) + i64::from(by) + i64::from(cy)) / 3) as i32,
347 ),
348 }
349 }
350
351 pub fn grab_origin(&self) -> Point {
354 match *self {
355 Self::Rect(r) => Point::new(r.x, r.y),
356 Self::Circle { cx, cy, .. } | Self::Ellipse { cx, cy, .. } => Point::new(cx, cy),
357 Self::Triangle { .. } | Self::Poly { .. } => {
358 let b = self.bbox();
359 Point::new(b.x, b.y)
360 }
361 }
362 }
363
364 #[must_use]
367 pub fn clamp_move(&self, grab_offset: Point, cursor: Point, region: Rect) -> Self {
368 let right = region.x + region.w;
372 let bottom = region.y + region.h;
373 match *self {
374 Self::Rect(rect) => {
375 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - rect.w).max(region.x));
376 let ny =
377 (cursor.y - grab_offset.y).clamp(region.y, (bottom - rect.h).max(region.y));
378 Self::Rect(Rect::new(nx, ny, rect.w, rect.h))
379 }
380 Self::Circle { r, .. } => {
381 let min_x = region.x + r.max(0);
382 let min_y = region.y + r.max(0);
383 let cx = (cursor.x - grab_offset.x).clamp(min_x, (right - r).max(min_x));
384 let cy = (cursor.y - grab_offset.y).clamp(min_y, (bottom - r).max(min_y));
385 Self::Circle { cx, cy, r }
386 }
387 Self::Ellipse { rx, ry, .. } => {
388 let min_x = region.x + rx.max(0);
389 let min_y = region.y + ry.max(0);
390 let cx = (cursor.x - grab_offset.x).clamp(min_x, (right - rx).max(min_x));
391 let cy = (cursor.y - grab_offset.y).clamp(min_y, (bottom - ry).max(min_y));
392 Self::Ellipse { cx, cy, rx, ry }
393 }
394 Self::Triangle { .. } | Self::Poly { .. } => {
395 let b = self.bbox();
396 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - b.w).max(region.x));
397 let ny = (cursor.y - grab_offset.y).clamp(region.y, (bottom - b.h).max(region.y));
398 self.translated(nx - b.x, ny - b.y)
399 }
400 }
401 }
402
403 pub fn resize_grab(&self, p: Point, tolerance: i32) -> Option<ResizeHandle> {
407 let tolerance = tolerance.max(1);
408 match *self {
409 Self::Circle { cx, cy, r } => {
410 let dist = f64::from(p.x - cx).hypot(f64::from(p.y - cy));
411 let on_rim = (dist - f64::from(r)).abs() <= f64::from(tolerance);
412 on_rim.then_some(ResizeHandle::CircleRadius)
413 }
414 Self::Rect(rect) => box_border_grab(rect, p, tolerance),
417 Self::Ellipse { .. } | Self::Triangle { .. } | Self::Poly { .. } => {
418 box_border_grab(self.bbox(), p, tolerance)
419 }
420 }
421 }
422
423 #[must_use]
434 pub fn resize_to(
435 &self,
436 handle: ResizeHandle,
437 cursor: Point,
438 region: Rect,
439 keep_aspect: bool,
440 ) -> Self {
441 let clamped = Point::new(
442 cursor.x.clamp(region.x, region.x + region.w - 1),
443 cursor.y.clamp(region.y, region.y + region.h - 1),
444 );
445 self.resize_to_local(handle, clamped, region, keep_aspect)
446 }
447
448 #[must_use]
452 fn resize_to_local(
453 &self,
454 handle: ResizeHandle,
455 clamped: Point,
456 region: Rect,
457 keep_aspect: bool,
458 ) -> Self {
459 const MIN: i32 = 2;
460 match (self.clone(), handle) {
461 (Self::Circle { cx, cy, .. }, ResizeHandle::CircleRadius) => {
462 let r = f64::from(clamped.x - cx).hypot(f64::from(clamped.y - cy)) as i32;
463 Self::Circle {
464 cx,
465 cy,
466 r: r.max(MIN),
467 }
468 }
469 (
470 Self::Rect(rect),
471 ResizeHandle::RectEdges {
472 left,
473 right,
474 top,
475 bottom,
476 },
477 ) => Self::Rect(resize_box(
478 rect,
479 (left, right, top, bottom),
480 clamped,
481 region,
482 keep_aspect,
483 )),
484 (
485 ell @ Self::Ellipse { .. },
486 ResizeHandle::RectEdges {
487 left,
488 right,
489 top,
490 bottom,
491 },
492 ) => {
493 let bb = resize_box(
496 ell.bbox(),
497 (left, right, top, bottom),
498 clamped,
499 region,
500 keep_aspect,
501 );
502 ellipse_in_box(bb, false)
503 }
504 (
505 poly @ Self::Poly { .. },
506 ResizeHandle::RectEdges {
507 left,
508 right,
509 top,
510 bottom,
511 },
512 ) => {
513 let old = poly.bbox();
514 let new = resize_box(
515 old,
516 (left, right, top, bottom),
517 clamped,
518 region,
519 keep_aspect,
520 );
521 scale_into_box(&poly, old, new)
522 }
523 (
524 tri @ Self::Triangle { .. },
525 ResizeHandle::RectEdges {
526 left,
527 right,
528 top,
529 bottom,
530 },
531 ) => {
532 let old = tri.bbox();
533 let new = resize_box(
534 old,
535 (left, right, top, bottom),
536 clamped,
537 region,
538 keep_aspect,
539 );
540 tri.mapped_between_boxes(old, new)
541 }
542 (shape, _) => shape,
545 }
546 }
547
548 #[must_use]
551 fn mapped_between_boxes(&self, old: Rect, new: Rect) -> Self {
552 let map_x = |v: i32| {
553 new.x
554 + (f64::from(v - old.x) * f64::from(new.w) / f64::from(old.w.max(1))).round() as i32
555 };
556 let map_y = |v: i32| {
557 new.y
558 + (f64::from(v - old.y) * f64::from(new.h) / f64::from(old.h.max(1))).round() as i32
559 };
560 match self.clone() {
561 Self::Triangle {
562 ax,
563 ay,
564 bx,
565 by,
566 cx,
567 cy,
568 } => Self::Triangle {
569 ax: map_x(ax),
570 ay: map_y(ay),
571 bx: map_x(bx),
572 by: map_y(by),
573 cx: map_x(cx),
574 cy: map_y(cy),
575 },
576 other => other,
577 }
578 }
579
580 #[must_use]
583 pub fn translated(&self, dx: i32, dy: i32) -> Self {
584 match *self {
585 Self::Poly { ref points } => Self::Poly {
586 points: points
587 .iter()
588 .map(|p| Point::new(p.x + dx, p.y + dy))
589 .collect(),
590 },
591 Self::Rect(r) => Self::Rect(Rect::new(r.x + dx, r.y + dy, r.w, r.h)),
592 Self::Circle { cx, cy, r } => Self::Circle {
593 cx: cx + dx,
594 cy: cy + dy,
595 r,
596 },
597 Self::Ellipse { cx, cy, rx, ry } => Self::Ellipse {
598 cx: cx + dx,
599 cy: cy + dy,
600 rx,
601 ry,
602 },
603 Self::Triangle {
604 ax,
605 ay,
606 bx,
607 by,
608 cx,
609 cy,
610 } => Self::Triangle {
611 ax: ax + dx,
612 ay: ay + dy,
613 bx: bx + dx,
614 by: by + dy,
615 cx: cx + dx,
616 cy: cy + dy,
617 },
618 }
619 }
620}
621
622pub fn normalize_deg(deg: i32) -> i32 {
624 deg.rem_euclid(360)
625}
626
627fn scale_into_box(shape: &Shape, old: Rect, new: Rect) -> Shape {
630 let map_x = |v: i32| {
631 new.x + (f64::from(v - old.x) * f64::from(new.w) / f64::from(old.w.max(1))).round() as i32
632 };
633 let map_y = |v: i32| {
634 new.y + (f64::from(v - old.y) * f64::from(new.h) / f64::from(old.h.max(1))).round() as i32
635 };
636 match shape {
637 Shape::Poly { points } => Shape::Poly {
638 points: points
639 .iter()
640 .map(|p| Point::new(map_x(p.x), map_y(p.y)))
641 .collect(),
642 },
643 other => other.clone(),
644 }
645}
646
647fn point_in_poly(points: &[Point], p: Point) -> bool {
650 if points.len() < 3 {
651 return false;
652 }
653 let n = points.len();
654 let mut inside = false;
655 for i in 0..n {
656 let a = points[i];
657 let b = points[(i + 1) % n];
658 if on_segment(a, b, p) {
659 return true;
660 }
661 if (a.y > p.y) != (b.y > p.y) {
663 let cross = i64::from(b.x - a.x) * i64::from(p.y - a.y)
664 - i64::from(b.y - a.y) * i64::from(p.x - a.x);
665 let crosses = if b.y > a.y { cross > 0 } else { cross < 0 };
666 if crosses {
667 inside = !inside;
668 }
669 }
670 }
671 inside
672}
673
674fn on_segment(a: Point, b: Point, p: Point) -> bool {
676 let cross =
677 i64::from(b.x - a.x) * i64::from(p.y - a.y) - i64::from(b.y - a.y) * i64::from(p.x - a.x);
678 cross == 0
679 && p.x >= a.x.min(b.x)
680 && p.x <= a.x.max(b.x)
681 && p.y >= a.y.min(b.y)
682 && p.y <= a.y.max(b.y)
683}
684
685fn poly_interior_point(points: &[Point]) -> Point {
690 if points.is_empty() {
691 return Point::new(0, 0);
692 }
693 let n = points.len() as i64;
694 let sx: i64 = points.iter().map(|p| i64::from(p.x)).sum();
695 let sy: i64 = points.iter().map(|p| i64::from(p.y)).sum();
696 let mean = Point::new((sx / n) as i32, (sy / n) as i32);
697 if point_in_poly(points, mean) {
698 return mean;
699 }
700 let shape = Shape::Poly {
701 points: points.to_vec(),
702 };
703 let bb = shape.bbox();
704 for y in bb.y..=bb.y.saturating_add(bb.h) {
705 for x in bb.x..=bb.x.saturating_add(bb.w) {
706 if point_in_poly(points, Point::new(x, y)) {
707 return Point::new(x, y);
708 }
709 }
710 }
711 mean
712}
713
714pub fn regular_polygon(center: Point, toward: Point, sides: u32) -> Shape {
717 let sides = sides.clamp(3, 12) as usize;
718 let r = f64::from(toward.x - center.x).hypot(f64::from(toward.y - center.y));
719 let base = f64::from(toward.y - center.y).atan2(f64::from(toward.x - center.x));
720 let step = std::f64::consts::TAU / sides as f64;
721 let points = (0..sides)
722 .map(|i| {
723 let a = base + step * i as f64;
724 Point::new(
725 f64::from(center.x).mul_add(1.0, r * a.cos()).round() as i32,
726 f64::from(center.y).mul_add(1.0, r * a.sin()).round() as i32,
727 )
728 })
729 .collect();
730 Shape::Poly { points }
731}
732
733pub fn simplify_path(points: &[Point], epsilon: f64) -> Vec<Point> {
737 if points.len() <= 2 {
738 return points.to_vec();
739 }
740 let mut keep = vec![false; points.len()];
741 keep[0] = true;
742 keep[points.len() - 1] = true;
743 let mut stack = vec![(0usize, points.len() - 1)];
744 while let Some((start, end)) = stack.pop() {
745 if end <= start + 1 {
746 continue;
747 }
748 let (mut worst, mut worst_dist) = (start, -1.0f64);
749 for (i, p) in points.iter().enumerate().take(end).skip(start + 1) {
750 let d = point_segment_distance(*p, points[start], points[end]);
751 if d > worst_dist {
752 worst = i;
753 worst_dist = d;
754 }
755 }
756 if worst_dist > epsilon {
757 keep[worst] = true;
758 stack.push((start, worst));
759 stack.push((worst, end));
760 }
761 }
762 points
763 .iter()
764 .zip(&keep)
765 .filter(|(_, k)| **k)
766 .map(|(p, _)| *p)
767 .collect()
768}
769
770fn point_segment_distance(p: Point, a: Point, b: Point) -> f64 {
772 let (px, py) = (f64::from(p.x), f64::from(p.y));
773 let (ax, ay) = (f64::from(a.x), f64::from(a.y));
774 let (bx, by) = (f64::from(b.x), f64::from(b.y));
775 let (dx, dy) = (bx - ax, by - ay);
776 let len2 = dx * dx + dy * dy;
777 if len2 <= f64::EPSILON {
778 return (px - ax).hypot(py - ay);
779 }
780 let t = ((px - ax) * dx + (py - ay) * dy) / len2;
781 let t = t.clamp(0.0, 1.0);
782 (px - (ax + t * dx)).hypot(py - (ay + t * dy))
783}
784
785fn ellipse_in_box(bbox: Rect, lock: bool) -> Shape {
788 let cx = bbox.x + bbox.w / 2;
789 let cy = bbox.y + bbox.h / 2;
790 let (rx, ry) = (bbox.w / 2, bbox.h / 2);
791 if lock {
792 let r = rx.min(ry).max(1);
793 return Shape::Ellipse {
794 cx,
795 cy,
796 rx: r,
797 ry: r,
798 };
799 }
800 Shape::Ellipse {
801 cx,
802 cy,
803 rx: rx.max(1),
804 ry: ry.max(1),
805 }
806}
807
808pub fn rotate_point_about(p: Point, center: Point, deg: i32) -> Point {
811 let rad = f64::from(deg).to_radians();
812 let (sin, cos) = rad.sin_cos();
813 let dx = f64::from(p.x) - f64::from(center.x);
816 let dy = f64::from(p.y) - f64::from(center.y);
817 Point::new(
818 (f64::from(center.x) + (dx * cos - dy * sin).round()) as i32,
819 (f64::from(center.y) + (dx * sin + dy * cos).round()) as i32,
820 )
821}
822
823impl Shape {
824 pub fn pivot(&self) -> Point {
826 let b = self.bbox();
827 Point::new(b.x.saturating_add(b.w / 2), b.y.saturating_add(b.h / 2))
828 }
829
830 pub fn rotated_bbox(&self, deg: i32) -> Rect {
832 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
833 return self.bbox();
834 }
835 let b = self.bbox();
836 let pivot = self.pivot();
837 let (bx1, by1) = (b.x.saturating_add(b.w), b.y.saturating_add(b.h));
838 let corners = [
839 Point::new(b.x, b.y),
840 Point::new(bx1, b.y),
841 Point::new(b.x, by1),
842 Point::new(bx1, by1),
843 ]
844 .map(|c| rotate_point_about(c, pivot, deg));
845 let x0 = corners.iter().map(|c| c.x).min().unwrap_or(b.x);
846 let y0 = corners.iter().map(|c| c.y).min().unwrap_or(b.y);
847 let x1 = corners.iter().map(|c| c.x).max().unwrap_or(bx1);
848 let y1 = corners.iter().map(|c| c.y).max().unwrap_or(by1);
849 Rect::new(x0, y0, x1.saturating_sub(x0), y1.saturating_sub(y0))
850 }
851
852 pub fn hit_test_rotated(&self, deg: i32, p: Point) -> bool {
855 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
856 return self.hit_test(p);
857 }
858 self.hit_test(rotate_point_about(p, self.pivot(), -deg))
859 }
860
861 pub fn resize_grab_rotated(&self, deg: i32, p: Point, tolerance: i32) -> Option<ResizeHandle> {
864 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
865 return self.resize_grab(p, tolerance);
866 }
867 self.resize_grab(rotate_point_about(p, self.pivot(), -deg), tolerance)
868 }
869
870 #[must_use]
872 pub fn resize_to_rotated(
873 &self,
874 deg: i32,
875 handle: ResizeHandle,
876 cursor: Point,
877 region: Rect,
878 keep_aspect: bool,
879 ) -> Self {
880 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
881 return self.resize_to(handle, cursor, region, keep_aspect);
882 }
883 let visual = Point::new(
887 cursor.x.clamp(region.x, region.x + region.w - 1),
888 cursor.y.clamp(region.y, region.y + region.h - 1),
889 );
890 let local = rotate_point_about(visual, self.pivot(), -deg);
891 self.resize_to_local(handle, local, region, keep_aspect)
892 }
893
894 #[must_use]
896 pub fn clamp_move_rotated(
897 &self,
898 deg: i32,
899 grab_offset: Point,
900 cursor: Point,
901 region: Rect,
902 ) -> Self {
903 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
904 return self.clamp_move(grab_offset, cursor, region);
905 }
906 let bb = self.rotated_bbox(deg);
907 let right = region.x + region.w;
908 let bottom = region.y + region.h;
909 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - bb.w).max(region.x));
910 let ny = (cursor.y - grab_offset.y).clamp(region.y, (bottom - bb.h).max(region.y));
911 self.translated(nx - bb.x, ny - bb.y)
912 }
913
914 pub fn grab_origin_rotated(&self, deg: i32) -> Point {
916 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
917 return self.grab_origin();
918 }
919 let bb = self.rotated_bbox(deg);
920 Point::new(bb.x, bb.y)
921 }
922
923 #[must_use]
927 pub fn with_rotation_baked(&self, deg: i32) -> Self {
928 if let Self::Poly { points } = self {
929 if normalize_deg(deg) == 0 {
930 return self.clone();
931 }
932 let pivot = self.pivot();
933 return Self::Poly {
934 points: points
935 .iter()
936 .map(|p| rotate_point_about(*p, pivot, deg))
937 .collect(),
938 };
939 }
940 match self.clone() {
941 Self::Triangle {
942 ax,
943 ay,
944 bx,
945 by,
946 cx,
947 cy,
948 } if normalize_deg(deg) != 0 => {
949 let pivot = self.pivot();
950 let a = rotate_point_about(Point::new(ax, ay), pivot, deg);
951 let b = rotate_point_about(Point::new(bx, by), pivot, deg);
952 let c = rotate_point_about(Point::new(cx, cy), pivot, deg);
953 Self::Triangle {
954 ax: a.x,
955 ay: a.y,
956 bx: b.x,
957 by: b.y,
958 cx: c.x,
959 cy: c.y,
960 }
961 }
962 other => other,
963 }
964 }
965}
966
967const fn triangle_in_box(bbox: Rect) -> Shape {
969 Shape::Triangle {
970 ax: bbox.x + bbox.w / 2,
971 ay: bbox.y,
972 bx: bbox.x,
973 by: bbox.y + bbox.h,
974 cx: bbox.x + bbox.w,
975 cy: bbox.y + bbox.h,
976 }
977}
978
979const fn cross(px: i32, py: i32, ax: i32, ay: i32, bx: i32, by: i32) -> i64 {
982 let abx = (bx - ax) as i64;
983 let aby = (by - ay) as i64;
984 let apx = (px - ax) as i64;
985 let apy = (py - ay) as i64;
986 abx * apy - aby * apx
987}
988
989const fn min3(a: i32, b: i32, c: i32) -> i32 {
990 if a <= b && a <= c {
991 return a;
992 }
993 if b <= c {
994 return b;
995 }
996 c
997}
998
999const fn max3(a: i32, b: i32, c: i32) -> i32 {
1000 if a >= b && a >= c {
1001 return a;
1002 }
1003 if b >= c {
1004 return b;
1005 }
1006 c
1007}
1008
1009fn box_border_grab(rect: Rect, p: Point, tolerance: i32) -> Option<ResizeHandle> {
1012 let (x1, y1) = (rect.x + rect.w, rect.y + rect.h);
1013 let within_x = p.x >= rect.x - tolerance && p.x <= x1 + tolerance;
1014 let within_y = p.y >= rect.y - tolerance && p.y <= y1 + tolerance;
1015 let left_d = (p.x - rect.x).abs();
1016 let right_d = (p.x - x1).abs();
1017 let top_d = (p.y - rect.y).abs();
1018 let bottom_d = (p.y - y1).abs();
1019 let mut left = left_d <= tolerance && within_y;
1020 let mut right = right_d <= tolerance && within_y;
1021 let mut top = top_d <= tolerance && within_x;
1022 let mut bottom = bottom_d <= tolerance && within_x;
1023 if left && right {
1026 right = right_d < left_d;
1027 left = !right;
1028 }
1029 if top && bottom {
1030 bottom = bottom_d < top_d;
1031 top = !bottom;
1032 }
1033 let grabbed = left || right || top || bottom;
1034 grabbed.then_some(ResizeHandle::RectEdges {
1035 left,
1036 right,
1037 top,
1038 bottom,
1039 })
1040}
1041
1042fn resize_box(
1046 rect: Rect,
1047 (left, right, top, bottom): (bool, bool, bool, bool),
1048 clamped: Point,
1049 region: Rect,
1050 keep_aspect: bool,
1051) -> Rect {
1052 const MIN: i32 = 2;
1053 let mut x0 = rect.x;
1054 let mut x1 = rect.x + rect.w;
1055 let mut y0 = rect.y;
1056 let mut y1 = rect.y + rect.h;
1057 if left {
1058 x0 = clamped.x.min(x1 - MIN);
1059 }
1060 if right {
1061 x1 = clamped.x.max(x0 + MIN);
1062 }
1063 if top {
1064 y0 = clamped.y.min(y1 - MIN);
1065 }
1066 if bottom {
1067 y1 = clamped.y.max(y0 + MIN);
1068 }
1069 if keep_aspect && rect.w >= MIN && rect.h >= MIN {
1070 let (w0, h0) = (f64::from(rect.w), f64::from(rect.h));
1071 match (left || right, top || bottom) {
1074 (true, true) => {
1075 let mut s = (f64::from(x1 - x0) / w0).max(f64::from(y1 - y0) / h0);
1078 let region_right = region.x + region.w;
1079 let region_bottom = region.y + region.h;
1080 let avail_w = if left {
1081 x1 - region.x
1082 } else {
1083 region_right - x0
1084 };
1085 let avail_h = if top {
1086 y1 - region.y
1087 } else {
1088 region_bottom - y0
1089 };
1090 s = s.min(f64::from(avail_w) / w0).min(f64::from(avail_h) / h0);
1091 let w = ((w0 * s).round() as i32).max(MIN);
1092 let h = ((h0 * s).round() as i32).max(MIN);
1093 (x0, x1) = if left { (x1 - w, x1) } else { (x0, x0 + w) };
1094 (y0, y1) = if top { (y1 - h, y1) } else { (y0, y0 + h) };
1095 }
1096 (true, false) => {
1097 let h = ((f64::from(x1 - x0) * h0 / w0).round() as i32)
1100 .max(MIN)
1101 .min(region.h);
1102 let center_y = rect.y + rect.h / 2;
1103 y0 = (center_y - h / 2).clamp(region.y, region.y + region.h - h);
1104 y1 = y0 + h;
1105 }
1106 (false, _) => {
1107 let w = ((f64::from(y1 - y0) * w0 / h0).round() as i32)
1108 .max(MIN)
1109 .min(region.w);
1110 let center_x = rect.x + rect.w / 2;
1111 x0 = (center_x - w / 2).clamp(region.x, region.x + region.w - w);
1112 x1 = x0 + w;
1113 }
1114 }
1115 }
1116 let (w, h) = (x1 - x0, y1 - y0);
1117 if rect.w >= MIN && rect.h >= MIN {
1118 return Rect::new(x0, y0, w, h);
1123 }
1124 let x0 = x0.clamp(region.x, (region.x + region.w - w).max(region.x));
1127 let y0 = y0.clamp(region.y, (region.y + region.h - h).max(region.y));
1128 Rect::new(x0, y0, w, h)
1129}
1130
1131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1138pub struct Line {
1139 pub a: Point,
1140 pub b: Point,
1141}
1142
1143impl Line {
1144 #[must_use]
1145 pub const fn new(a: Point, b: Point) -> Self {
1146 Self { a, b }
1147 }
1148
1149 #[must_use]
1151 pub const fn delta(self) -> (i32, i32) {
1152 (self.b.x - self.a.x, self.b.y - self.a.y)
1153 }
1154
1155 #[must_use]
1157 pub fn length(self) -> f64 {
1158 let (dx, dy) = self.delta();
1159 f64::from(dx).hypot(f64::from(dy))
1160 }
1161
1162 #[must_use]
1172 pub fn angle_deg(self) -> f64 {
1173 let (dx, dy) = self.delta();
1174 if dx == 0 && dy == 0 {
1175 return 0.0;
1176 }
1177 let deg = f64::from(dy).atan2(f64::from(dx)).to_degrees();
1178 if deg < 0.0 { deg + 360.0 } else { deg }
1179 }
1180
1181 #[must_use]
1184 pub fn bbox(self) -> Rect {
1185 let x = self.a.x.min(self.b.x);
1186 let y = self.a.y.min(self.b.y);
1187 Rect::new(
1188 x,
1189 y,
1190 (self.a.x - self.b.x).abs(),
1191 (self.a.y - self.b.y).abs(),
1192 )
1193 }
1194
1195 #[must_use]
1196 pub const fn translated(self, dx: i32, dy: i32) -> Self {
1197 Self::new(
1198 Point::new(self.a.x + dx, self.a.y + dy),
1199 Point::new(self.b.x + dx, self.b.y + dy),
1200 )
1201 }
1202
1203 #[must_use]
1209 pub fn endpoint_grab(self, p: Point, tolerance: i32) -> Option<bool> {
1210 let near = |q: Point| {
1211 let (dx, dy) = (i64::from(p.x - q.x), i64::from(p.y - q.y));
1212 dx * dx + dy * dy <= i64::from(tolerance) * i64::from(tolerance)
1213 };
1214 if near(self.a) {
1215 return Some(true);
1216 }
1217 near(self.b).then_some(false)
1218 }
1219
1220 #[must_use]
1223 pub fn hit_test(self, p: Point, tolerance: i32) -> bool {
1224 self.distance_to(p) <= f64::from(tolerance)
1225 }
1226
1227 #[must_use]
1231 pub fn distance_to(self, p: Point) -> f64 {
1232 let (dx, dy) = self.delta();
1233 let (dx, dy) = (f64::from(dx), f64::from(dy));
1234 let len_sq = dx.mul_add(dx, dy * dy);
1235 let (px, py) = (f64::from(p.x - self.a.x), f64::from(p.y - self.a.y));
1236 if len_sq <= f64::EPSILON {
1237 return px.hypot(py);
1238 }
1239 let t = px.mul_add(dx, py * dy) / len_sq;
1240 let t = t.clamp(0.0, 1.0);
1241 (px - t * dx).hypot(py - t * dy)
1242 }
1243
1244 #[must_use]
1252 pub fn constrained(self) -> Self {
1253 const AXES: [(f64, f64); 8] = [
1255 (1.0, 0.0),
1256 (-1.0, 0.0),
1257 (0.0, 1.0),
1258 (0.0, -1.0),
1259 (
1260 std::f64::consts::FRAC_1_SQRT_2,
1261 std::f64::consts::FRAC_1_SQRT_2,
1262 ),
1263 (
1264 std::f64::consts::FRAC_1_SQRT_2,
1265 -std::f64::consts::FRAC_1_SQRT_2,
1266 ),
1267 (
1268 -std::f64::consts::FRAC_1_SQRT_2,
1269 std::f64::consts::FRAC_1_SQRT_2,
1270 ),
1271 (
1272 -std::f64::consts::FRAC_1_SQRT_2,
1273 -std::f64::consts::FRAC_1_SQRT_2,
1274 ),
1275 ];
1276 let (dx, dy) = self.delta();
1277 if dx == 0 && dy == 0 {
1278 return self;
1279 }
1280 let (fx, fy) = (f64::from(dx), f64::from(dy));
1281 let mut best = (f64::NEG_INFINITY, 0.0, 0.0);
1282 for (ax, ay) in AXES {
1283 let projection = fx.mul_add(ax, fy * ay);
1284 if projection > best.0 {
1285 best = (projection, ax, ay);
1286 }
1287 }
1288 let (projection, ax, ay) = best;
1289 let projection = projection.max(0.0);
1290 Self::new(
1291 self.a,
1292 Point::new(
1293 self.a.x + (projection * ax).round() as i32,
1294 self.a.y + (projection * ay).round() as i32,
1295 ),
1296 )
1297 }
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302 use super::*;
1303
1304 #[test]
1305 fn length_and_delta_are_the_plain_arithmetic() {
1306 let line = Line::new(Point::new(10, 20), Point::new(40, 60));
1307 assert_eq!(line.delta(), (30, 40));
1308 assert!((line.length() - 50.0).abs() < 1e-9, "3-4-5 triangle");
1309 }
1310
1311 #[test]
1312 fn length_is_invariant_under_translation() {
1313 let line = Line::new(Point::new(-5, 7), Point::new(11, -3));
1314 let moved = line.translated(1000, -400);
1315 assert!((line.length() - moved.length()).abs() < 1e-9);
1316 assert_eq!(line.delta(), moved.delta());
1317 }
1318
1319 #[test]
1320 fn angle_is_clockwise_from_positive_x() {
1321 let at = |dx, dy| Line::new(Point::new(0, 0), Point::new(dx, dy)).angle_deg();
1323 assert!((at(10, 0) - 0.0).abs() < 1e-9, "right");
1324 assert!((at(0, 10) - 90.0).abs() < 1e-9, "down");
1325 assert!((at(-10, 0) - 180.0).abs() < 1e-9, "left");
1326 assert!((at(0, -10) - 270.0).abs() < 1e-9, "up");
1327 assert!((at(10, 10) - 45.0).abs() < 1e-9, "down-right");
1328 }
1329
1330 #[test]
1331 fn angle_is_antisymmetric_under_endpoint_swap() {
1332 for (ax, ay, bx, by) in [
1334 (0, 0, 10, 0),
1335 (3, 7, -11, 2),
1336 (-5, -5, 5, 5),
1337 (100, -20, 100, 40),
1338 ] {
1339 let ab = Line::new(Point::new(ax, ay), Point::new(bx, by)).angle_deg();
1340 let ba = Line::new(Point::new(bx, by), Point::new(ax, ay)).angle_deg();
1341 let expected = (ba + 180.0) % 360.0;
1342 assert!((ab - expected).abs() < 1e-9, "{ab} vs {expected}");
1343 }
1344 }
1345
1346 #[test]
1347 fn a_zero_length_measure_has_no_direction_rather_than_nan() {
1348 let dot = Line::new(Point::new(4, 4), Point::new(4, 4));
1349 assert!((dot.length() - 0.0).abs() < f64::EPSILON);
1350 assert!(dot.angle_deg().is_finite(), "atan2(0,0) must not escape");
1351 assert!((dot.angle_deg() - 0.0).abs() < f64::EPSILON);
1352 assert!(dot.hit_test(Point::new(4, 4), 6));
1354 }
1355
1356 #[test]
1357 fn distance_clamps_at_the_ends_rather_than_using_the_infinite_line() {
1358 let line = Line::new(Point::new(0, 0), Point::new(100, 0));
1359 assert!((line.distance_to(Point::new(50, 10)) - 10.0).abs() < 1e-9);
1361 assert!((line.distance_to(Point::new(200, 0)) - 100.0).abs() < 1e-9);
1364 assert!((line.distance_to(Point::new(-30, 40)) - 50.0).abs() < 1e-9);
1365 }
1366
1367 #[test]
1368 fn grabbing_prefers_an_endpoint_then_the_segment() {
1369 let line = Line::new(Point::new(0, 0), Point::new(100, 0));
1370 assert_eq!(line.endpoint_grab(Point::new(2, 2), 6), Some(true), "a");
1371 assert_eq!(line.endpoint_grab(Point::new(98, 1), 6), Some(false), "b");
1372 assert_eq!(line.endpoint_grab(Point::new(50, 0), 6), None, "middle");
1373 assert!(line.hit_test(Point::new(50, 3), 6), "still on the line");
1374 assert!(!line.hit_test(Point::new(50, 40), 6));
1375 }
1376
1377 #[test]
1378 fn shift_snaps_to_the_eight_directions_and_tracks_the_pointer() {
1379 let from = Point::new(100, 100);
1380 let nearly = Line::new(from, Point::new(200, 104)).constrained();
1382 assert_eq!(nearly.b.y, 100, "snapped to horizontal");
1383 assert!((nearly.length() - 100.0).abs() < 1.0, "reach preserved");
1384
1385 let diag = Line::new(from, Point::new(160, 172)).constrained();
1387 assert!(
1388 ((diag.b.x - from.x) - (diag.b.y - from.y)).abs() <= 1,
1389 "equal legs: {diag:?}"
1390 );
1391 assert!((diag.angle_deg() - 45.0).abs() < 1.0);
1392
1393 let up_left = Line::new(from, Point::new(30, 26)).constrained();
1395 assert!((up_left.angle_deg() - 225.0).abs() < 1.0, "{up_left:?}");
1396 }
1397
1398 #[test]
1399 fn constraining_a_zero_length_measure_leaves_it_alone() {
1400 let dot = Line::new(Point::new(9, 9), Point::new(9, 9));
1401 assert_eq!(dot.constrained(), dot);
1402 }
1403
1404 const BOUNDS: Size = Size::new(1920, 1080);
1405 const BOUNDS_RECT: Rect = Rect::new(0, 0, BOUNDS.w, BOUNDS.h);
1406
1407 #[test]
1408 fn rect_preview_normalizes_inverted_drag() {
1409 let s = Shape::compute_preview(
1410 ToolKind::Rect,
1411 Point::new(100, 200),
1412 Point::new(40, 50),
1413 BOUNDS_RECT,
1414 false,
1415 );
1416 assert_eq!(s, Some(Shape::Rect(Rect::new(40, 50, 60, 150))));
1417 }
1418
1419 #[test]
1420 fn rect_preview_clamps_cursor_to_bounds() {
1421 let s = Shape::compute_preview(
1422 ToolKind::Rect,
1423 Point::new(1900, 1000),
1424 Point::new(5000, 5000),
1425 BOUNDS_RECT,
1426 false,
1427 );
1428 assert_eq!(s, Some(Shape::Rect(Rect::new(1900, 1000, 19, 79))));
1429 }
1430
1431 #[test]
1432 fn rect_preview_degenerate_is_none() {
1433 assert_eq!(
1434 Shape::compute_preview(
1435 ToolKind::Rect,
1436 Point::new(10, 10),
1437 Point::new(10, 300),
1438 BOUNDS_RECT,
1439 false
1440 ),
1441 None
1442 );
1443 assert_eq!(
1444 Shape::compute_preview(
1445 ToolKind::Rect,
1446 Point::new(10, 10),
1447 Point::new(10, 10),
1448 BOUNDS_RECT,
1449 false
1450 ),
1451 None
1452 );
1453 }
1454
1455 #[test]
1456 fn circle_preview_radius_is_distance() {
1457 let s = Shape::compute_preview(
1458 ToolKind::Circle,
1459 Point::new(100, 100),
1460 Point::new(103, 104),
1461 BOUNDS_RECT,
1462 false,
1463 );
1464 assert_eq!(
1465 s,
1466 Some(Shape::Circle {
1467 cx: 100,
1468 cy: 100,
1469 r: 5
1470 })
1471 );
1472 }
1473
1474 #[test]
1475 fn circle_preview_zero_radius_is_none() {
1476 assert_eq!(
1477 Shape::compute_preview(
1478 ToolKind::Circle,
1479 Point::new(7, 7),
1480 Point::new(7, 7),
1481 BOUNDS_RECT,
1482 false
1483 ),
1484 None
1485 );
1486 }
1487
1488 #[test]
1489 fn rect_hit_test_edges() {
1490 let s = Shape::Rect(Rect::new(10, 10, 20, 20));
1491 assert!(s.hit_test(Point::new(10, 10)));
1492 assert!(s.hit_test(Point::new(29, 29)));
1493 assert!(!s.hit_test(Point::new(30, 30)));
1494 assert!(!s.hit_test(Point::new(9, 10)));
1495 }
1496
1497 #[test]
1498 fn circle_hit_test_boundary_inclusive() {
1499 let s = Shape::Circle {
1500 cx: 0,
1501 cy: 0,
1502 r: 10,
1503 };
1504 assert!(s.hit_test(Point::new(10, 0)));
1505 assert!(s.hit_test(Point::new(6, 8)));
1506 assert!(!s.hit_test(Point::new(8, 8)));
1507 }
1508
1509 #[test]
1510 fn circle_hit_test_survives_extreme_coords() {
1511 let s = Shape::Circle { cx: 0, cy: 0, r: 5 };
1512 assert!(!s.hit_test(Point::new(i32::MAX, i32::MAX)));
1513 }
1514
1515 #[test]
1516 fn bbox_of_circle() {
1517 let s = Shape::Circle {
1518 cx: 50,
1519 cy: 60,
1520 r: 10,
1521 };
1522 assert_eq!(s.bbox(), Rect::new(40, 50, 20, 20));
1523 }
1524
1525 #[test]
1526 fn rect_clamp_move_never_escapes_bounds() {
1527 let s = Shape::Rect(Rect::new(0, 0, 300, 200));
1528 let grab = Point::new(0, 0);
1529 for cx in [-500, 0, 960, 5000] {
1530 for cy in [-500, 0, 540, 5000] {
1531 let Shape::Rect(r) = s.clamp_move(grab, Point::new(cx, cy), BOUNDS_RECT) else {
1532 panic!("rect stayed rect");
1533 };
1534 assert!(r.x >= 0 && r.y >= 0, "({cx},{cy}) gave {r:?}");
1535 assert!(
1536 r.x + r.w <= BOUNDS.w && r.y + r.h <= BOUNDS.h,
1537 "({cx},{cy}) gave {r:?}"
1538 );
1539 }
1540 }
1541 }
1542
1543 #[test]
1544 fn circle_clamp_move_never_escapes_bounds() {
1545 let s = Shape::Circle {
1546 cx: 500,
1547 cy: 500,
1548 r: 40,
1549 };
1550 let grab = Point::new(0, 0);
1551 for cx in [-500, 0, 960, 5000] {
1552 for cy in [-500, 0, 540, 5000] {
1553 let Shape::Circle {
1554 cx: ncx,
1555 cy: ncy,
1556 r,
1557 } = s.clamp_move(grab, Point::new(cx, cy), BOUNDS_RECT)
1558 else {
1559 panic!("circle stayed circle");
1560 };
1561 assert!(
1562 ncx - r >= 0 && ncy - r >= 0,
1563 "({cx},{cy}) gave center ({ncx},{ncy})"
1564 );
1565 assert!(
1566 ncx + r <= BOUNDS.w && ncy + r <= BOUNDS.h,
1567 "({cx},{cy}) gave center ({ncx},{ncy})"
1568 );
1569 }
1570 }
1571 }
1572
1573 #[test]
1574 fn oversized_circle_clamp_is_stable() {
1575 let s = Shape::Circle {
1578 cx: 100,
1579 cy: 100,
1580 r: 2000,
1581 };
1582 let moved = s.clamp_move(Point::new(0, 0), Point::new(0, 0), BOUNDS_RECT);
1583 assert_eq!(
1584 moved,
1585 Shape::Circle {
1586 cx: 2000,
1587 cy: 2000,
1588 r: 2000
1589 }
1590 );
1591 }
1592
1593 #[test]
1594 fn translated_shifts_both_kinds() {
1595 assert_eq!(
1596 Shape::Rect(Rect::new(1, 2, 3, 4)).translated(10, 20),
1597 Shape::Rect(Rect::new(11, 22, 3, 4))
1598 );
1599 assert_eq!(
1600 Shape::Circle { cx: 1, cy: 2, r: 3 }.translated(10, 20),
1601 Shape::Circle {
1602 cx: 11,
1603 cy: 22,
1604 r: 3
1605 }
1606 );
1607 }
1608
1609 #[test]
1610 fn circle_rim_grab_within_tolerance_only() {
1611 let s = Shape::Circle {
1612 cx: 100,
1613 cy: 100,
1614 r: 50,
1615 };
1616 assert_eq!(
1617 s.resize_grab(Point::new(153, 100), 5),
1618 Some(ResizeHandle::CircleRadius)
1619 );
1620 assert_eq!(
1621 s.resize_grab(Point::new(147, 100), 5),
1622 Some(ResizeHandle::CircleRadius)
1623 );
1624 assert_eq!(s.resize_grab(Point::new(100, 100), 5), None); assert_eq!(s.resize_grab(Point::new(160, 100), 5), None); }
1627
1628 #[test]
1629 fn rect_edge_and_corner_grabs() {
1630 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1631 assert_eq!(
1632 s.resize_grab(Point::new(100, 150), 5),
1633 Some(ResizeHandle::RectEdges {
1634 left: true,
1635 right: false,
1636 top: false,
1637 bottom: false
1638 })
1639 );
1640 assert_eq!(
1641 s.resize_grab(Point::new(302, 150), 5), Some(ResizeHandle::RectEdges {
1643 left: false,
1644 right: true,
1645 top: false,
1646 bottom: false
1647 })
1648 );
1649 assert_eq!(
1650 s.resize_grab(Point::new(298, 202), 5), Some(ResizeHandle::RectEdges {
1652 left: false,
1653 right: true,
1654 top: false,
1655 bottom: true
1656 })
1657 );
1658 assert_eq!(s.resize_grab(Point::new(200, 150), 5), None); assert_eq!(s.resize_grab(Point::new(90, 150), 5), None); }
1661
1662 #[test]
1663 fn tiny_rect_grabs_nearer_edge_not_both() {
1664 let s = Shape::Rect(Rect::new(100, 100, 6, 6));
1665 let Some(ResizeHandle::RectEdges { left, right, .. }) =
1666 s.resize_grab(Point::new(101, 103), 5)
1667 else {
1668 panic!("expected an edge grab");
1669 };
1670 assert!(left && !right);
1671 }
1672
1673 #[test]
1674 fn circle_resize_follows_cursor_distance() {
1675 let s = Shape::Circle {
1676 cx: 100,
1677 cy: 100,
1678 r: 50,
1679 };
1680 let resized = s.resize_to(
1681 ResizeHandle::CircleRadius,
1682 Point::new(100, 180),
1683 BOUNDS_RECT,
1684 false,
1685 );
1686 assert_eq!(
1687 resized,
1688 Shape::Circle {
1689 cx: 100,
1690 cy: 100,
1691 r: 80
1692 }
1693 );
1694 let tiny = s.resize_to(
1696 ResizeHandle::CircleRadius,
1697 Point::new(100, 100),
1698 BOUNDS_RECT,
1699 false,
1700 );
1701 assert_eq!(
1702 tiny,
1703 Shape::Circle {
1704 cx: 100,
1705 cy: 100,
1706 r: 2
1707 }
1708 );
1709 }
1710
1711 #[test]
1712 fn rect_corner_resize_anchors_opposite_corner() {
1713 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1714 let handle = ResizeHandle::RectEdges {
1715 left: false,
1716 right: true,
1717 top: false,
1718 bottom: true,
1719 };
1720 let resized = s.resize_to(handle, Point::new(400, 300), BOUNDS_RECT, false);
1721 assert_eq!(resized, Shape::Rect(Rect::new(100, 100, 300, 200)));
1722 }
1723
1724 #[test]
1725 fn rect_edge_resize_moves_one_axis_only() {
1726 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1727 let handle = ResizeHandle::RectEdges {
1728 left: true,
1729 right: false,
1730 top: false,
1731 bottom: false,
1732 };
1733 let resized = s.resize_to(handle, Point::new(50, 999), BOUNDS_RECT, false);
1734 assert_eq!(resized, Shape::Rect(Rect::new(50, 100, 250, 100)));
1735 }
1736
1737 #[test]
1738 fn rect_resize_cannot_invert_or_vanish() {
1739 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1740 let handle = ResizeHandle::RectEdges {
1741 left: true,
1742 right: false,
1743 top: false,
1744 bottom: false,
1745 };
1746 let resized = s.resize_to(handle, Point::new(500, 150), BOUNDS_RECT, false);
1748 assert_eq!(resized, Shape::Rect(Rect::new(298, 100, 2, 100)));
1749 }
1750
1751 #[test]
1752 fn resize_cursor_is_clamped_to_bounds() {
1753 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1754 let handle = ResizeHandle::RectEdges {
1755 left: false,
1756 right: true,
1757 top: false,
1758 bottom: false,
1759 };
1760 let resized = s.resize_to(handle, Point::new(99_999, 150), BOUNDS_RECT, false);
1761 assert_eq!(
1762 resized,
1763 Shape::Rect(Rect::new(100, 100, BOUNDS.w - 1 - 100, 100))
1764 );
1765 }
1766
1767 #[test]
1768 fn locked_corner_resize_keeps_ratio_dominant_axis_wins() {
1769 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1772 let corner = ResizeHandle::RectEdges {
1773 left: false,
1774 right: true,
1775 top: false,
1776 bottom: true,
1777 };
1778 let resized = s.resize_to(corner, Point::new(400, 300), BOUNDS_RECT, true);
1779 assert_eq!(resized, Shape::Rect(Rect::new(100, 100, 400, 200)));
1780 }
1781
1782 #[test]
1783 fn locked_corner_resize_anchors_the_opposite_corner() {
1784 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1786 let corner = ResizeHandle::RectEdges {
1787 left: true,
1788 right: false,
1789 top: true,
1790 bottom: false,
1791 };
1792 let resized = s.resize_to(corner, Point::new(0, 80), BOUNDS_RECT, true);
1793 let Shape::Rect(r) = resized else {
1794 panic!("still a rect")
1795 };
1796 assert_eq!((r.x + r.w, r.y + r.h), (300, 200), "anchor moved");
1797 assert_eq!(r.w * 100, r.h * 200, "ratio drifted: {r:?}");
1798 }
1799
1800 #[test]
1801 fn locked_corner_resize_caps_scale_at_bounds() {
1802 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1806 let corner = ResizeHandle::RectEdges {
1807 left: false,
1808 right: true,
1809 top: false,
1810 bottom: true,
1811 };
1812 let resized = s.resize_to(
1813 corner,
1814 Point::new(BOUNDS_RECT.w - 1, BOUNDS_RECT.h - 1),
1815 BOUNDS_RECT,
1816 true,
1817 );
1818 let Shape::Rect(r) = resized else {
1819 panic!("still a rect")
1820 };
1821 assert!(
1822 r.x + r.w <= BOUNDS.w && r.y + r.h <= BOUNDS.h,
1823 "escaped: {r:?}"
1824 );
1825 assert_eq!(r.w, BOUNDS.w - 100);
1826 assert_eq!(r.w, 2 * r.h);
1827 }
1828
1829 #[test]
1830 fn locked_edge_resize_scales_other_axis_centered() {
1831 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1834 let edge = ResizeHandle::RectEdges {
1835 left: false,
1836 right: true,
1837 top: false,
1838 bottom: false,
1839 };
1840 let resized = s.resize_to(edge, Point::new(500, 150), BOUNDS_RECT, true);
1841 assert_eq!(resized, Shape::Rect(Rect::new(100, 50, 400, 200)));
1842 }
1843
1844 #[test]
1845 fn locked_edge_resize_clamps_centered_axis_to_bounds() {
1846 let s = Shape::Rect(Rect::new(100, 10, 200, 100));
1849 let edge = ResizeHandle::RectEdges {
1850 left: false,
1851 right: true,
1852 top: false,
1853 bottom: false,
1854 };
1855 let resized = s.resize_to(edge, Point::new(500, 60), BOUNDS_RECT, true);
1856 let Shape::Rect(r) = resized else {
1857 panic!("still a rect")
1858 };
1859 assert_eq!((r.w, r.h), (400, 200));
1860 assert_eq!(r.y, 0, "clamped to the top edge");
1861 }
1862
1863 #[test]
1864 fn locked_circle_resize_is_unchanged_by_lock() {
1865 let s = Shape::Circle {
1866 cx: 100,
1867 cy: 100,
1868 r: 50,
1869 };
1870 let unlocked = s.resize_to(
1871 ResizeHandle::CircleRadius,
1872 Point::new(100, 180),
1873 BOUNDS_RECT,
1874 false,
1875 );
1876 let locked = s.resize_to(
1877 ResizeHandle::CircleRadius,
1878 Point::new(100, 180),
1879 BOUNDS_RECT,
1880 true,
1881 );
1882 assert_eq!(unlocked, locked);
1883 }
1884
1885 #[test]
1886 fn mismatched_handle_is_inert() {
1887 let s = Shape::Circle { cx: 5, cy: 5, r: 5 };
1888 let handle = ResizeHandle::RectEdges {
1889 left: true,
1890 right: false,
1891 top: false,
1892 bottom: false,
1893 };
1894 assert_eq!(
1895 s.resize_to(handle, Point::new(50, 50), BOUNDS_RECT, false),
1896 s
1897 );
1898 }
1899
1900 #[test]
1901 fn ellipse_preview_inscribes_the_drag_box_and_shift_locks_a_circle() {
1902 let free = Shape::compute_preview(
1903 ToolKind::Ellipse,
1904 Point::new(10, 10),
1905 Point::new(50, 30),
1906 BOUNDS_RECT,
1907 false,
1908 );
1909 assert_eq!(
1910 free,
1911 Some(Shape::Ellipse {
1912 cx: 30,
1913 cy: 20,
1914 rx: 20,
1915 ry: 10,
1916 })
1917 );
1918 let locked = Shape::compute_preview(
1919 ToolKind::Ellipse,
1920 Point::new(10, 10),
1921 Point::new(50, 30),
1922 BOUNDS_RECT,
1923 true,
1924 );
1925 assert_eq!(
1926 locked,
1927 Some(Shape::Ellipse {
1928 cx: 30,
1929 cy: 20,
1930 rx: 10,
1931 ry: 10,
1932 }),
1933 "Shift inscribes the circle instead"
1934 );
1935 }
1936
1937 #[test]
1938 fn ellipse_hit_test_is_boundary_inclusive_and_excludes_bbox_corners() {
1939 let e = Shape::Ellipse {
1940 cx: 50,
1941 cy: 40,
1942 rx: 30,
1943 ry: 10,
1944 };
1945 assert!(e.hit_test(Point::new(50, 40)));
1946 assert!(e.hit_test(Point::new(80, 40)), "rx vertex inclusive");
1947 assert!(e.hit_test(Point::new(50, 30)), "ry vertex inclusive");
1948 assert!(!e.hit_test(Point::new(80, 30)), "bbox corner outside");
1949 assert!(!e.hit_test(Point::new(81, 40)));
1950 assert_eq!(e.bbox(), Rect::new(20, 30, 60, 20));
1951 }
1952
1953 #[test]
1954 fn ellipse_resize_rides_its_bounding_box() {
1955 let e = Shape::Ellipse {
1956 cx: 50,
1957 cy: 40,
1958 rx: 20,
1959 ry: 10,
1960 };
1961 let handle = e.resize_grab(Point::new(70, 40), 2).expect("edge grab");
1963 let resized = e.resize_to(handle, Point::new(90, 40), BOUNDS_RECT, false);
1964 assert_eq!(
1965 resized,
1966 Shape::Ellipse {
1967 cx: 60,
1968 cy: 40,
1969 rx: 30,
1970 ry: 10,
1971 },
1972 "left edge anchored, rx grew"
1973 );
1974 }
1975
1976 #[test]
1977 fn rotated_ellipse_hit_follows_the_turn() {
1978 let e = Shape::Ellipse {
1979 cx: 50,
1980 cy: 40,
1981 rx: 30,
1982 ry: 8,
1983 };
1984 assert!(e.hit_test_rotated(90, Point::new(50, 65)));
1986 assert!(!e.hit_test_rotated(90, Point::new(75, 40)));
1987 assert!(e.hit_test(Point::new(75, 40)), "unrotated it lies flat");
1988 }
1989
1990 #[test]
1991 fn point_in_poly_handles_concave_shapes_edges_included() {
1992 let u = vec![
1994 Point::new(0, 0),
1995 Point::new(10, 0),
1996 Point::new(10, 30),
1997 Point::new(20, 30),
1998 Point::new(20, 0),
1999 Point::new(30, 0),
2000 Point::new(30, 40),
2001 Point::new(0, 40),
2002 ];
2003 let shape = Shape::Poly { points: u };
2004 assert!(shape.hit_test(Point::new(5, 20)), "left arm");
2005 assert!(shape.hit_test(Point::new(25, 20)), "right arm");
2006 assert!(shape.hit_test(Point::new(15, 35)), "base");
2007 assert!(!shape.hit_test(Point::new(15, 10)), "the notch is outside");
2008 assert!(shape.hit_test(Point::new(0, 0)), "vertex inclusive");
2009 assert!(shape.hit_test(Point::new(5, 0)), "edge inclusive");
2010 assert!(!shape.hit_test(Point::new(-1, 20)));
2011 assert!(shape.hit_test(shape.click_point()));
2013 }
2014
2015 #[test]
2016 fn regular_polygon_puts_the_first_vertex_at_the_cursor() {
2017 let hex = regular_polygon(Point::new(100, 100), Point::new(140, 100), 6);
2018 let Shape::Poly { ref points } = hex else {
2019 panic!("regular polygon is a poly")
2020 };
2021 assert_eq!(points.len(), 6);
2022 assert_eq!(points[0], Point::new(140, 100), "first vertex at cursor");
2023 for p in points {
2024 let d = f64::from(p.x - 100).hypot(f64::from(p.y - 100));
2025 assert!((d - 40.0).abs() < 1.5, "vertex {p:?} off the radius: {d}");
2026 }
2027 let tri = regular_polygon(Point::new(0, 0), Point::new(10, 0), 1);
2029 let Shape::Poly { points } = tri else {
2030 panic!()
2031 };
2032 assert_eq!(points.len(), 3);
2033 }
2034
2035 #[test]
2036 fn simplify_path_drops_jitter_and_keeps_corners() {
2037 let path: Vec<Point> = (0..=20)
2040 .map(|x| Point::new(x * 5, i32::from(x % 2 != 0)))
2041 .chain((1..=10).map(|y| Point::new(100, y * 5)))
2042 .collect();
2043 let simplified = simplify_path(&path, 2.0);
2044 assert!(
2045 simplified.len() <= 5,
2046 "expected a handful of points, got {}",
2047 simplified.len()
2048 );
2049 assert_eq!(*simplified.first().unwrap(), Point::new(0, 0));
2050 assert_eq!(*simplified.last().unwrap(), Point::new(100, 50));
2051 assert!(
2052 simplified.contains(&Point::new(100, 1)) || simplified.contains(&Point::new(100, 0)),
2053 "the corner survives: {simplified:?}"
2054 );
2055 }
2056
2057 #[test]
2058 fn poly_moves_resizes_and_rotates_like_any_shape() {
2059 let square = Shape::Poly {
2060 points: vec![
2061 Point::new(10, 10),
2062 Point::new(30, 10),
2063 Point::new(30, 30),
2064 Point::new(10, 30),
2065 ],
2066 };
2067 assert_eq!(square.bbox(), Rect::new(10, 10, 20, 20));
2068 let moved = square.translated(5, -5);
2069 assert_eq!(moved.bbox(), Rect::new(15, 5, 20, 20));
2070 let handle = square.resize_grab(Point::new(30, 20), 2).expect("edge");
2072 let grown = square.resize_to(handle, Point::new(50, 20), BOUNDS_RECT, false);
2073 assert_eq!(grown.bbox(), Rect::new(10, 10, 40, 20));
2074 let turned = square.with_rotation_baked(90);
2076 assert_eq!(turned.bbox(), square.bbox(), "square is 90-symmetric");
2077 assert!(matches!(turned, Shape::Poly { .. }));
2078 }
2079
2080 #[test]
2081 fn click_point_centers_each_kind() {
2082 assert_eq!(
2083 Shape::Rect(Rect::new(10, 20, 30, 40)).click_point(),
2084 Point::new(25, 40)
2085 );
2086 assert_eq!(
2087 Shape::Circle { cx: 5, cy: 6, r: 7 }.click_point(),
2088 Point::new(5, 6)
2089 );
2090 let tri = Shape::Triangle {
2091 ax: 30,
2092 ay: 0,
2093 bx: 0,
2094 by: 60,
2095 cx: 60,
2096 cy: 60,
2097 };
2098 assert_eq!(tri.click_point(), Point::new(30, 40));
2099 assert!(tri.hit_test(tri.click_point()));
2100 let rect = Shape::Rect(Rect::new(10, 10, 40, 10));
2103 assert!(rect.hit_test_rotated(90, rect.click_point()));
2104 }
2105
2106 #[test]
2107 fn clamp_point_lands_inside_and_leaves_interior_points_alone() {
2108 let r = Rect::new(10, 20, 30, 40);
2109 let inside = Point::new(15, 25);
2110 assert_eq!(r.clamp_point(inside), inside);
2111 assert_eq!(r.clamp_point(Point::new(100, 100)), Point::new(39, 59));
2113 assert_eq!(r.clamp_point(Point::new(-5, -5)), Point::new(10, 20));
2114 for p in [
2115 Point::new(100, 100),
2116 Point::new(-5, -5),
2117 Point::new(15, 900),
2118 ] {
2119 assert!(r.contains(r.clamp_point(p)));
2120 }
2121 }
2122
2123 #[test]
2124 fn clamp_point_on_a_zero_sized_rect_gives_the_origin_corner() {
2125 let r = Rect::new(7, 9, 0, 0);
2126 assert_eq!(r.clamp_point(Point::new(100, 100)), Point::new(7, 9));
2127 }
2128
2129 #[test]
2130 fn line_bbox_spans_both_endpoints_in_any_direction() {
2131 let down = Line::new(Point::new(10, 20), Point::new(40, 60));
2132 let up = Line::new(Point::new(40, 60), Point::new(10, 20));
2133 assert_eq!(down.bbox(), Rect::new(10, 20, 30, 40));
2134 assert_eq!(up.bbox(), down.bbox());
2135 let dot = Line::new(Point::new(5, 5), Point::new(5, 5));
2137 assert_eq!(dot.bbox(), Rect::new(5, 5, 0, 0));
2138 }
2139
2140 #[test]
2141 fn tool_kind_cycles_through_the_drawing_tools() {
2142 assert_eq!(ToolKind::Rect.next(), ToolKind::Ellipse);
2143 assert_eq!(ToolKind::Ellipse.next(), ToolKind::Triangle);
2144 assert_eq!(ToolKind::Triangle.next(), ToolKind::Polygon);
2145 assert_eq!(ToolKind::Polygon.next(), ToolKind::Freehand);
2146 assert_eq!(ToolKind::Freehand.next(), ToolKind::Measure);
2147 assert_eq!(ToolKind::Measure.next(), ToolKind::Rect);
2148 assert_eq!(ToolKind::Circle.next(), ToolKind::Triangle);
2150 assert_eq!(ToolKind::Poly.next(), ToolKind::Rect);
2151 }
2152
2153 #[test]
2154 fn triangle_preview_is_apex_top_center_in_drag_box() {
2155 let s = Shape::compute_preview(
2156 ToolKind::Triangle,
2157 Point::new(100, 100),
2158 Point::new(300, 200),
2159 BOUNDS_RECT,
2160 false,
2161 );
2162 assert_eq!(
2163 s,
2164 Some(Shape::Triangle {
2165 ax: 200,
2166 ay: 100,
2167 bx: 100,
2168 by: 200,
2169 cx: 300,
2170 cy: 200,
2171 })
2172 );
2173 }
2174
2175 #[test]
2176 fn triangle_hit_test_excludes_bbox_corners() {
2177 let tri = Shape::Triangle {
2178 ax: 200,
2179 ay: 100,
2180 bx: 100,
2181 by: 200,
2182 cx: 300,
2183 cy: 200,
2184 };
2185 assert!(tri.hit_test(Point::new(200, 150))); assert!(tri.hit_test(Point::new(200, 100))); assert!(tri.hit_test(Point::new(150, 200))); assert!(!tri.hit_test(Point::new(105, 105))); assert!(!tri.hit_test(Point::new(295, 105))); }
2191
2192 #[test]
2193 fn triangle_bbox_and_move_clamp() {
2194 let tri = Shape::Triangle {
2195 ax: 200,
2196 ay: 100,
2197 bx: 100,
2198 by: 200,
2199 cx: 300,
2200 cy: 200,
2201 };
2202 assert_eq!(tri.bbox(), Rect::new(100, 100, 200, 100));
2203 let moved = tri.clamp_move(Point::new(0, 0), Point::new(-500, -500), BOUNDS_RECT);
2206 assert_eq!(moved.bbox(), Rect::new(0, 0, 200, 100));
2207 assert_eq!(
2208 moved,
2209 Shape::Triangle {
2210 ax: 100,
2211 ay: 0,
2212 bx: 0,
2213 by: 100,
2214 cx: 200,
2215 cy: 100,
2216 }
2217 );
2218 }
2219
2220 #[test]
2221 fn triangle_resize_scales_vertices_into_new_bbox() {
2222 let tri = Shape::Triangle {
2223 ax: 200,
2224 ay: 100,
2225 bx: 100,
2226 by: 200,
2227 cx: 300,
2228 cy: 200,
2229 };
2230 let handle = ResizeHandle::RectEdges {
2232 left: false,
2233 right: true,
2234 top: false,
2235 bottom: true,
2236 };
2237 let resized = tri.resize_to(handle, Point::new(500, 300), BOUNDS_RECT, false);
2238 assert_eq!(
2239 resized,
2240 Shape::Triangle {
2241 ax: 300,
2242 ay: 100,
2243 bx: 100,
2244 by: 300,
2245 cx: 500,
2246 cy: 300,
2247 }
2248 );
2249 }
2250
2251 #[test]
2252 fn triangle_resize_grab_is_on_the_bbox_border() {
2253 let tri = Shape::Triangle {
2254 ax: 200,
2255 ay: 100,
2256 bx: 100,
2257 by: 200,
2258 cx: 300,
2259 cy: 200,
2260 };
2261 assert_eq!(
2263 tri.resize_grab(Point::new(150, 100), 5),
2264 Some(ResizeHandle::RectEdges {
2265 left: false,
2266 right: false,
2267 top: true,
2268 bottom: false
2269 })
2270 );
2271 assert_eq!(tri.resize_grab(Point::new(200, 150), 5), None); }
2273
2274 #[test]
2275 fn degenerate_triangles_cover_nothing() {
2276 let point = Shape::Triangle {
2277 ax: 0,
2278 ay: 0,
2279 bx: 0,
2280 by: 0,
2281 cx: 0,
2282 cy: 0,
2283 };
2284 assert!(!point.hit_test(Point::new(500, 500)));
2285 assert!(!point.hit_test(Point::new(0, 0)));
2286 let line = Shape::Triangle {
2287 ax: 0,
2288 ay: 0,
2289 bx: 10,
2290 by: 10,
2291 cx: 20,
2292 cy: 20,
2293 };
2294 assert!(!line.hit_test(Point::new(400, 400)));
2295 assert!(!line.hit_test(Point::new(5, 5)));
2296 }
2297
2298 #[test]
2299 fn extreme_shapes_do_not_panic() {
2300 let huge = Shape::Circle {
2301 cx: 0,
2302 cy: 0,
2303 r: 2_000_000_000,
2304 };
2305 let bb = huge.bbox();
2306 assert!(bb.w > 0);
2307 let far = Shape::Rect(Rect::new(
2308 2_000_000_000,
2309 2_000_000_000,
2310 400_000_000,
2311 400_000_000,
2312 ));
2313 let _ = far.rotated_bbox(45);
2314 }
2315
2316 #[test]
2317 fn resize_of_sub_min_rect_stays_in_bounds() {
2318 let s = Shape::Rect(Rect::new(0, 0, 1, 100));
2321 let handle = ResizeHandle::RectEdges {
2322 left: true,
2323 right: false,
2324 top: false,
2325 bottom: false,
2326 };
2327 let Shape::Rect(r) = s.resize_to(handle, Point::new(0, 50), BOUNDS_RECT, false) else {
2328 panic!("still a rect")
2329 };
2330 assert!(r.x >= 0, "escaped left: {r:?}");
2331 let s = Shape::Rect(Rect::new(BOUNDS.w - 1, 0, 1, 100));
2333 let handle = ResizeHandle::RectEdges {
2334 left: false,
2335 right: true,
2336 top: false,
2337 bottom: false,
2338 };
2339 let Shape::Rect(r) = s.resize_to(
2340 handle,
2341 Point::new(BOUNDS_RECT.w - 1, 50),
2342 BOUNDS_RECT,
2343 false,
2344 ) else {
2345 panic!("still a rect")
2346 };
2347 assert!(r.x + r.w <= BOUNDS.w, "escaped right: {r:?}");
2348 }
2349
2350 #[test]
2351 fn rotated_resize_never_moves_the_anchored_edge() {
2352 let s = Shape::Rect(Rect::new(800, 500, 200, 100));
2356 let handle = ResizeHandle::RectEdges {
2357 left: false,
2358 right: true,
2359 top: false,
2360 bottom: false,
2361 };
2362 let Shape::Rect(r) =
2363 s.resize_to_rotated(45, handle, Point::new(1900, 1000), BOUNDS_RECT, false)
2364 else {
2365 panic!("still a rect")
2366 };
2367 assert_eq!(r.x, 800, "anchored left edge moved");
2368 assert_eq!(r.y, 500, "anchored top edge moved");
2369 }
2370
2371 #[test]
2372 fn resize_of_offscreen_local_box_does_not_teleport() {
2373 let s = Shape::Rect(Rect::new(-90, 0, 200, 20));
2377 let handle = ResizeHandle::RectEdges {
2378 left: false,
2379 right: true,
2380 top: false,
2381 bottom: false,
2382 };
2383 let Shape::Rect(r) = s.resize_to(handle, Point::new(120, 10), BOUNDS_RECT, false) else {
2384 panic!("still a rect")
2385 };
2386 assert_eq!(r.x, -90, "shape teleported");
2387 assert_eq!(r.w, 210);
2388 }
2389
2390 #[test]
2391 fn rotated_resize_tracks_cursor_at_screen_edge() {
2392 let s = Shape::Rect(Rect::new(800, 500, 200, 100));
2396 let handle = ResizeHandle::RectEdges {
2397 left: false,
2398 right: true,
2399 top: false,
2400 bottom: false,
2401 };
2402 let r45 = s.resize_to_rotated(45, handle, Point::new(99_999, 99_999), BOUNDS_RECT, false);
2403 assert_ne!(r45, s);
2405 }
2406
2407 #[test]
2408 fn rotate_point_quarter_turn() {
2409 let center = Point::new(100, 100);
2410 assert_eq!(
2412 rotate_point_about(Point::new(110, 100), center, 90),
2413 Point::new(100, 110)
2414 );
2415 assert_eq!(
2416 rotate_point_about(Point::new(110, 100), center, -90),
2417 Point::new(100, 90)
2418 );
2419 assert_eq!(
2420 rotate_point_about(Point::new(110, 100), center, 360),
2421 Point::new(110, 100)
2422 );
2423 }
2424
2425 #[test]
2426 fn normalize_deg_wraps_into_range() {
2427 assert_eq!(normalize_deg(0), 0);
2428 assert_eq!(normalize_deg(-1), 359);
2429 assert_eq!(normalize_deg(360), 0);
2430 assert_eq!(normalize_deg(725), 5);
2431 }
2432
2433 #[test]
2434 fn rotated_bbox_of_quarter_turned_rect_swaps_dimensions() {
2435 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
2436 let bb = s.rotated_bbox(90);
2437 assert_eq!((bb.w, bb.h), (100, 200));
2438 assert_eq!(bb.x + bb.w / 2, 200);
2440 assert_eq!(bb.y + bb.h / 2, 150);
2441 assert_eq!(s.rotated_bbox(0), s.bbox());
2443 let c = Shape::Circle {
2444 cx: 50,
2445 cy: 50,
2446 r: 20,
2447 };
2448 assert_eq!(c.rotated_bbox(45), c.bbox());
2449 }
2450
2451 #[test]
2452 fn rotated_hit_test_follows_the_turned_shape() {
2453 let s = Shape::Rect(Rect::new(100, 100, 200, 20));
2457 assert!(s.hit_test_rotated(90, Point::new(200, 30)));
2458 assert!(!s.hit_test_rotated(90, Point::new(290, 110)));
2459 assert!(s.hit_test_rotated(0, Point::new(290, 110)));
2460 }
2461
2462 #[test]
2463 fn rotated_resize_grab_finds_the_visual_edge() {
2464 let s = Shape::Rect(Rect::new(100, 100, 200, 20));
2466 assert!(s.resize_grab_rotated(90, Point::new(190, 110), 5).is_some());
2468 assert!(s.resize_grab_rotated(90, Point::new(150, 110), 5).is_none());
2469 }
2470
2471 #[test]
2472 fn baked_triangle_rotates_vertices_others_unchanged() {
2473 let tri = Shape::Triangle {
2474 ax: 200,
2475 ay: 100,
2476 bx: 100,
2477 by: 200,
2478 cx: 300,
2479 cy: 200,
2480 };
2481 let baked = tri.with_rotation_baked(180);
2482 assert_eq!(
2484 baked,
2485 Shape::Triangle {
2486 ax: 200,
2487 ay: 200,
2488 bx: 300,
2489 by: 100,
2490 cx: 100,
2491 cy: 100,
2492 }
2493 );
2494 let rect = Shape::Rect(Rect::new(1, 2, 3, 4));
2495 assert_eq!(rect.with_rotation_baked(90), rect);
2496 assert_eq!(tri.with_rotation_baked(0), tri);
2497 }
2498
2499 #[test]
2500 fn triangle_serde_is_distinct_from_rect_and_circle() {
2501 let tri = Shape::Triangle {
2502 ax: 1,
2503 ay: 2,
2504 bx: 3,
2505 by: 4,
2506 cx: 5,
2507 cy: 6,
2508 };
2509 let json = serde_json::to_string(&tri).unwrap();
2510 let back: Shape = serde_json::from_str(&json).unwrap();
2511 assert_eq!(back, tri);
2512 let rect: Shape = serde_json::from_str(r#"{"x":1,"y":2,"w":3,"h":4}"#).unwrap();
2514 assert_eq!(rect, Shape::Rect(Rect::new(1, 2, 3, 4)));
2515 let circle: Shape = serde_json::from_str(r#"{"cx":1,"cy":2,"r":3}"#).unwrap();
2516 assert_eq!(circle, Shape::Circle { cx: 1, cy: 2, r: 3 });
2517 }
2518
2519 #[test]
2520 fn a_triangle_grabs_from_its_bbox_origin() {
2521 let tri = Shape::Triangle {
2522 ax: 50,
2523 ay: 10,
2524 bx: 20,
2525 by: 70,
2526 cx: 80,
2527 cy: 70,
2528 };
2529 assert_eq!(tri.grab_origin(), Point::new(20, 10));
2530 }
2531
2532 #[test]
2533 fn a_rotated_move_clamps_the_rotated_box_to_bounds() {
2534 let rect = Shape::Rect(Rect::new(10, 10, 40, 20));
2535 let bounds = Size::new(200, 200);
2536 let moved = rect.clamp_move_rotated(
2539 45,
2540 Point::new(0, 0),
2541 Point::new(500, 500),
2542 Rect::new(0, 0, bounds.w, bounds.h),
2543 );
2544 let bb = moved.rotated_bbox(45);
2545 assert!(bb.x >= 0 && bb.y >= 0, "{bb:?}");
2546 assert!(bb.x + bb.w <= bounds.w, "{bb:?}");
2547 assert!(bb.y + bb.h <= bounds.h, "{bb:?}");
2548 }
2549
2550 #[test]
2551 fn a_rotated_grab_references_the_rotated_box_origin() {
2552 let rect = Shape::Rect(Rect::new(10, 10, 40, 20));
2553 assert_eq!(rect.grab_origin_rotated(0), rect.grab_origin());
2554 let rotated = rect.grab_origin_rotated(45);
2555 assert_eq!(
2556 rotated,
2557 Point::new(rect.rotated_bbox(45).x, rect.rotated_bbox(45).y)
2558 );
2559 let circle = Shape::Circle {
2561 cx: 40,
2562 cy: 40,
2563 r: 9,
2564 };
2565 assert_eq!(circle.grab_origin_rotated(30), circle.grab_origin());
2566 }
2567
2568 #[test]
2569 fn min3_and_max3_pick_each_position() {
2570 assert_eq!(min3(1, 2, 3), 1);
2571 assert_eq!(min3(2, 1, 3), 1);
2572 assert_eq!(min3(3, 2, 1), 1);
2573 assert_eq!(max3(3, 2, 1), 3);
2574 assert_eq!(max3(1, 3, 2), 3);
2575 assert_eq!(max3(1, 2, 3), 3);
2576 }
2577
2578 #[test]
2579 fn a_proportional_vertical_edge_resize_keeps_the_aspect() {
2580 let rect = Shape::Rect(Rect::new(20, 20, 40, 20));
2582 let resized = rect.resize_to_rotated(
2583 0,
2584 ResizeHandle::RectEdges {
2585 left: false,
2586 right: false,
2587 top: true,
2588 bottom: false,
2589 },
2590 Point::new(30, 0),
2591 Rect::new(0, 0, 300, 300),
2592 true,
2593 );
2594 let bb = resized.bbox();
2595 assert!(bb.w >= 2 && bb.h >= 2, "{bb:?}");
2596 assert!(bb.x >= 0 && bb.x + bb.w <= 300, "{bb:?}");
2597 }
2598}