1use serde::{Deserialize, Serialize};
22
23pub const MAX_COORD: i32 = 1_000_000;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46pub struct Point {
47 pub x: i32,
48 pub y: i32,
49}
50
51impl Point {
52 pub const fn new(x: i32, y: i32) -> Self {
53 Self { x, y }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Size {
59 pub w: i32,
60 pub h: i32,
61}
62
63impl Size {
64 pub const fn new(w: i32, h: i32) -> Self {
65 Self { w, h }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub struct Rect {
71 pub x: i32,
72 pub y: i32,
73 pub w: i32,
74 pub h: i32,
75}
76
77impl Rect {
78 pub const fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
79 Self { x, y, w, h }
80 }
81
82 pub const fn contains(&self, p: Point) -> bool {
83 p.x >= self.x && p.y >= self.y && p.x < self.x + self.w && p.y < self.y + self.h
84 }
85
86 #[must_use]
92 pub fn clamp_point(&self, p: Point) -> Point {
93 Point::new(
94 p.x.clamp(self.x, self.x + (self.w - 1).max(0)),
95 p.y.clamp(self.y, self.y + (self.h - 1).max(0)),
96 )
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum ToolKind {
103 Rect,
104 Circle,
107 Ellipse,
108 Triangle,
109 Polygon,
111 Freehand,
113 Measure,
117 Poly,
120}
121
122impl ToolKind {
123 #[must_use]
124 pub const fn next(self) -> Self {
125 match self {
126 Self::Rect => Self::Ellipse,
127 Self::Circle | Self::Ellipse => Self::Triangle,
128 Self::Triangle => Self::Polygon,
129 Self::Polygon => Self::Freehand,
130 Self::Freehand => Self::Measure,
131 Self::Measure | Self::Poly => Self::Rect,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ResizeHandle {
139 CircleRadius,
141 RectEdges {
143 left: bool,
144 right: bool,
145 top: bool,
146 bottom: bool,
147 },
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(untagged)]
160pub enum Shape {
161 Rect(Rect),
162 Circle {
163 cx: i32,
164 cy: i32,
165 r: i32,
166 },
167 Ellipse {
171 cx: i32,
172 cy: i32,
173 rx: i32,
174 ry: i32,
175 },
176 Triangle {
177 ax: i32,
178 ay: i32,
179 bx: i32,
180 by: i32,
181 cx: i32,
182 cy: i32,
183 },
184 Poly {
188 points: Vec<Point>,
189 },
190}
191
192impl Shape {
193 pub fn compute_preview(
199 tool: ToolKind,
200 start: Point,
201 current: Point,
202 region: Rect,
203 lock: bool,
204 ) -> Option<Self> {
205 let cx = current.x.clamp(region.x, region.x + region.w - 1);
210 let cy = current.y.clamp(region.y, region.y + region.h - 1);
211 match tool {
212 ToolKind::Rect | ToolKind::Triangle | ToolKind::Ellipse => {
213 let x = start.x.min(cx);
214 let y = start.y.min(cy);
215 let w = (start.x - cx).abs();
216 let h = (start.y - cy).abs();
217 if w <= 1 || h <= 1 {
218 return None;
219 }
220 let bbox = Rect::new(x, y, w, h);
221 Some(match tool {
222 ToolKind::Rect => Self::Rect(bbox),
223 ToolKind::Ellipse => ellipse_in_box(bbox, lock),
224 _ => triangle_in_box(bbox),
225 })
226 }
227 ToolKind::Circle => {
228 let dx = f64::from(start.x - cx);
229 let dy = f64::from(start.y - cy);
230 let r = dx.hypot(dy) as i32;
231 if r <= 0 {
232 return None;
233 }
234 Some(Self::Circle {
235 cx: start.x,
236 cy: start.y,
237 r,
238 })
239 }
240 ToolKind::Polygon | ToolKind::Freehand | ToolKind::Poly | ToolKind::Measure => None,
246 }
247 }
248
249 pub const fn kind(&self) -> ToolKind {
250 match self {
251 Self::Rect(_) => ToolKind::Rect,
252 Self::Circle { .. } => ToolKind::Circle,
253 Self::Ellipse { .. } => ToolKind::Ellipse,
254 Self::Triangle { .. } => ToolKind::Triangle,
255 Self::Poly { .. } => ToolKind::Poly,
256 }
257 }
258
259 pub fn bbox(&self) -> Rect {
262 match *self {
263 Self::Poly { ref points } => {
264 let mut x0 = i32::MAX;
265 let mut y0 = i32::MAX;
266 let mut x1 = i32::MIN;
267 let mut y1 = i32::MIN;
268 for p in points {
269 x0 = x0.min(p.x);
270 y0 = y0.min(p.y);
271 x1 = x1.max(p.x);
272 y1 = y1.max(p.y);
273 }
274 if points.is_empty() {
275 return Rect::new(0, 0, 0, 0);
276 }
277 Rect::new(x0, y0, x1.saturating_sub(x0), y1.saturating_sub(y0))
278 }
279 Self::Rect(r) => r,
280 Self::Ellipse { cx, cy, rx, ry } => Rect::new(
281 cx.saturating_sub(rx),
282 cy.saturating_sub(ry),
283 rx.saturating_mul(2),
284 ry.saturating_mul(2),
285 ),
286 Self::Circle { cx, cy, r } => Rect::new(
287 cx.saturating_sub(r),
288 cy.saturating_sub(r),
289 r.saturating_mul(2),
290 r.saturating_mul(2),
291 ),
292 Self::Triangle {
293 ax,
294 ay,
295 bx,
296 by,
297 cx,
298 cy,
299 } => {
300 let x0 = min3(ax, bx, cx);
301 let y0 = min3(ay, by, cy);
302 Rect::new(
303 x0,
304 y0,
305 max3(ax, bx, cx).saturating_sub(x0),
306 max3(ay, by, cy).saturating_sub(y0),
307 )
308 }
309 }
310 }
311
312 pub fn hit_test(&self, p: Point) -> bool {
315 match *self {
316 Self::Poly { ref points } => point_in_poly(points, p),
317 Self::Rect(r) => r.contains(p),
318 Self::Ellipse { cx, cy, rx, ry } => {
319 let dx = i128::from(p.x) - i128::from(cx);
322 let dy = i128::from(p.y) - i128::from(cy);
323 let rx = i128::from(rx);
324 let ry = i128::from(ry);
325 dx * dx * ry * ry + dy * dy * rx * rx <= rx * rx * ry * ry
326 }
327 Self::Circle { cx, cy, r } => {
328 let dx = i64::from(p.x - cx);
329 let dy = i64::from(p.y - cy);
330 dx * dx + dy * dy <= i64::from(r) * i64::from(r)
331 }
332 Self::Triangle {
333 ax,
334 ay,
335 bx,
336 by,
337 cx,
338 cy,
339 } => {
340 if cross(cx, cy, ax, ay, bx, by) == 0 {
343 return false;
344 }
345 let d1 = cross(p.x, p.y, ax, ay, bx, by);
348 let d2 = cross(p.x, p.y, bx, by, cx, cy);
349 let d3 = cross(p.x, p.y, cx, cy, ax, ay);
350 let has_neg = d1 < 0 || d2 < 0 || d3 < 0;
351 let has_pos = d1 > 0 || d2 > 0 || d3 > 0;
352 !(has_neg && has_pos)
353 }
354 }
355 }
356
357 pub fn covers(&self, x: i32, y: i32) -> bool {
360 self.hit_test(Point::new(x, y))
361 }
362
363 pub fn click_point(&self) -> Point {
369 match *self {
370 Self::Poly { ref points } => poly_interior_point(points),
371 Self::Rect(_) => self.pivot(),
372 Self::Circle { cx, cy, .. } | Self::Ellipse { cx, cy, .. } => Point::new(cx, cy),
373 Self::Triangle {
374 ax,
375 ay,
376 bx,
377 by,
378 cx,
379 cy,
380 } => Point::new(
381 ((i64::from(ax) + i64::from(bx) + i64::from(cx)) / 3) as i32,
382 ((i64::from(ay) + i64::from(by) + i64::from(cy)) / 3) as i32,
383 ),
384 }
385 }
386
387 pub fn grab_origin(&self) -> Point {
390 match *self {
391 Self::Rect(r) => Point::new(r.x, r.y),
392 Self::Circle { cx, cy, .. } | Self::Ellipse { cx, cy, .. } => Point::new(cx, cy),
393 Self::Triangle { .. } | Self::Poly { .. } => {
394 let b = self.bbox();
395 Point::new(b.x, b.y)
396 }
397 }
398 }
399
400 #[must_use]
403 pub fn clamp_move(&self, grab_offset: Point, cursor: Point, region: Rect) -> Self {
404 let right = region.x + region.w;
408 let bottom = region.y + region.h;
409 match *self {
410 Self::Rect(rect) => {
411 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - rect.w).max(region.x));
412 let ny =
413 (cursor.y - grab_offset.y).clamp(region.y, (bottom - rect.h).max(region.y));
414 Self::Rect(Rect::new(nx, ny, rect.w, rect.h))
415 }
416 Self::Circle { r, .. } => {
417 let min_x = region.x + r.max(0);
418 let min_y = region.y + r.max(0);
419 let cx = (cursor.x - grab_offset.x).clamp(min_x, (right - r).max(min_x));
420 let cy = (cursor.y - grab_offset.y).clamp(min_y, (bottom - r).max(min_y));
421 Self::Circle { cx, cy, r }
422 }
423 Self::Ellipse { rx, ry, .. } => {
424 let min_x = region.x + rx.max(0);
425 let min_y = region.y + ry.max(0);
426 let cx = (cursor.x - grab_offset.x).clamp(min_x, (right - rx).max(min_x));
427 let cy = (cursor.y - grab_offset.y).clamp(min_y, (bottom - ry).max(min_y));
428 Self::Ellipse { cx, cy, rx, ry }
429 }
430 Self::Triangle { .. } | Self::Poly { .. } => {
431 let b = self.bbox();
432 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - b.w).max(region.x));
433 let ny = (cursor.y - grab_offset.y).clamp(region.y, (bottom - b.h).max(region.y));
434 self.translated(nx - b.x, ny - b.y)
435 }
436 }
437 }
438
439 pub fn resize_grab(&self, p: Point, tolerance: i32) -> Option<ResizeHandle> {
443 let tolerance = tolerance.max(1);
444 match *self {
445 Self::Circle { cx, cy, r } => {
446 let dist = f64::from(p.x - cx).hypot(f64::from(p.y - cy));
447 let on_rim = (dist - f64::from(r)).abs() <= f64::from(tolerance);
448 on_rim.then_some(ResizeHandle::CircleRadius)
449 }
450 Self::Rect(rect) => box_border_grab(rect, p, tolerance),
453 Self::Ellipse { .. } | Self::Triangle { .. } | Self::Poly { .. } => {
454 box_border_grab(self.bbox(), p, tolerance)
455 }
456 }
457 }
458
459 #[must_use]
470 pub fn resize_to(
471 &self,
472 handle: ResizeHandle,
473 cursor: Point,
474 region: Rect,
475 keep_aspect: bool,
476 ) -> Self {
477 let clamped = Point::new(
478 cursor.x.clamp(region.x, region.x + region.w - 1),
479 cursor.y.clamp(region.y, region.y + region.h - 1),
480 );
481 self.resize_to_local(handle, clamped, region, keep_aspect)
482 }
483
484 #[must_use]
488 fn resize_to_local(
489 &self,
490 handle: ResizeHandle,
491 clamped: Point,
492 region: Rect,
493 keep_aspect: bool,
494 ) -> Self {
495 const MIN: i32 = 2;
496 match (self.clone(), handle) {
497 (Self::Circle { cx, cy, .. }, ResizeHandle::CircleRadius) => {
498 let r = f64::from(clamped.x - cx).hypot(f64::from(clamped.y - cy)) as i32;
499 Self::Circle {
500 cx,
501 cy,
502 r: r.max(MIN),
503 }
504 }
505 (
506 Self::Rect(rect),
507 ResizeHandle::RectEdges {
508 left,
509 right,
510 top,
511 bottom,
512 },
513 ) => Self::Rect(resize_box(
514 rect,
515 (left, right, top, bottom),
516 clamped,
517 region,
518 keep_aspect,
519 )),
520 (
521 ell @ Self::Ellipse { .. },
522 ResizeHandle::RectEdges {
523 left,
524 right,
525 top,
526 bottom,
527 },
528 ) => {
529 let bb = resize_box(
532 ell.bbox(),
533 (left, right, top, bottom),
534 clamped,
535 region,
536 keep_aspect,
537 );
538 ellipse_in_box(bb, false)
539 }
540 (
541 poly @ Self::Poly { .. },
542 ResizeHandle::RectEdges {
543 left,
544 right,
545 top,
546 bottom,
547 },
548 ) => {
549 let old = poly.bbox();
550 let new = resize_box(
551 old,
552 (left, right, top, bottom),
553 clamped,
554 region,
555 keep_aspect,
556 );
557 scale_into_box(&poly, old, new)
558 }
559 (
560 tri @ Self::Triangle { .. },
561 ResizeHandle::RectEdges {
562 left,
563 right,
564 top,
565 bottom,
566 },
567 ) => {
568 let old = tri.bbox();
569 let new = resize_box(
570 old,
571 (left, right, top, bottom),
572 clamped,
573 region,
574 keep_aspect,
575 );
576 tri.mapped_between_boxes(old, new)
577 }
578 (shape, _) => shape,
581 }
582 }
583
584 #[must_use]
587 fn mapped_between_boxes(&self, old: Rect, new: Rect) -> Self {
588 let map_x = |v: i32| {
589 new.x
590 + (f64::from(v - old.x) * f64::from(new.w) / f64::from(old.w.max(1))).round() as i32
591 };
592 let map_y = |v: i32| {
593 new.y
594 + (f64::from(v - old.y) * f64::from(new.h) / f64::from(old.h.max(1))).round() as i32
595 };
596 match self.clone() {
597 Self::Triangle {
598 ax,
599 ay,
600 bx,
601 by,
602 cx,
603 cy,
604 } => Self::Triangle {
605 ax: map_x(ax),
606 ay: map_y(ay),
607 bx: map_x(bx),
608 by: map_y(by),
609 cx: map_x(cx),
610 cy: map_y(cy),
611 },
612 other => other,
613 }
614 }
615
616 #[must_use]
619 pub fn translated(&self, dx: i32, dy: i32) -> Self {
620 match *self {
621 Self::Poly { ref points } => Self::Poly {
622 points: points
623 .iter()
624 .map(|p| Point::new(p.x + dx, p.y + dy))
625 .collect(),
626 },
627 Self::Rect(r) => Self::Rect(Rect::new(r.x + dx, r.y + dy, r.w, r.h)),
628 Self::Circle { cx, cy, r } => Self::Circle {
629 cx: cx + dx,
630 cy: cy + dy,
631 r,
632 },
633 Self::Ellipse { cx, cy, rx, ry } => Self::Ellipse {
634 cx: cx + dx,
635 cy: cy + dy,
636 rx,
637 ry,
638 },
639 Self::Triangle {
640 ax,
641 ay,
642 bx,
643 by,
644 cx,
645 cy,
646 } => Self::Triangle {
647 ax: ax + dx,
648 ay: ay + dy,
649 bx: bx + dx,
650 by: by + dy,
651 cx: cx + dx,
652 cy: cy + dy,
653 },
654 }
655 }
656}
657
658pub fn normalize_deg(deg: i32) -> i32 {
660 deg.rem_euclid(360)
661}
662
663fn scale_into_box(shape: &Shape, old: Rect, new: Rect) -> Shape {
666 let map_x = |v: i32| {
667 new.x + (f64::from(v - old.x) * f64::from(new.w) / f64::from(old.w.max(1))).round() as i32
668 };
669 let map_y = |v: i32| {
670 new.y + (f64::from(v - old.y) * f64::from(new.h) / f64::from(old.h.max(1))).round() as i32
671 };
672 match shape {
673 Shape::Poly { points } => Shape::Poly {
674 points: points
675 .iter()
676 .map(|p| Point::new(map_x(p.x), map_y(p.y)))
677 .collect(),
678 },
679 other => other.clone(),
680 }
681}
682
683fn point_in_poly(points: &[Point], p: Point) -> bool {
686 if points.len() < 3 {
687 return false;
688 }
689 let n = points.len();
690 let mut inside = false;
691 for i in 0..n {
692 let a = points[i];
693 let b = points[(i + 1) % n];
694 if on_segment(a, b, p) {
695 return true;
696 }
697 if (a.y > p.y) != (b.y > p.y) {
699 let cross = i64::from(b.x - a.x) * i64::from(p.y - a.y)
700 - i64::from(b.y - a.y) * i64::from(p.x - a.x);
701 let crosses = if b.y > a.y { cross > 0 } else { cross < 0 };
702 if crosses {
703 inside = !inside;
704 }
705 }
706 }
707 inside
708}
709
710fn on_segment(a: Point, b: Point, p: Point) -> bool {
712 let cross =
713 i64::from(b.x - a.x) * i64::from(p.y - a.y) - i64::from(b.y - a.y) * i64::from(p.x - a.x);
714 cross == 0
715 && p.x >= a.x.min(b.x)
716 && p.x <= a.x.max(b.x)
717 && p.y >= a.y.min(b.y)
718 && p.y <= a.y.max(b.y)
719}
720
721fn poly_interior_point(points: &[Point]) -> Point {
726 if points.is_empty() {
727 return Point::new(0, 0);
728 }
729 let n = points.len() as i64;
730 let sx: i64 = points.iter().map(|p| i64::from(p.x)).sum();
731 let sy: i64 = points.iter().map(|p| i64::from(p.y)).sum();
732 let mean = Point::new((sx / n) as i32, (sy / n) as i32);
733 if point_in_poly(points, mean) {
734 return mean;
735 }
736 let bb = Shape::Poly {
737 points: points.to_vec(),
738 }
739 .bbox();
740 for eighth in [4, 2, 6, 1, 3, 5, 7] {
752 let offset = i64::from(bb.h) * eighth / 8;
753 let Ok(offset) = i32::try_from(offset) else {
754 continue;
755 };
756 let row = bb.y.saturating_add(offset);
757 let crossings = scanline_spans(points, row);
758 let widest = crossings
759 .chunks_exact(2)
760 .filter_map(|pair| match pair {
761 [left, right] if right > left => Some((right - left, (left + right) / 2.0)),
762 _ => None,
763 })
764 .max_by(|(a, _), (b, _)| a.total_cmp(b));
765 let Some((_, center)) = widest else {
766 continue;
767 };
768 let candidate = Point::new(center.round() as i32, row);
769 if point_in_poly(points, candidate) {
774 return candidate;
775 }
776 }
777 mean
778}
779
780pub(crate) fn scanline_spans(points: &[Point], row: i32) -> Vec<f64> {
784 let mid = f64::from(row) + 0.5;
785 let count = points.len();
786 let mut crossings = Vec::new();
787 for i in 0..count {
788 let from = points[i];
789 let to = points[(i + 1) % count];
790 let (from_y, to_y) = (f64::from(from.y), f64::from(to.y));
791 if (from_y < mid) == (to_y < mid) {
792 continue;
793 }
794 let t = (mid - from_y) / (to_y - from_y);
795 crossings.push(f64::from(from.x) + t * f64::from(to.x - from.x));
796 }
797 crossings.sort_by(f64::total_cmp);
798 crossings
799}
800
801pub const MIN_POLYGON_SIDES: u32 = 3;
804
805pub const MAX_POLYGON_SIDES: u32 = 1_000;
817
818pub fn regular_polygon(center: Point, toward: Point, sides: u32) -> Shape {
826 let sides = sides.clamp(MIN_POLYGON_SIDES, MAX_POLYGON_SIDES) as usize;
827 let dx = f64::from(toward.x) - f64::from(center.x);
828 let dy = f64::from(toward.y) - f64::from(center.y);
829 let r = dx.hypot(dy);
830 let base = dy.atan2(dx);
831 let step = std::f64::consts::TAU / sides as f64;
832 let points = (0..sides)
833 .map(|i| {
834 let a = base + step * i as f64;
835 Point::new(
836 f64::from(center.x).mul_add(1.0, r * a.cos()).round() as i32,
837 f64::from(center.y).mul_add(1.0, r * a.sin()).round() as i32,
838 )
839 })
840 .collect();
841 Shape::Poly { points }
842}
843
844pub fn simplify_path(points: &[Point], epsilon: f64) -> Vec<Point> {
848 if points.len() <= 2 {
849 return points.to_vec();
850 }
851 let mut keep = vec![false; points.len()];
852 keep[0] = true;
853 keep[points.len() - 1] = true;
854 let mut stack = vec![(0usize, points.len() - 1)];
855 while let Some((start, end)) = stack.pop() {
856 if end <= start + 1 {
857 continue;
858 }
859 let (mut worst, mut worst_dist) = (start, -1.0f64);
860 for (i, p) in points.iter().enumerate().take(end).skip(start + 1) {
861 let d = point_segment_distance(*p, points[start], points[end]);
862 if d > worst_dist {
863 worst = i;
864 worst_dist = d;
865 }
866 }
867 if worst_dist > epsilon {
868 keep[worst] = true;
869 stack.push((start, worst));
870 stack.push((worst, end));
871 }
872 }
873 points
874 .iter()
875 .zip(&keep)
876 .filter(|(_, k)| **k)
877 .map(|(p, _)| *p)
878 .collect()
879}
880
881fn point_segment_distance(p: Point, a: Point, b: Point) -> f64 {
883 let (px, py) = (f64::from(p.x), f64::from(p.y));
884 let (ax, ay) = (f64::from(a.x), f64::from(a.y));
885 let (bx, by) = (f64::from(b.x), f64::from(b.y));
886 let (dx, dy) = (bx - ax, by - ay);
887 let len2 = dx * dx + dy * dy;
888 if len2 <= f64::EPSILON {
889 return (px - ax).hypot(py - ay);
890 }
891 let t = ((px - ax) * dx + (py - ay) * dy) / len2;
892 let t = t.clamp(0.0, 1.0);
893 (px - (ax + t * dx)).hypot(py - (ay + t * dy))
894}
895
896fn ellipse_in_box(bbox: Rect, lock: bool) -> Shape {
899 let cx = bbox.x + bbox.w / 2;
900 let cy = bbox.y + bbox.h / 2;
901 let (rx, ry) = (bbox.w / 2, bbox.h / 2);
902 if lock {
903 let r = rx.min(ry).max(1);
904 return Shape::Ellipse {
905 cx,
906 cy,
907 rx: r,
908 ry: r,
909 };
910 }
911 Shape::Ellipse {
912 cx,
913 cy,
914 rx: rx.max(1),
915 ry: ry.max(1),
916 }
917}
918
919pub fn rotate_point_about(p: Point, center: Point, deg: i32) -> Point {
922 let rad = f64::from(deg).to_radians();
923 let (sin, cos) = rad.sin_cos();
924 let dx = f64::from(p.x) - f64::from(center.x);
927 let dy = f64::from(p.y) - f64::from(center.y);
928 Point::new(
929 (f64::from(center.x) + (dx * cos - dy * sin).round()) as i32,
930 (f64::from(center.y) + (dx * sin + dy * cos).round()) as i32,
931 )
932}
933
934impl Shape {
935 pub fn pivot(&self) -> Point {
937 let b = self.bbox();
938 Point::new(b.x.saturating_add(b.w / 2), b.y.saturating_add(b.h / 2))
939 }
940
941 pub fn rotated_bbox(&self, deg: i32) -> Rect {
943 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
944 return self.bbox();
945 }
946 let b = self.bbox();
947 let pivot = self.pivot();
948 let (bx1, by1) = (b.x.saturating_add(b.w), b.y.saturating_add(b.h));
949 let corners = [
950 Point::new(b.x, b.y),
951 Point::new(bx1, b.y),
952 Point::new(b.x, by1),
953 Point::new(bx1, by1),
954 ]
955 .map(|c| rotate_point_about(c, pivot, deg));
956 let x0 = corners.iter().map(|c| c.x).min().unwrap_or(b.x);
957 let y0 = corners.iter().map(|c| c.y).min().unwrap_or(b.y);
958 let x1 = corners.iter().map(|c| c.x).max().unwrap_or(bx1);
959 let y1 = corners.iter().map(|c| c.y).max().unwrap_or(by1);
960 Rect::new(x0, y0, x1.saturating_sub(x0), y1.saturating_sub(y0))
961 }
962
963 pub fn hit_test_rotated(&self, deg: i32, p: Point) -> bool {
966 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
967 return self.hit_test(p);
968 }
969 self.hit_test(rotate_point_about(p, self.pivot(), -deg))
970 }
971
972 pub fn resize_grab_rotated(&self, deg: i32, p: Point, tolerance: i32) -> Option<ResizeHandle> {
975 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
976 return self.resize_grab(p, tolerance);
977 }
978 self.resize_grab(rotate_point_about(p, self.pivot(), -deg), tolerance)
979 }
980
981 #[must_use]
983 pub fn resize_to_rotated(
984 &self,
985 deg: i32,
986 handle: ResizeHandle,
987 cursor: Point,
988 region: Rect,
989 keep_aspect: bool,
990 ) -> Self {
991 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
992 return self.resize_to(handle, cursor, region, keep_aspect);
993 }
994 let visual = Point::new(
998 cursor.x.clamp(region.x, region.x + region.w - 1),
999 cursor.y.clamp(region.y, region.y + region.h - 1),
1000 );
1001 let local = rotate_point_about(visual, self.pivot(), -deg);
1002 self.resize_to_local(handle, local, region, keep_aspect)
1003 }
1004
1005 #[must_use]
1007 pub fn clamp_move_rotated(
1008 &self,
1009 deg: i32,
1010 grab_offset: Point,
1011 cursor: Point,
1012 region: Rect,
1013 ) -> Self {
1014 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
1015 return self.clamp_move(grab_offset, cursor, region);
1016 }
1017 let bb = self.rotated_bbox(deg);
1018 let right = region.x.saturating_add(region.w);
1019 let bottom = region.y.saturating_add(region.h);
1020 let nx = cursor
1021 .x
1022 .saturating_sub(grab_offset.x)
1023 .clamp(region.x, right.saturating_sub(bb.w).max(region.x));
1024 let ny = cursor
1025 .y
1026 .saturating_sub(grab_offset.y)
1027 .clamp(region.y, bottom.saturating_sub(bb.h).max(region.y));
1028 self.translated(nx.saturating_sub(bb.x), ny.saturating_sub(bb.y))
1029 }
1030
1031 pub fn grab_origin_rotated(&self, deg: i32) -> Point {
1033 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
1034 return self.grab_origin();
1035 }
1036 let bb = self.rotated_bbox(deg);
1037 Point::new(bb.x, bb.y)
1038 }
1039
1040 #[must_use]
1044 pub fn with_rotation_baked(&self, deg: i32) -> Self {
1045 if let Self::Poly { points } = self {
1046 if normalize_deg(deg) == 0 {
1047 return self.clone();
1048 }
1049 let pivot = self.pivot();
1050 return Self::Poly {
1051 points: points
1052 .iter()
1053 .map(|p| rotate_point_about(*p, pivot, deg))
1054 .collect(),
1055 };
1056 }
1057 match self.clone() {
1058 Self::Triangle {
1059 ax,
1060 ay,
1061 bx,
1062 by,
1063 cx,
1064 cy,
1065 } if normalize_deg(deg) != 0 => {
1066 let pivot = self.pivot();
1067 let a = rotate_point_about(Point::new(ax, ay), pivot, deg);
1068 let b = rotate_point_about(Point::new(bx, by), pivot, deg);
1069 let c = rotate_point_about(Point::new(cx, cy), pivot, deg);
1070 Self::Triangle {
1071 ax: a.x,
1072 ay: a.y,
1073 bx: b.x,
1074 by: b.y,
1075 cx: c.x,
1076 cy: c.y,
1077 }
1078 }
1079 other => other,
1080 }
1081 }
1082}
1083
1084const fn triangle_in_box(bbox: Rect) -> Shape {
1086 Shape::Triangle {
1087 ax: bbox.x + bbox.w / 2,
1088 ay: bbox.y,
1089 bx: bbox.x,
1090 by: bbox.y + bbox.h,
1091 cx: bbox.x + bbox.w,
1092 cy: bbox.y + bbox.h,
1093 }
1094}
1095
1096const fn cross(px: i32, py: i32, ax: i32, ay: i32, bx: i32, by: i32) -> i64 {
1099 let abx = bx as i64 - ax as i64;
1100 let aby = by as i64 - ay as i64;
1101 let apx = px as i64 - ax as i64;
1102 let apy = py as i64 - ay as i64;
1103 abx * apy - aby * apx
1104}
1105
1106const fn min3(a: i32, b: i32, c: i32) -> i32 {
1107 if a <= b && a <= c {
1108 return a;
1109 }
1110 if b <= c {
1111 return b;
1112 }
1113 c
1114}
1115
1116const fn max3(a: i32, b: i32, c: i32) -> i32 {
1117 if a >= b && a >= c {
1118 return a;
1119 }
1120 if b >= c {
1121 return b;
1122 }
1123 c
1124}
1125
1126fn box_border_grab(rect: Rect, p: Point, tolerance: i32) -> Option<ResizeHandle> {
1129 let (x1, y1) = (rect.x + rect.w, rect.y + rect.h);
1130 let within_x = p.x >= rect.x - tolerance && p.x <= x1 + tolerance;
1131 let within_y = p.y >= rect.y - tolerance && p.y <= y1 + tolerance;
1132 let left_d = (p.x - rect.x).abs();
1133 let right_d = (p.x - x1).abs();
1134 let top_d = (p.y - rect.y).abs();
1135 let bottom_d = (p.y - y1).abs();
1136 let mut left = left_d <= tolerance && within_y;
1137 let mut right = right_d <= tolerance && within_y;
1138 let mut top = top_d <= tolerance && within_x;
1139 let mut bottom = bottom_d <= tolerance && within_x;
1140 if left && right {
1143 right = right_d < left_d;
1144 left = !right;
1145 }
1146 if top && bottom {
1147 bottom = bottom_d < top_d;
1148 top = !bottom;
1149 }
1150 let grabbed = left || right || top || bottom;
1151 grabbed.then_some(ResizeHandle::RectEdges {
1152 left,
1153 right,
1154 top,
1155 bottom,
1156 })
1157}
1158
1159fn resize_box(
1163 rect: Rect,
1164 (left, right, top, bottom): (bool, bool, bool, bool),
1165 clamped: Point,
1166 region: Rect,
1167 keep_aspect: bool,
1168) -> Rect {
1169 const MIN: i32 = 2;
1170 let mut x0 = rect.x;
1171 let mut x1 = rect.x + rect.w;
1172 let mut y0 = rect.y;
1173 let mut y1 = rect.y + rect.h;
1174 if left {
1175 x0 = clamped.x.min(x1 - MIN);
1176 }
1177 if right {
1178 x1 = clamped.x.max(x0 + MIN);
1179 }
1180 if top {
1181 y0 = clamped.y.min(y1 - MIN);
1182 }
1183 if bottom {
1184 y1 = clamped.y.max(y0 + MIN);
1185 }
1186 if keep_aspect && rect.w >= MIN && rect.h >= MIN {
1187 let (w0, h0) = (f64::from(rect.w), f64::from(rect.h));
1188 match (left || right, top || bottom) {
1191 (true, true) => {
1192 let mut s = (f64::from(x1 - x0) / w0).max(f64::from(y1 - y0) / h0);
1195 let region_right = region.x + region.w;
1196 let region_bottom = region.y + region.h;
1197 let avail_w = if left {
1198 x1 - region.x
1199 } else {
1200 region_right - x0
1201 };
1202 let avail_h = if top {
1203 y1 - region.y
1204 } else {
1205 region_bottom - y0
1206 };
1207 s = s.min(f64::from(avail_w) / w0).min(f64::from(avail_h) / h0);
1208 let w = ((w0 * s).round() as i32).max(MIN);
1209 let h = ((h0 * s).round() as i32).max(MIN);
1210 (x0, x1) = if left { (x1 - w, x1) } else { (x0, x0 + w) };
1211 (y0, y1) = if top { (y1 - h, y1) } else { (y0, y0 + h) };
1212 }
1213 (true, false) => {
1214 let h = ((f64::from(x1 - x0) * h0 / w0).round() as i32)
1217 .max(MIN)
1218 .min(region.h);
1219 let center_y = rect.y + rect.h / 2;
1220 y0 = (center_y - h / 2).clamp(region.y, region.y + region.h - h);
1221 y1 = y0 + h;
1222 }
1223 (false, _) => {
1224 let w = ((f64::from(y1 - y0) * w0 / h0).round() as i32)
1225 .max(MIN)
1226 .min(region.w);
1227 let center_x = rect.x + rect.w / 2;
1228 x0 = (center_x - w / 2).clamp(region.x, region.x + region.w - w);
1229 x1 = x0 + w;
1230 }
1231 }
1232 }
1233 let (w, h) = (x1 - x0, y1 - y0);
1234 if rect.w >= MIN && rect.h >= MIN {
1235 return Rect::new(x0, y0, w, h);
1240 }
1241 let x0 = x0.clamp(region.x, (region.x + region.w - w).max(region.x));
1244 let y0 = y0.clamp(region.y, (region.y + region.h - h).max(region.y));
1245 Rect::new(x0, y0, w, h)
1246}
1247
1248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1255pub struct Line {
1256 pub a: Point,
1257 pub b: Point,
1258}
1259
1260impl Line {
1261 #[must_use]
1262 pub const fn new(a: Point, b: Point) -> Self {
1263 Self { a, b }
1264 }
1265
1266 #[must_use]
1275 pub const fn delta(self) -> (i32, i32) {
1276 (
1277 self.b.x.saturating_sub(self.a.x),
1278 self.b.y.saturating_sub(self.a.y),
1279 )
1280 }
1281
1282 #[must_use]
1284 pub fn length(self) -> f64 {
1285 let (dx, dy) = self.delta_f64();
1286 dx.hypot(dy)
1287 }
1288
1289 fn delta_f64(self) -> (f64, f64) {
1292 (
1293 f64::from(self.b.x) - f64::from(self.a.x),
1294 f64::from(self.b.y) - f64::from(self.a.y),
1295 )
1296 }
1297
1298 #[must_use]
1308 pub fn angle_deg(self) -> f64 {
1309 let (dx, dy) = self.delta_f64();
1310 if dx == 0.0 && dy == 0.0 {
1311 return 0.0;
1312 }
1313 let deg = dy.atan2(dx).to_degrees();
1314 if deg < 0.0 { deg + 360.0 } else { deg }
1315 }
1316
1317 #[must_use]
1320 pub fn bbox(self) -> Rect {
1321 let x = self.a.x.min(self.b.x);
1322 let y = self.a.y.min(self.b.y);
1323 Rect::new(
1324 x,
1325 y,
1326 i32::try_from(self.a.x.abs_diff(self.b.x)).unwrap_or(i32::MAX),
1327 i32::try_from(self.a.y.abs_diff(self.b.y)).unwrap_or(i32::MAX),
1328 )
1329 }
1330
1331 #[must_use]
1332 pub const fn translated(self, dx: i32, dy: i32) -> Self {
1333 Self::new(
1334 Point::new(self.a.x + dx, self.a.y + dy),
1335 Point::new(self.b.x + dx, self.b.y + dy),
1336 )
1337 }
1338
1339 #[must_use]
1345 pub fn endpoint_grab(self, p: Point, tolerance: i32) -> Option<bool> {
1346 let near = |q: Point| {
1347 let (dx, dy) = (i64::from(p.x - q.x), i64::from(p.y - q.y));
1348 dx * dx + dy * dy <= i64::from(tolerance) * i64::from(tolerance)
1349 };
1350 if near(self.a) {
1351 return Some(true);
1352 }
1353 near(self.b).then_some(false)
1354 }
1355
1356 #[must_use]
1359 pub fn hit_test(self, p: Point, tolerance: i32) -> bool {
1360 self.distance_to(p) <= f64::from(tolerance)
1361 }
1362
1363 #[must_use]
1367 pub fn distance_to(self, p: Point) -> f64 {
1368 let (dx, dy) = self.delta();
1369 let (dx, dy) = (f64::from(dx), f64::from(dy));
1370 let len_sq = dx.mul_add(dx, dy * dy);
1371 let (px, py) = (f64::from(p.x - self.a.x), f64::from(p.y - self.a.y));
1372 if len_sq <= f64::EPSILON {
1373 return px.hypot(py);
1374 }
1375 let t = px.mul_add(dx, py * dy) / len_sq;
1376 let t = t.clamp(0.0, 1.0);
1377 (px - t * dx).hypot(py - t * dy)
1378 }
1379
1380 #[must_use]
1388 pub fn constrained(self) -> Self {
1389 const AXES: [(f64, f64); 8] = [
1391 (1.0, 0.0),
1392 (-1.0, 0.0),
1393 (0.0, 1.0),
1394 (0.0, -1.0),
1395 (
1396 std::f64::consts::FRAC_1_SQRT_2,
1397 std::f64::consts::FRAC_1_SQRT_2,
1398 ),
1399 (
1400 std::f64::consts::FRAC_1_SQRT_2,
1401 -std::f64::consts::FRAC_1_SQRT_2,
1402 ),
1403 (
1404 -std::f64::consts::FRAC_1_SQRT_2,
1405 std::f64::consts::FRAC_1_SQRT_2,
1406 ),
1407 (
1408 -std::f64::consts::FRAC_1_SQRT_2,
1409 -std::f64::consts::FRAC_1_SQRT_2,
1410 ),
1411 ];
1412 let (dx, dy) = self.delta();
1413 if dx == 0 && dy == 0 {
1414 return self;
1415 }
1416 let (fx, fy) = (f64::from(dx), f64::from(dy));
1417 let mut best = (f64::NEG_INFINITY, 0.0, 0.0);
1418 for (ax, ay) in AXES {
1419 let projection = fx.mul_add(ax, fy * ay);
1420 if projection > best.0 {
1421 best = (projection, ax, ay);
1422 }
1423 }
1424 let (projection, ax, ay) = best;
1425 let projection = projection.max(0.0);
1426 Self::new(
1427 self.a,
1428 Point::new(
1429 self.a.x + (projection * ax).round() as i32,
1430 self.a.y + (projection * ay).round() as i32,
1431 ),
1432 )
1433 }
1434}
1435
1436#[cfg(test)]
1437mod tests {
1438 use super::*;
1439
1440 #[test]
1441 fn length_and_delta_are_the_plain_arithmetic() {
1442 let line = Line::new(Point::new(10, 20), Point::new(40, 60));
1443 assert_eq!(line.delta(), (30, 40));
1444 assert!((line.length() - 50.0).abs() < 1e-9, "3-4-5 triangle");
1445 }
1446
1447 #[test]
1448 fn length_is_invariant_under_translation() {
1449 let line = Line::new(Point::new(-5, 7), Point::new(11, -3));
1450 let moved = line.translated(1000, -400);
1451 assert!((line.length() - moved.length()).abs() < 1e-9);
1452 assert_eq!(line.delta(), moved.delta());
1453 }
1454
1455 #[test]
1456 fn angle_is_clockwise_from_positive_x() {
1457 let at = |dx, dy| Line::new(Point::new(0, 0), Point::new(dx, dy)).angle_deg();
1459 assert!((at(10, 0) - 0.0).abs() < 1e-9, "right");
1460 assert!((at(0, 10) - 90.0).abs() < 1e-9, "down");
1461 assert!((at(-10, 0) - 180.0).abs() < 1e-9, "left");
1462 assert!((at(0, -10) - 270.0).abs() < 1e-9, "up");
1463 assert!((at(10, 10) - 45.0).abs() < 1e-9, "down-right");
1464 }
1465
1466 #[test]
1467 fn angle_is_antisymmetric_under_endpoint_swap() {
1468 for (ax, ay, bx, by) in [
1470 (0, 0, 10, 0),
1471 (3, 7, -11, 2),
1472 (-5, -5, 5, 5),
1473 (100, -20, 100, 40),
1474 ] {
1475 let ab = Line::new(Point::new(ax, ay), Point::new(bx, by)).angle_deg();
1476 let ba = Line::new(Point::new(bx, by), Point::new(ax, ay)).angle_deg();
1477 let expected = (ba + 180.0) % 360.0;
1478 assert!((ab - expected).abs() < 1e-9, "{ab} vs {expected}");
1479 }
1480 }
1481
1482 #[test]
1483 fn a_zero_length_measure_has_no_direction_rather_than_nan() {
1484 let dot = Line::new(Point::new(4, 4), Point::new(4, 4));
1485 assert!((dot.length() - 0.0).abs() < f64::EPSILON);
1486 assert!(dot.angle_deg().is_finite(), "atan2(0,0) must not escape");
1487 assert!((dot.angle_deg() - 0.0).abs() < f64::EPSILON);
1488 assert!(dot.hit_test(Point::new(4, 4), 6));
1490 }
1491
1492 #[test]
1493 fn distance_clamps_at_the_ends_rather_than_using_the_infinite_line() {
1494 let line = Line::new(Point::new(0, 0), Point::new(100, 0));
1495 assert!((line.distance_to(Point::new(50, 10)) - 10.0).abs() < 1e-9);
1497 assert!((line.distance_to(Point::new(200, 0)) - 100.0).abs() < 1e-9);
1500 assert!((line.distance_to(Point::new(-30, 40)) - 50.0).abs() < 1e-9);
1501 }
1502
1503 #[test]
1504 fn grabbing_prefers_an_endpoint_then_the_segment() {
1505 let line = Line::new(Point::new(0, 0), Point::new(100, 0));
1506 assert_eq!(line.endpoint_grab(Point::new(2, 2), 6), Some(true), "a");
1507 assert_eq!(line.endpoint_grab(Point::new(98, 1), 6), Some(false), "b");
1508 assert_eq!(line.endpoint_grab(Point::new(50, 0), 6), None, "middle");
1509 assert!(line.hit_test(Point::new(50, 3), 6), "still on the line");
1510 assert!(!line.hit_test(Point::new(50, 40), 6));
1511 }
1512
1513 #[test]
1514 fn shift_snaps_to_the_eight_directions_and_tracks_the_pointer() {
1515 let from = Point::new(100, 100);
1516 let nearly = Line::new(from, Point::new(200, 104)).constrained();
1518 assert_eq!(nearly.b.y, 100, "snapped to horizontal");
1519 assert!((nearly.length() - 100.0).abs() < 1.0, "reach preserved");
1520
1521 let diag = Line::new(from, Point::new(160, 172)).constrained();
1523 assert!(
1524 ((diag.b.x - from.x) - (diag.b.y - from.y)).abs() <= 1,
1525 "equal legs: {diag:?}"
1526 );
1527 assert!((diag.angle_deg() - 45.0).abs() < 1.0);
1528
1529 let up_left = Line::new(from, Point::new(30, 26)).constrained();
1531 assert!((up_left.angle_deg() - 225.0).abs() < 1.0, "{up_left:?}");
1532 }
1533
1534 #[test]
1535 fn constraining_a_zero_length_measure_leaves_it_alone() {
1536 let dot = Line::new(Point::new(9, 9), Point::new(9, 9));
1537 assert_eq!(dot.constrained(), dot);
1538 }
1539
1540 const BOUNDS: Size = Size::new(1920, 1080);
1541 const BOUNDS_RECT: Rect = Rect::new(0, 0, BOUNDS.w, BOUNDS.h);
1542
1543 #[test]
1544 fn rect_preview_normalizes_inverted_drag() {
1545 let s = Shape::compute_preview(
1546 ToolKind::Rect,
1547 Point::new(100, 200),
1548 Point::new(40, 50),
1549 BOUNDS_RECT,
1550 false,
1551 );
1552 assert_eq!(s, Some(Shape::Rect(Rect::new(40, 50, 60, 150))));
1553 }
1554
1555 #[test]
1556 fn rect_preview_clamps_cursor_to_bounds() {
1557 let s = Shape::compute_preview(
1558 ToolKind::Rect,
1559 Point::new(1900, 1000),
1560 Point::new(5000, 5000),
1561 BOUNDS_RECT,
1562 false,
1563 );
1564 assert_eq!(s, Some(Shape::Rect(Rect::new(1900, 1000, 19, 79))));
1565 }
1566
1567 #[test]
1568 fn rect_preview_degenerate_is_none() {
1569 assert_eq!(
1570 Shape::compute_preview(
1571 ToolKind::Rect,
1572 Point::new(10, 10),
1573 Point::new(10, 300),
1574 BOUNDS_RECT,
1575 false
1576 ),
1577 None
1578 );
1579 assert_eq!(
1580 Shape::compute_preview(
1581 ToolKind::Rect,
1582 Point::new(10, 10),
1583 Point::new(10, 10),
1584 BOUNDS_RECT,
1585 false
1586 ),
1587 None
1588 );
1589 }
1590
1591 #[test]
1592 fn circle_preview_radius_is_distance() {
1593 let s = Shape::compute_preview(
1594 ToolKind::Circle,
1595 Point::new(100, 100),
1596 Point::new(103, 104),
1597 BOUNDS_RECT,
1598 false,
1599 );
1600 assert_eq!(
1601 s,
1602 Some(Shape::Circle {
1603 cx: 100,
1604 cy: 100,
1605 r: 5
1606 })
1607 );
1608 }
1609
1610 #[test]
1611 fn circle_preview_zero_radius_is_none() {
1612 assert_eq!(
1613 Shape::compute_preview(
1614 ToolKind::Circle,
1615 Point::new(7, 7),
1616 Point::new(7, 7),
1617 BOUNDS_RECT,
1618 false
1619 ),
1620 None
1621 );
1622 }
1623
1624 #[test]
1625 fn rect_hit_test_edges() {
1626 let s = Shape::Rect(Rect::new(10, 10, 20, 20));
1627 assert!(s.hit_test(Point::new(10, 10)));
1628 assert!(s.hit_test(Point::new(29, 29)));
1629 assert!(!s.hit_test(Point::new(30, 30)));
1630 assert!(!s.hit_test(Point::new(9, 10)));
1631 }
1632
1633 #[test]
1634 fn circle_hit_test_boundary_inclusive() {
1635 let s = Shape::Circle {
1636 cx: 0,
1637 cy: 0,
1638 r: 10,
1639 };
1640 assert!(s.hit_test(Point::new(10, 0)));
1641 assert!(s.hit_test(Point::new(6, 8)));
1642 assert!(!s.hit_test(Point::new(8, 8)));
1643 }
1644
1645 #[test]
1646 fn circle_hit_test_survives_extreme_coords() {
1647 let s = Shape::Circle { cx: 0, cy: 0, r: 5 };
1648 assert!(!s.hit_test(Point::new(i32::MAX, i32::MAX)));
1649 }
1650
1651 #[test]
1652 fn bbox_of_circle() {
1653 let s = Shape::Circle {
1654 cx: 50,
1655 cy: 60,
1656 r: 10,
1657 };
1658 assert_eq!(s.bbox(), Rect::new(40, 50, 20, 20));
1659 }
1660
1661 #[test]
1662 fn rect_clamp_move_never_escapes_bounds() {
1663 let s = Shape::Rect(Rect::new(0, 0, 300, 200));
1664 let grab = Point::new(0, 0);
1665 for cx in [-500, 0, 960, 5000] {
1666 for cy in [-500, 0, 540, 5000] {
1667 let Shape::Rect(r) = s.clamp_move(grab, Point::new(cx, cy), BOUNDS_RECT) else {
1668 panic!("rect stayed rect");
1669 };
1670 assert!(r.x >= 0 && r.y >= 0, "({cx},{cy}) gave {r:?}");
1671 assert!(
1672 r.x + r.w <= BOUNDS.w && r.y + r.h <= BOUNDS.h,
1673 "({cx},{cy}) gave {r:?}"
1674 );
1675 }
1676 }
1677 }
1678
1679 #[test]
1680 fn circle_clamp_move_never_escapes_bounds() {
1681 let s = Shape::Circle {
1682 cx: 500,
1683 cy: 500,
1684 r: 40,
1685 };
1686 let grab = Point::new(0, 0);
1687 for cx in [-500, 0, 960, 5000] {
1688 for cy in [-500, 0, 540, 5000] {
1689 let Shape::Circle {
1690 cx: ncx,
1691 cy: ncy,
1692 r,
1693 } = s.clamp_move(grab, Point::new(cx, cy), BOUNDS_RECT)
1694 else {
1695 panic!("circle stayed circle");
1696 };
1697 assert!(
1698 ncx - r >= 0 && ncy - r >= 0,
1699 "({cx},{cy}) gave center ({ncx},{ncy})"
1700 );
1701 assert!(
1702 ncx + r <= BOUNDS.w && ncy + r <= BOUNDS.h,
1703 "({cx},{cy}) gave center ({ncx},{ncy})"
1704 );
1705 }
1706 }
1707 }
1708
1709 #[test]
1710 fn oversized_circle_clamp_is_stable() {
1711 let s = Shape::Circle {
1714 cx: 100,
1715 cy: 100,
1716 r: 2000,
1717 };
1718 let moved = s.clamp_move(Point::new(0, 0), Point::new(0, 0), BOUNDS_RECT);
1719 assert_eq!(
1720 moved,
1721 Shape::Circle {
1722 cx: 2000,
1723 cy: 2000,
1724 r: 2000
1725 }
1726 );
1727 }
1728
1729 #[test]
1730 fn translated_shifts_both_kinds() {
1731 assert_eq!(
1732 Shape::Rect(Rect::new(1, 2, 3, 4)).translated(10, 20),
1733 Shape::Rect(Rect::new(11, 22, 3, 4))
1734 );
1735 assert_eq!(
1736 Shape::Circle { cx: 1, cy: 2, r: 3 }.translated(10, 20),
1737 Shape::Circle {
1738 cx: 11,
1739 cy: 22,
1740 r: 3
1741 }
1742 );
1743 }
1744
1745 #[test]
1746 fn circle_rim_grab_within_tolerance_only() {
1747 let s = Shape::Circle {
1748 cx: 100,
1749 cy: 100,
1750 r: 50,
1751 };
1752 assert_eq!(
1753 s.resize_grab(Point::new(153, 100), 5),
1754 Some(ResizeHandle::CircleRadius)
1755 );
1756 assert_eq!(
1757 s.resize_grab(Point::new(147, 100), 5),
1758 Some(ResizeHandle::CircleRadius)
1759 );
1760 assert_eq!(s.resize_grab(Point::new(100, 100), 5), None); assert_eq!(s.resize_grab(Point::new(160, 100), 5), None); }
1763
1764 #[test]
1765 fn rect_edge_and_corner_grabs() {
1766 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1767 assert_eq!(
1768 s.resize_grab(Point::new(100, 150), 5),
1769 Some(ResizeHandle::RectEdges {
1770 left: true,
1771 right: false,
1772 top: false,
1773 bottom: false
1774 })
1775 );
1776 assert_eq!(
1777 s.resize_grab(Point::new(302, 150), 5), Some(ResizeHandle::RectEdges {
1779 left: false,
1780 right: true,
1781 top: false,
1782 bottom: false
1783 })
1784 );
1785 assert_eq!(
1786 s.resize_grab(Point::new(298, 202), 5), Some(ResizeHandle::RectEdges {
1788 left: false,
1789 right: true,
1790 top: false,
1791 bottom: true
1792 })
1793 );
1794 assert_eq!(s.resize_grab(Point::new(200, 150), 5), None); assert_eq!(s.resize_grab(Point::new(90, 150), 5), None); }
1797
1798 #[test]
1799 fn tiny_rect_grabs_nearer_edge_not_both() {
1800 let s = Shape::Rect(Rect::new(100, 100, 6, 6));
1801 let Some(ResizeHandle::RectEdges { left, right, .. }) =
1802 s.resize_grab(Point::new(101, 103), 5)
1803 else {
1804 panic!("expected an edge grab");
1805 };
1806 assert!(left && !right);
1807 }
1808
1809 #[test]
1810 fn circle_resize_follows_cursor_distance() {
1811 let s = Shape::Circle {
1812 cx: 100,
1813 cy: 100,
1814 r: 50,
1815 };
1816 let resized = s.resize_to(
1817 ResizeHandle::CircleRadius,
1818 Point::new(100, 180),
1819 BOUNDS_RECT,
1820 false,
1821 );
1822 assert_eq!(
1823 resized,
1824 Shape::Circle {
1825 cx: 100,
1826 cy: 100,
1827 r: 80
1828 }
1829 );
1830 let tiny = s.resize_to(
1832 ResizeHandle::CircleRadius,
1833 Point::new(100, 100),
1834 BOUNDS_RECT,
1835 false,
1836 );
1837 assert_eq!(
1838 tiny,
1839 Shape::Circle {
1840 cx: 100,
1841 cy: 100,
1842 r: 2
1843 }
1844 );
1845 }
1846
1847 #[test]
1848 fn rect_corner_resize_anchors_opposite_corner() {
1849 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1850 let handle = ResizeHandle::RectEdges {
1851 left: false,
1852 right: true,
1853 top: false,
1854 bottom: true,
1855 };
1856 let resized = s.resize_to(handle, Point::new(400, 300), BOUNDS_RECT, false);
1857 assert_eq!(resized, Shape::Rect(Rect::new(100, 100, 300, 200)));
1858 }
1859
1860 #[test]
1861 fn rect_edge_resize_moves_one_axis_only() {
1862 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1863 let handle = ResizeHandle::RectEdges {
1864 left: true,
1865 right: false,
1866 top: false,
1867 bottom: false,
1868 };
1869 let resized = s.resize_to(handle, Point::new(50, 999), BOUNDS_RECT, false);
1870 assert_eq!(resized, Shape::Rect(Rect::new(50, 100, 250, 100)));
1871 }
1872
1873 #[test]
1874 fn rect_resize_cannot_invert_or_vanish() {
1875 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1876 let handle = ResizeHandle::RectEdges {
1877 left: true,
1878 right: false,
1879 top: false,
1880 bottom: false,
1881 };
1882 let resized = s.resize_to(handle, Point::new(500, 150), BOUNDS_RECT, false);
1884 assert_eq!(resized, Shape::Rect(Rect::new(298, 100, 2, 100)));
1885 }
1886
1887 #[test]
1888 fn resize_cursor_is_clamped_to_bounds() {
1889 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1890 let handle = ResizeHandle::RectEdges {
1891 left: false,
1892 right: true,
1893 top: false,
1894 bottom: false,
1895 };
1896 let resized = s.resize_to(handle, Point::new(99_999, 150), BOUNDS_RECT, false);
1897 assert_eq!(
1898 resized,
1899 Shape::Rect(Rect::new(100, 100, BOUNDS.w - 1 - 100, 100))
1900 );
1901 }
1902
1903 #[test]
1904 fn locked_corner_resize_keeps_ratio_dominant_axis_wins() {
1905 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1908 let corner = ResizeHandle::RectEdges {
1909 left: false,
1910 right: true,
1911 top: false,
1912 bottom: true,
1913 };
1914 let resized = s.resize_to(corner, Point::new(400, 300), BOUNDS_RECT, true);
1915 assert_eq!(resized, Shape::Rect(Rect::new(100, 100, 400, 200)));
1916 }
1917
1918 #[test]
1919 fn locked_corner_resize_anchors_the_opposite_corner() {
1920 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1922 let corner = ResizeHandle::RectEdges {
1923 left: true,
1924 right: false,
1925 top: true,
1926 bottom: false,
1927 };
1928 let resized = s.resize_to(corner, Point::new(0, 80), BOUNDS_RECT, true);
1929 let Shape::Rect(r) = resized else {
1930 panic!("still a rect")
1931 };
1932 assert_eq!((r.x + r.w, r.y + r.h), (300, 200), "anchor moved");
1933 assert_eq!(r.w * 100, r.h * 200, "ratio drifted: {r:?}");
1934 }
1935
1936 #[test]
1937 fn locked_corner_resize_caps_scale_at_bounds() {
1938 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1942 let corner = ResizeHandle::RectEdges {
1943 left: false,
1944 right: true,
1945 top: false,
1946 bottom: true,
1947 };
1948 let resized = s.resize_to(
1949 corner,
1950 Point::new(BOUNDS_RECT.w - 1, BOUNDS_RECT.h - 1),
1951 BOUNDS_RECT,
1952 true,
1953 );
1954 let Shape::Rect(r) = resized else {
1955 panic!("still a rect")
1956 };
1957 assert!(
1958 r.x + r.w <= BOUNDS.w && r.y + r.h <= BOUNDS.h,
1959 "escaped: {r:?}"
1960 );
1961 assert_eq!(r.w, BOUNDS.w - 100);
1962 assert_eq!(r.w, 2 * r.h);
1963 }
1964
1965 #[test]
1966 fn locked_edge_resize_scales_other_axis_centered() {
1967 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1970 let edge = ResizeHandle::RectEdges {
1971 left: false,
1972 right: true,
1973 top: false,
1974 bottom: false,
1975 };
1976 let resized = s.resize_to(edge, Point::new(500, 150), BOUNDS_RECT, true);
1977 assert_eq!(resized, Shape::Rect(Rect::new(100, 50, 400, 200)));
1978 }
1979
1980 #[test]
1981 fn locked_edge_resize_clamps_centered_axis_to_bounds() {
1982 let s = Shape::Rect(Rect::new(100, 10, 200, 100));
1985 let edge = ResizeHandle::RectEdges {
1986 left: false,
1987 right: true,
1988 top: false,
1989 bottom: false,
1990 };
1991 let resized = s.resize_to(edge, Point::new(500, 60), BOUNDS_RECT, true);
1992 let Shape::Rect(r) = resized else {
1993 panic!("still a rect")
1994 };
1995 assert_eq!((r.w, r.h), (400, 200));
1996 assert_eq!(r.y, 0, "clamped to the top edge");
1997 }
1998
1999 #[test]
2000 fn locked_circle_resize_is_unchanged_by_lock() {
2001 let s = Shape::Circle {
2002 cx: 100,
2003 cy: 100,
2004 r: 50,
2005 };
2006 let unlocked = s.resize_to(
2007 ResizeHandle::CircleRadius,
2008 Point::new(100, 180),
2009 BOUNDS_RECT,
2010 false,
2011 );
2012 let locked = s.resize_to(
2013 ResizeHandle::CircleRadius,
2014 Point::new(100, 180),
2015 BOUNDS_RECT,
2016 true,
2017 );
2018 assert_eq!(unlocked, locked);
2019 }
2020
2021 #[test]
2022 fn mismatched_handle_is_inert() {
2023 let s = Shape::Circle { cx: 5, cy: 5, r: 5 };
2024 let handle = ResizeHandle::RectEdges {
2025 left: true,
2026 right: false,
2027 top: false,
2028 bottom: false,
2029 };
2030 assert_eq!(
2031 s.resize_to(handle, Point::new(50, 50), BOUNDS_RECT, false),
2032 s
2033 );
2034 }
2035
2036 #[test]
2037 fn ellipse_preview_inscribes_the_drag_box_and_shift_locks_a_circle() {
2038 let free = Shape::compute_preview(
2039 ToolKind::Ellipse,
2040 Point::new(10, 10),
2041 Point::new(50, 30),
2042 BOUNDS_RECT,
2043 false,
2044 );
2045 assert_eq!(
2046 free,
2047 Some(Shape::Ellipse {
2048 cx: 30,
2049 cy: 20,
2050 rx: 20,
2051 ry: 10,
2052 })
2053 );
2054 let locked = Shape::compute_preview(
2055 ToolKind::Ellipse,
2056 Point::new(10, 10),
2057 Point::new(50, 30),
2058 BOUNDS_RECT,
2059 true,
2060 );
2061 assert_eq!(
2062 locked,
2063 Some(Shape::Ellipse {
2064 cx: 30,
2065 cy: 20,
2066 rx: 10,
2067 ry: 10,
2068 }),
2069 "Shift inscribes the circle instead"
2070 );
2071 }
2072
2073 #[test]
2074 fn ellipse_hit_test_is_boundary_inclusive_and_excludes_bbox_corners() {
2075 let e = Shape::Ellipse {
2076 cx: 50,
2077 cy: 40,
2078 rx: 30,
2079 ry: 10,
2080 };
2081 assert!(e.hit_test(Point::new(50, 40)));
2082 assert!(e.hit_test(Point::new(80, 40)), "rx vertex inclusive");
2083 assert!(e.hit_test(Point::new(50, 30)), "ry vertex inclusive");
2084 assert!(!e.hit_test(Point::new(80, 30)), "bbox corner outside");
2085 assert!(!e.hit_test(Point::new(81, 40)));
2086 assert_eq!(e.bbox(), Rect::new(20, 30, 60, 20));
2087 }
2088
2089 #[test]
2090 fn ellipse_resize_rides_its_bounding_box() {
2091 let e = Shape::Ellipse {
2092 cx: 50,
2093 cy: 40,
2094 rx: 20,
2095 ry: 10,
2096 };
2097 let handle = e.resize_grab(Point::new(70, 40), 2).expect("edge grab");
2099 let resized = e.resize_to(handle, Point::new(90, 40), BOUNDS_RECT, false);
2100 assert_eq!(
2101 resized,
2102 Shape::Ellipse {
2103 cx: 60,
2104 cy: 40,
2105 rx: 30,
2106 ry: 10,
2107 },
2108 "left edge anchored, rx grew"
2109 );
2110 }
2111
2112 #[test]
2113 fn rotated_ellipse_hit_follows_the_turn() {
2114 let e = Shape::Ellipse {
2115 cx: 50,
2116 cy: 40,
2117 rx: 30,
2118 ry: 8,
2119 };
2120 assert!(e.hit_test_rotated(90, Point::new(50, 65)));
2122 assert!(!e.hit_test_rotated(90, Point::new(75, 40)));
2123 assert!(e.hit_test(Point::new(75, 40)), "unrotated it lies flat");
2124 }
2125
2126 #[test]
2127 fn a_crescents_click_point_is_inside_it_not_in_the_hollow() {
2128 let c = Shape::Poly {
2131 points: vec![
2132 Point::new(0, 0),
2133 Point::new(100, 0),
2134 Point::new(100, 20),
2135 Point::new(30, 20),
2136 Point::new(30, 80),
2137 Point::new(100, 80),
2138 Point::new(100, 100),
2139 Point::new(0, 100),
2140 ],
2141 };
2142 let p = c.click_point();
2143 assert!(c.hit_test(p), "{p:?} landed outside the crescent");
2144 }
2145
2146 #[test]
2147 fn a_click_point_is_interior_for_every_awkward_polygon() {
2148 let shapes = [
2149 vec![
2151 Point::new(0, 0),
2152 Point::new(10, 0),
2153 Point::new(100, 90),
2154 Point::new(100, 100),
2155 Point::new(90, 100),
2156 Point::new(0, 10),
2157 ],
2158 vec![
2160 Point::new(0, 0),
2161 Point::new(20, 60),
2162 Point::new(40, 0),
2163 Point::new(60, 60),
2164 Point::new(80, 0),
2165 Point::new(80, 10),
2166 Point::new(60, 70),
2167 Point::new(40, 10),
2168 Point::new(20, 70),
2169 Point::new(0, 10),
2170 ],
2171 vec![
2173 Point::new(0, 0),
2174 Point::new(500, 0),
2175 Point::new(500, 1),
2176 Point::new(0, 1),
2177 ],
2178 ];
2179 for points in shapes {
2180 let shape = Shape::Poly {
2181 points: points.clone(),
2182 };
2183 let p = shape.click_point();
2184 assert!(shape.hit_test(p), "{p:?} outside {points:?}");
2185 }
2186 }
2187
2188 #[test]
2189 fn a_click_point_does_not_scan_the_bounding_box() {
2190 let wide = Shape::Poly {
2194 points: vec![
2195 Point::new(1_000_000, 0),
2196 Point::new(500_000, 750_000),
2197 Point::new(0, 375_000),
2198 Point::new(500_000, 650_000),
2199 Point::new(940_000, 90_000),
2200 ],
2201 };
2202 let started = std::time::Instant::now();
2203 let p = wide.click_point();
2204 assert!(wide.hit_test(p), "{p:?} outside");
2205 assert!(
2206 started.elapsed() < std::time::Duration::from_millis(50),
2207 "took {:?} — the bounding-box scan is back",
2208 started.elapsed()
2209 );
2210 }
2211
2212 #[test]
2213 fn point_in_poly_handles_concave_shapes_edges_included() {
2214 let u = vec![
2216 Point::new(0, 0),
2217 Point::new(10, 0),
2218 Point::new(10, 30),
2219 Point::new(20, 30),
2220 Point::new(20, 0),
2221 Point::new(30, 0),
2222 Point::new(30, 40),
2223 Point::new(0, 40),
2224 ];
2225 let shape = Shape::Poly { points: u };
2226 assert!(shape.hit_test(Point::new(5, 20)), "left arm");
2227 assert!(shape.hit_test(Point::new(25, 20)), "right arm");
2228 assert!(shape.hit_test(Point::new(15, 35)), "base");
2229 assert!(!shape.hit_test(Point::new(15, 10)), "the notch is outside");
2230 assert!(shape.hit_test(Point::new(0, 0)), "vertex inclusive");
2231 assert!(shape.hit_test(Point::new(5, 0)), "edge inclusive");
2232 assert!(!shape.hit_test(Point::new(-1, 20)));
2233 assert!(shape.hit_test(shape.click_point()));
2235 }
2236
2237 #[test]
2238 fn regular_polygon_puts_the_first_vertex_at_the_cursor() {
2239 let hex = regular_polygon(Point::new(100, 100), Point::new(140, 100), 6);
2240 let Shape::Poly { ref points } = hex else {
2241 panic!("regular polygon is a poly")
2242 };
2243 assert_eq!(points.len(), 6);
2244 assert_eq!(points[0], Point::new(140, 100), "first vertex at cursor");
2245 for p in points {
2246 let d = f64::from(p.x - 100).hypot(f64::from(p.y - 100));
2247 assert!((d - 40.0).abs() < 1.5, "vertex {p:?} off the radius: {d}");
2248 }
2249 let tri = regular_polygon(Point::new(0, 0), Point::new(10, 0), 1);
2251 let Shape::Poly { points } = tri else {
2252 panic!()
2253 };
2254 assert_eq!(points.len(), 3);
2255 }
2256
2257 #[test]
2258 fn simplify_path_drops_jitter_and_keeps_corners() {
2259 let path: Vec<Point> = (0..=20)
2262 .map(|x| Point::new(x * 5, i32::from(x % 2 != 0)))
2263 .chain((1..=10).map(|y| Point::new(100, y * 5)))
2264 .collect();
2265 let simplified = simplify_path(&path, 2.0);
2266 assert!(
2267 simplified.len() <= 5,
2268 "expected a handful of points, got {}",
2269 simplified.len()
2270 );
2271 assert_eq!(*simplified.first().unwrap(), Point::new(0, 0));
2272 assert_eq!(*simplified.last().unwrap(), Point::new(100, 50));
2273 assert!(
2274 simplified.contains(&Point::new(100, 1)) || simplified.contains(&Point::new(100, 0)),
2275 "the corner survives: {simplified:?}"
2276 );
2277 }
2278
2279 #[test]
2280 fn poly_moves_resizes_and_rotates_like_any_shape() {
2281 let square = Shape::Poly {
2282 points: vec![
2283 Point::new(10, 10),
2284 Point::new(30, 10),
2285 Point::new(30, 30),
2286 Point::new(10, 30),
2287 ],
2288 };
2289 assert_eq!(square.bbox(), Rect::new(10, 10, 20, 20));
2290 let moved = square.translated(5, -5);
2291 assert_eq!(moved.bbox(), Rect::new(15, 5, 20, 20));
2292 let handle = square.resize_grab(Point::new(30, 20), 2).expect("edge");
2294 let grown = square.resize_to(handle, Point::new(50, 20), BOUNDS_RECT, false);
2295 assert_eq!(grown.bbox(), Rect::new(10, 10, 40, 20));
2296 let turned = square.with_rotation_baked(90);
2298 assert_eq!(turned.bbox(), square.bbox(), "square is 90-symmetric");
2299 assert!(matches!(turned, Shape::Poly { .. }));
2300 }
2301
2302 #[test]
2303 fn click_point_centers_each_kind() {
2304 assert_eq!(
2305 Shape::Rect(Rect::new(10, 20, 30, 40)).click_point(),
2306 Point::new(25, 40)
2307 );
2308 assert_eq!(
2309 Shape::Circle { cx: 5, cy: 6, r: 7 }.click_point(),
2310 Point::new(5, 6)
2311 );
2312 let tri = Shape::Triangle {
2313 ax: 30,
2314 ay: 0,
2315 bx: 0,
2316 by: 60,
2317 cx: 60,
2318 cy: 60,
2319 };
2320 assert_eq!(tri.click_point(), Point::new(30, 40));
2321 assert!(tri.hit_test(tri.click_point()));
2322 let rect = Shape::Rect(Rect::new(10, 10, 40, 10));
2325 assert!(rect.hit_test_rotated(90, rect.click_point()));
2326 }
2327
2328 #[test]
2329 fn clamp_point_lands_inside_and_leaves_interior_points_alone() {
2330 let r = Rect::new(10, 20, 30, 40);
2331 let inside = Point::new(15, 25);
2332 assert_eq!(r.clamp_point(inside), inside);
2333 assert_eq!(r.clamp_point(Point::new(100, 100)), Point::new(39, 59));
2335 assert_eq!(r.clamp_point(Point::new(-5, -5)), Point::new(10, 20));
2336 for p in [
2337 Point::new(100, 100),
2338 Point::new(-5, -5),
2339 Point::new(15, 900),
2340 ] {
2341 assert!(r.contains(r.clamp_point(p)));
2342 }
2343 }
2344
2345 #[test]
2346 fn clamp_point_on_a_zero_sized_rect_gives_the_origin_corner() {
2347 let r = Rect::new(7, 9, 0, 0);
2348 assert_eq!(r.clamp_point(Point::new(100, 100)), Point::new(7, 9));
2349 }
2350
2351 #[test]
2352 fn line_bbox_spans_both_endpoints_in_any_direction() {
2353 let down = Line::new(Point::new(10, 20), Point::new(40, 60));
2354 let up = Line::new(Point::new(40, 60), Point::new(10, 20));
2355 assert_eq!(down.bbox(), Rect::new(10, 20, 30, 40));
2356 assert_eq!(up.bbox(), down.bbox());
2357 let dot = Line::new(Point::new(5, 5), Point::new(5, 5));
2359 assert_eq!(dot.bbox(), Rect::new(5, 5, 0, 0));
2360 }
2361
2362 #[test]
2363 fn tool_kind_cycles_through_the_drawing_tools() {
2364 assert_eq!(ToolKind::Rect.next(), ToolKind::Ellipse);
2365 assert_eq!(ToolKind::Ellipse.next(), ToolKind::Triangle);
2366 assert_eq!(ToolKind::Triangle.next(), ToolKind::Polygon);
2367 assert_eq!(ToolKind::Polygon.next(), ToolKind::Freehand);
2368 assert_eq!(ToolKind::Freehand.next(), ToolKind::Measure);
2369 assert_eq!(ToolKind::Measure.next(), ToolKind::Rect);
2370 assert_eq!(ToolKind::Circle.next(), ToolKind::Triangle);
2372 assert_eq!(ToolKind::Poly.next(), ToolKind::Rect);
2373 }
2374
2375 #[test]
2376 fn triangle_preview_is_apex_top_center_in_drag_box() {
2377 let s = Shape::compute_preview(
2378 ToolKind::Triangle,
2379 Point::new(100, 100),
2380 Point::new(300, 200),
2381 BOUNDS_RECT,
2382 false,
2383 );
2384 assert_eq!(
2385 s,
2386 Some(Shape::Triangle {
2387 ax: 200,
2388 ay: 100,
2389 bx: 100,
2390 by: 200,
2391 cx: 300,
2392 cy: 200,
2393 })
2394 );
2395 }
2396
2397 #[test]
2398 fn triangle_hit_test_excludes_bbox_corners() {
2399 let tri = Shape::Triangle {
2400 ax: 200,
2401 ay: 100,
2402 bx: 100,
2403 by: 200,
2404 cx: 300,
2405 cy: 200,
2406 };
2407 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))); }
2413
2414 #[test]
2415 fn triangle_bbox_and_move_clamp() {
2416 let tri = Shape::Triangle {
2417 ax: 200,
2418 ay: 100,
2419 bx: 100,
2420 by: 200,
2421 cx: 300,
2422 cy: 200,
2423 };
2424 assert_eq!(tri.bbox(), Rect::new(100, 100, 200, 100));
2425 let moved = tri.clamp_move(Point::new(0, 0), Point::new(-500, -500), BOUNDS_RECT);
2428 assert_eq!(moved.bbox(), Rect::new(0, 0, 200, 100));
2429 assert_eq!(
2430 moved,
2431 Shape::Triangle {
2432 ax: 100,
2433 ay: 0,
2434 bx: 0,
2435 by: 100,
2436 cx: 200,
2437 cy: 100,
2438 }
2439 );
2440 }
2441
2442 #[test]
2443 fn triangle_resize_scales_vertices_into_new_bbox() {
2444 let tri = Shape::Triangle {
2445 ax: 200,
2446 ay: 100,
2447 bx: 100,
2448 by: 200,
2449 cx: 300,
2450 cy: 200,
2451 };
2452 let handle = ResizeHandle::RectEdges {
2454 left: false,
2455 right: true,
2456 top: false,
2457 bottom: true,
2458 };
2459 let resized = tri.resize_to(handle, Point::new(500, 300), BOUNDS_RECT, false);
2460 assert_eq!(
2461 resized,
2462 Shape::Triangle {
2463 ax: 300,
2464 ay: 100,
2465 bx: 100,
2466 by: 300,
2467 cx: 500,
2468 cy: 300,
2469 }
2470 );
2471 }
2472
2473 #[test]
2474 fn triangle_resize_grab_is_on_the_bbox_border() {
2475 let tri = Shape::Triangle {
2476 ax: 200,
2477 ay: 100,
2478 bx: 100,
2479 by: 200,
2480 cx: 300,
2481 cy: 200,
2482 };
2483 assert_eq!(
2485 tri.resize_grab(Point::new(150, 100), 5),
2486 Some(ResizeHandle::RectEdges {
2487 left: false,
2488 right: false,
2489 top: true,
2490 bottom: false
2491 })
2492 );
2493 assert_eq!(tri.resize_grab(Point::new(200, 150), 5), None); }
2495
2496 #[test]
2497 fn degenerate_triangles_cover_nothing() {
2498 let point = Shape::Triangle {
2499 ax: 0,
2500 ay: 0,
2501 bx: 0,
2502 by: 0,
2503 cx: 0,
2504 cy: 0,
2505 };
2506 assert!(!point.hit_test(Point::new(500, 500)));
2507 assert!(!point.hit_test(Point::new(0, 0)));
2508 let line = Shape::Triangle {
2509 ax: 0,
2510 ay: 0,
2511 bx: 10,
2512 by: 10,
2513 cx: 20,
2514 cy: 20,
2515 };
2516 assert!(!line.hit_test(Point::new(400, 400)));
2517 assert!(!line.hit_test(Point::new(5, 5)));
2518 }
2519
2520 #[test]
2521 fn extreme_shapes_do_not_panic() {
2522 let huge = Shape::Circle {
2523 cx: 0,
2524 cy: 0,
2525 r: 2_000_000_000,
2526 };
2527 let bb = huge.bbox();
2528 assert!(bb.w > 0);
2529 let far = Shape::Rect(Rect::new(
2530 2_000_000_000,
2531 2_000_000_000,
2532 400_000_000,
2533 400_000_000,
2534 ));
2535 let _ = far.rotated_bbox(45);
2536 }
2537
2538 #[test]
2539 fn resize_of_sub_min_rect_stays_in_bounds() {
2540 let s = Shape::Rect(Rect::new(0, 0, 1, 100));
2543 let handle = ResizeHandle::RectEdges {
2544 left: true,
2545 right: false,
2546 top: false,
2547 bottom: false,
2548 };
2549 let Shape::Rect(r) = s.resize_to(handle, Point::new(0, 50), BOUNDS_RECT, false) else {
2550 panic!("still a rect")
2551 };
2552 assert!(r.x >= 0, "escaped left: {r:?}");
2553 let s = Shape::Rect(Rect::new(BOUNDS.w - 1, 0, 1, 100));
2555 let handle = ResizeHandle::RectEdges {
2556 left: false,
2557 right: true,
2558 top: false,
2559 bottom: false,
2560 };
2561 let Shape::Rect(r) = s.resize_to(
2562 handle,
2563 Point::new(BOUNDS_RECT.w - 1, 50),
2564 BOUNDS_RECT,
2565 false,
2566 ) else {
2567 panic!("still a rect")
2568 };
2569 assert!(r.x + r.w <= BOUNDS.w, "escaped right: {r:?}");
2570 }
2571
2572 #[test]
2573 fn rotated_resize_never_moves_the_anchored_edge() {
2574 let s = Shape::Rect(Rect::new(800, 500, 200, 100));
2578 let handle = ResizeHandle::RectEdges {
2579 left: false,
2580 right: true,
2581 top: false,
2582 bottom: false,
2583 };
2584 let Shape::Rect(r) =
2585 s.resize_to_rotated(45, handle, Point::new(1900, 1000), BOUNDS_RECT, false)
2586 else {
2587 panic!("still a rect")
2588 };
2589 assert_eq!(r.x, 800, "anchored left edge moved");
2590 assert_eq!(r.y, 500, "anchored top edge moved");
2591 }
2592
2593 #[test]
2594 fn resize_of_offscreen_local_box_does_not_teleport() {
2595 let s = Shape::Rect(Rect::new(-90, 0, 200, 20));
2599 let handle = ResizeHandle::RectEdges {
2600 left: false,
2601 right: true,
2602 top: false,
2603 bottom: false,
2604 };
2605 let Shape::Rect(r) = s.resize_to(handle, Point::new(120, 10), BOUNDS_RECT, false) else {
2606 panic!("still a rect")
2607 };
2608 assert_eq!(r.x, -90, "shape teleported");
2609 assert_eq!(r.w, 210);
2610 }
2611
2612 #[test]
2613 fn rotated_resize_tracks_cursor_at_screen_edge() {
2614 let s = Shape::Rect(Rect::new(800, 500, 200, 100));
2618 let handle = ResizeHandle::RectEdges {
2619 left: false,
2620 right: true,
2621 top: false,
2622 bottom: false,
2623 };
2624 let r45 = s.resize_to_rotated(45, handle, Point::new(99_999, 99_999), BOUNDS_RECT, false);
2625 assert_ne!(r45, s);
2627 }
2628
2629 #[test]
2630 fn rotate_point_quarter_turn() {
2631 let center = Point::new(100, 100);
2632 assert_eq!(
2634 rotate_point_about(Point::new(110, 100), center, 90),
2635 Point::new(100, 110)
2636 );
2637 assert_eq!(
2638 rotate_point_about(Point::new(110, 100), center, -90),
2639 Point::new(100, 90)
2640 );
2641 assert_eq!(
2642 rotate_point_about(Point::new(110, 100), center, 360),
2643 Point::new(110, 100)
2644 );
2645 }
2646
2647 #[test]
2648 fn normalize_deg_wraps_into_range() {
2649 assert_eq!(normalize_deg(0), 0);
2650 assert_eq!(normalize_deg(-1), 359);
2651 assert_eq!(normalize_deg(360), 0);
2652 assert_eq!(normalize_deg(725), 5);
2653 }
2654
2655 #[test]
2656 fn rotated_bbox_of_quarter_turned_rect_swaps_dimensions() {
2657 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
2658 let bb = s.rotated_bbox(90);
2659 assert_eq!((bb.w, bb.h), (100, 200));
2660 assert_eq!(bb.x + bb.w / 2, 200);
2662 assert_eq!(bb.y + bb.h / 2, 150);
2663 assert_eq!(s.rotated_bbox(0), s.bbox());
2665 let c = Shape::Circle {
2666 cx: 50,
2667 cy: 50,
2668 r: 20,
2669 };
2670 assert_eq!(c.rotated_bbox(45), c.bbox());
2671 }
2672
2673 #[test]
2674 fn rotated_hit_test_follows_the_turned_shape() {
2675 let s = Shape::Rect(Rect::new(100, 100, 200, 20));
2679 assert!(s.hit_test_rotated(90, Point::new(200, 30)));
2680 assert!(!s.hit_test_rotated(90, Point::new(290, 110)));
2681 assert!(s.hit_test_rotated(0, Point::new(290, 110)));
2682 }
2683
2684 #[test]
2685 fn rotated_resize_grab_finds_the_visual_edge() {
2686 let s = Shape::Rect(Rect::new(100, 100, 200, 20));
2688 assert!(s.resize_grab_rotated(90, Point::new(190, 110), 5).is_some());
2690 assert!(s.resize_grab_rotated(90, Point::new(150, 110), 5).is_none());
2691 }
2692
2693 #[test]
2694 fn baked_triangle_rotates_vertices_others_unchanged() {
2695 let tri = Shape::Triangle {
2696 ax: 200,
2697 ay: 100,
2698 bx: 100,
2699 by: 200,
2700 cx: 300,
2701 cy: 200,
2702 };
2703 let baked = tri.with_rotation_baked(180);
2704 assert_eq!(
2706 baked,
2707 Shape::Triangle {
2708 ax: 200,
2709 ay: 200,
2710 bx: 300,
2711 by: 100,
2712 cx: 100,
2713 cy: 100,
2714 }
2715 );
2716 let rect = Shape::Rect(Rect::new(1, 2, 3, 4));
2717 assert_eq!(rect.with_rotation_baked(90), rect);
2718 assert_eq!(tri.with_rotation_baked(0), tri);
2719 }
2720
2721 #[test]
2722 fn triangle_serde_is_distinct_from_rect_and_circle() {
2723 let tri = Shape::Triangle {
2724 ax: 1,
2725 ay: 2,
2726 bx: 3,
2727 by: 4,
2728 cx: 5,
2729 cy: 6,
2730 };
2731 let json = serde_json::to_string(&tri).unwrap();
2732 let back: Shape = serde_json::from_str(&json).unwrap();
2733 assert_eq!(back, tri);
2734 let rect: Shape = serde_json::from_str(r#"{"x":1,"y":2,"w":3,"h":4}"#).unwrap();
2736 assert_eq!(rect, Shape::Rect(Rect::new(1, 2, 3, 4)));
2737 let circle: Shape = serde_json::from_str(r#"{"cx":1,"cy":2,"r":3}"#).unwrap();
2738 assert_eq!(circle, Shape::Circle { cx: 1, cy: 2, r: 3 });
2739 }
2740
2741 #[test]
2742 fn a_triangle_grabs_from_its_bbox_origin() {
2743 let tri = Shape::Triangle {
2744 ax: 50,
2745 ay: 10,
2746 bx: 20,
2747 by: 70,
2748 cx: 80,
2749 cy: 70,
2750 };
2751 assert_eq!(tri.grab_origin(), Point::new(20, 10));
2752 }
2753
2754 #[test]
2755 fn a_rotated_move_clamps_the_rotated_box_to_bounds() {
2756 let rect = Shape::Rect(Rect::new(10, 10, 40, 20));
2757 let bounds = Size::new(200, 200);
2758 let moved = rect.clamp_move_rotated(
2761 45,
2762 Point::new(0, 0),
2763 Point::new(500, 500),
2764 Rect::new(0, 0, bounds.w, bounds.h),
2765 );
2766 let bb = moved.rotated_bbox(45);
2767 assert!(bb.x >= 0 && bb.y >= 0, "{bb:?}");
2768 assert!(bb.x + bb.w <= bounds.w, "{bb:?}");
2769 assert!(bb.y + bb.h <= bounds.h, "{bb:?}");
2770 }
2771
2772 #[test]
2773 fn a_rotated_grab_references_the_rotated_box_origin() {
2774 let rect = Shape::Rect(Rect::new(10, 10, 40, 20));
2775 assert_eq!(rect.grab_origin_rotated(0), rect.grab_origin());
2776 let rotated = rect.grab_origin_rotated(45);
2777 assert_eq!(
2778 rotated,
2779 Point::new(rect.rotated_bbox(45).x, rect.rotated_bbox(45).y)
2780 );
2781 let circle = Shape::Circle {
2783 cx: 40,
2784 cy: 40,
2785 r: 9,
2786 };
2787 assert_eq!(circle.grab_origin_rotated(30), circle.grab_origin());
2788 }
2789
2790 #[test]
2791 fn min3_and_max3_pick_each_position() {
2792 assert_eq!(min3(1, 2, 3), 1);
2793 assert_eq!(min3(2, 1, 3), 1);
2794 assert_eq!(min3(3, 2, 1), 1);
2795 assert_eq!(max3(3, 2, 1), 3);
2796 assert_eq!(max3(1, 3, 2), 3);
2797 assert_eq!(max3(1, 2, 3), 3);
2798 }
2799
2800 #[test]
2801 fn a_proportional_vertical_edge_resize_keeps_the_aspect() {
2802 let rect = Shape::Rect(Rect::new(20, 20, 40, 20));
2804 let resized = rect.resize_to_rotated(
2805 0,
2806 ResizeHandle::RectEdges {
2807 left: false,
2808 right: false,
2809 top: true,
2810 bottom: false,
2811 },
2812 Point::new(30, 0),
2813 Rect::new(0, 0, 300, 300),
2814 true,
2815 );
2816 let bb = resized.bbox();
2817 assert!(bb.w >= 2 && bb.h >= 2, "{bb:?}");
2818 assert!(bb.x >= 0 && bb.x + bb.w <= 300, "{bb:?}");
2819 }
2820}