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
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ToolKind {
54 Rect,
55 Circle,
58 Ellipse,
59 Triangle,
60 Polygon,
62 Freehand,
64 Poly,
67}
68
69impl ToolKind {
70 #[must_use]
71 pub const fn next(self) -> Self {
72 match self {
73 Self::Rect => Self::Ellipse,
74 Self::Circle | Self::Ellipse => Self::Triangle,
75 Self::Triangle => Self::Polygon,
76 Self::Polygon => Self::Freehand,
77 Self::Freehand | Self::Poly => Self::Rect,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ResizeHandle {
85 CircleRadius,
87 RectEdges {
89 left: bool,
90 right: bool,
91 top: bool,
92 bottom: bool,
93 },
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(untagged)]
106pub enum Shape {
107 Rect(Rect),
108 Circle {
109 cx: i32,
110 cy: i32,
111 r: i32,
112 },
113 Ellipse {
117 cx: i32,
118 cy: i32,
119 rx: i32,
120 ry: i32,
121 },
122 Triangle {
123 ax: i32,
124 ay: i32,
125 bx: i32,
126 by: i32,
127 cx: i32,
128 cy: i32,
129 },
130 Poly {
134 points: Vec<Point>,
135 },
136}
137
138impl Shape {
139 pub fn compute_preview(
145 tool: ToolKind,
146 start: Point,
147 current: Point,
148 region: Rect,
149 lock: bool,
150 ) -> Option<Self> {
151 let cx = current.x.clamp(region.x, region.x + region.w - 1);
156 let cy = current.y.clamp(region.y, region.y + region.h - 1);
157 match tool {
158 ToolKind::Rect | ToolKind::Triangle | ToolKind::Ellipse => {
159 let x = start.x.min(cx);
160 let y = start.y.min(cy);
161 let w = (start.x - cx).abs();
162 let h = (start.y - cy).abs();
163 if w <= 1 || h <= 1 {
164 return None;
165 }
166 let bbox = Rect::new(x, y, w, h);
167 Some(match tool {
168 ToolKind::Rect => Self::Rect(bbox),
169 ToolKind::Ellipse => ellipse_in_box(bbox, lock),
170 _ => triangle_in_box(bbox),
171 })
172 }
173 ToolKind::Circle => {
174 let dx = f64::from(start.x - cx);
175 let dy = f64::from(start.y - cy);
176 let r = dx.hypot(dy) as i32;
177 if r <= 0 {
178 return None;
179 }
180 Some(Self::Circle {
181 cx: start.x,
182 cy: start.y,
183 r,
184 })
185 }
186 ToolKind::Polygon | ToolKind::Freehand | ToolKind::Poly => None,
191 }
192 }
193
194 pub const fn kind(&self) -> ToolKind {
195 match self {
196 Self::Rect(_) => ToolKind::Rect,
197 Self::Circle { .. } => ToolKind::Circle,
198 Self::Ellipse { .. } => ToolKind::Ellipse,
199 Self::Triangle { .. } => ToolKind::Triangle,
200 Self::Poly { .. } => ToolKind::Poly,
201 }
202 }
203
204 pub fn bbox(&self) -> Rect {
207 match *self {
208 Self::Poly { ref points } => {
209 let mut x0 = i32::MAX;
210 let mut y0 = i32::MAX;
211 let mut x1 = i32::MIN;
212 let mut y1 = i32::MIN;
213 for p in points {
214 x0 = x0.min(p.x);
215 y0 = y0.min(p.y);
216 x1 = x1.max(p.x);
217 y1 = y1.max(p.y);
218 }
219 if points.is_empty() {
220 return Rect::new(0, 0, 0, 0);
221 }
222 Rect::new(x0, y0, x1.saturating_sub(x0), y1.saturating_sub(y0))
223 }
224 Self::Rect(r) => r,
225 Self::Ellipse { cx, cy, rx, ry } => Rect::new(
226 cx.saturating_sub(rx),
227 cy.saturating_sub(ry),
228 rx.saturating_mul(2),
229 ry.saturating_mul(2),
230 ),
231 Self::Circle { cx, cy, r } => Rect::new(
232 cx.saturating_sub(r),
233 cy.saturating_sub(r),
234 r.saturating_mul(2),
235 r.saturating_mul(2),
236 ),
237 Self::Triangle {
238 ax,
239 ay,
240 bx,
241 by,
242 cx,
243 cy,
244 } => {
245 let x0 = min3(ax, bx, cx);
246 let y0 = min3(ay, by, cy);
247 Rect::new(
248 x0,
249 y0,
250 max3(ax, bx, cx).saturating_sub(x0),
251 max3(ay, by, cy).saturating_sub(y0),
252 )
253 }
254 }
255 }
256
257 pub fn hit_test(&self, p: Point) -> bool {
260 match *self {
261 Self::Poly { ref points } => point_in_poly(points, p),
262 Self::Rect(r) => r.contains(p),
263 Self::Ellipse { cx, cy, rx, ry } => {
264 let dx = i128::from(p.x - cx);
267 let dy = i128::from(p.y - cy);
268 let rx = i128::from(rx);
269 let ry = i128::from(ry);
270 dx * dx * ry * ry + dy * dy * rx * rx <= rx * rx * ry * ry
271 }
272 Self::Circle { cx, cy, r } => {
273 let dx = i64::from(p.x - cx);
274 let dy = i64::from(p.y - cy);
275 dx * dx + dy * dy <= i64::from(r) * i64::from(r)
276 }
277 Self::Triangle {
278 ax,
279 ay,
280 bx,
281 by,
282 cx,
283 cy,
284 } => {
285 if cross(cx, cy, ax, ay, bx, by) == 0 {
288 return false;
289 }
290 let d1 = cross(p.x, p.y, ax, ay, bx, by);
293 let d2 = cross(p.x, p.y, bx, by, cx, cy);
294 let d3 = cross(p.x, p.y, cx, cy, ax, ay);
295 let has_neg = d1 < 0 || d2 < 0 || d3 < 0;
296 let has_pos = d1 > 0 || d2 > 0 || d3 > 0;
297 !(has_neg && has_pos)
298 }
299 }
300 }
301
302 pub fn covers(&self, x: i32, y: i32) -> bool {
305 self.hit_test(Point::new(x, y))
306 }
307
308 pub fn click_point(&self) -> Point {
314 match *self {
315 Self::Poly { ref points } => poly_interior_point(points),
316 Self::Rect(_) => self.pivot(),
317 Self::Circle { cx, cy, .. } | Self::Ellipse { cx, cy, .. } => Point::new(cx, cy),
318 Self::Triangle {
319 ax,
320 ay,
321 bx,
322 by,
323 cx,
324 cy,
325 } => Point::new(
326 ((i64::from(ax) + i64::from(bx) + i64::from(cx)) / 3) as i32,
327 ((i64::from(ay) + i64::from(by) + i64::from(cy)) / 3) as i32,
328 ),
329 }
330 }
331
332 pub fn grab_origin(&self) -> Point {
335 match *self {
336 Self::Rect(r) => Point::new(r.x, r.y),
337 Self::Circle { cx, cy, .. } | Self::Ellipse { cx, cy, .. } => Point::new(cx, cy),
338 Self::Triangle { .. } | Self::Poly { .. } => {
339 let b = self.bbox();
340 Point::new(b.x, b.y)
341 }
342 }
343 }
344
345 #[must_use]
348 pub fn clamp_move(&self, grab_offset: Point, cursor: Point, region: Rect) -> Self {
349 let right = region.x + region.w;
353 let bottom = region.y + region.h;
354 match *self {
355 Self::Rect(rect) => {
356 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - rect.w).max(region.x));
357 let ny =
358 (cursor.y - grab_offset.y).clamp(region.y, (bottom - rect.h).max(region.y));
359 Self::Rect(Rect::new(nx, ny, rect.w, rect.h))
360 }
361 Self::Circle { r, .. } => {
362 let min_x = region.x + r.max(0);
363 let min_y = region.y + r.max(0);
364 let cx = (cursor.x - grab_offset.x).clamp(min_x, (right - r).max(min_x));
365 let cy = (cursor.y - grab_offset.y).clamp(min_y, (bottom - r).max(min_y));
366 Self::Circle { cx, cy, r }
367 }
368 Self::Ellipse { rx, ry, .. } => {
369 let min_x = region.x + rx.max(0);
370 let min_y = region.y + ry.max(0);
371 let cx = (cursor.x - grab_offset.x).clamp(min_x, (right - rx).max(min_x));
372 let cy = (cursor.y - grab_offset.y).clamp(min_y, (bottom - ry).max(min_y));
373 Self::Ellipse { cx, cy, rx, ry }
374 }
375 Self::Triangle { .. } | Self::Poly { .. } => {
376 let b = self.bbox();
377 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - b.w).max(region.x));
378 let ny = (cursor.y - grab_offset.y).clamp(region.y, (bottom - b.h).max(region.y));
379 self.translated(nx - b.x, ny - b.y)
380 }
381 }
382 }
383
384 pub fn resize_grab(&self, p: Point, tolerance: i32) -> Option<ResizeHandle> {
388 let tolerance = tolerance.max(1);
389 match *self {
390 Self::Circle { cx, cy, r } => {
391 let dist = f64::from(p.x - cx).hypot(f64::from(p.y - cy));
392 let on_rim = (dist - f64::from(r)).abs() <= f64::from(tolerance);
393 on_rim.then_some(ResizeHandle::CircleRadius)
394 }
395 Self::Rect(rect) => box_border_grab(rect, p, tolerance),
398 Self::Ellipse { .. } | Self::Triangle { .. } | Self::Poly { .. } => {
399 box_border_grab(self.bbox(), p, tolerance)
400 }
401 }
402 }
403
404 #[must_use]
415 pub fn resize_to(
416 &self,
417 handle: ResizeHandle,
418 cursor: Point,
419 region: Rect,
420 keep_aspect: bool,
421 ) -> Self {
422 let clamped = Point::new(
423 cursor.x.clamp(region.x, region.x + region.w - 1),
424 cursor.y.clamp(region.y, region.y + region.h - 1),
425 );
426 self.resize_to_local(handle, clamped, region, keep_aspect)
427 }
428
429 #[must_use]
433 fn resize_to_local(
434 &self,
435 handle: ResizeHandle,
436 clamped: Point,
437 region: Rect,
438 keep_aspect: bool,
439 ) -> Self {
440 const MIN: i32 = 2;
441 match (self.clone(), handle) {
442 (Self::Circle { cx, cy, .. }, ResizeHandle::CircleRadius) => {
443 let r = f64::from(clamped.x - cx).hypot(f64::from(clamped.y - cy)) as i32;
444 Self::Circle {
445 cx,
446 cy,
447 r: r.max(MIN),
448 }
449 }
450 (
451 Self::Rect(rect),
452 ResizeHandle::RectEdges {
453 left,
454 right,
455 top,
456 bottom,
457 },
458 ) => Self::Rect(resize_box(
459 rect,
460 (left, right, top, bottom),
461 clamped,
462 region,
463 keep_aspect,
464 )),
465 (
466 ell @ Self::Ellipse { .. },
467 ResizeHandle::RectEdges {
468 left,
469 right,
470 top,
471 bottom,
472 },
473 ) => {
474 let bb = resize_box(
477 ell.bbox(),
478 (left, right, top, bottom),
479 clamped,
480 region,
481 keep_aspect,
482 );
483 ellipse_in_box(bb, false)
484 }
485 (
486 poly @ Self::Poly { .. },
487 ResizeHandle::RectEdges {
488 left,
489 right,
490 top,
491 bottom,
492 },
493 ) => {
494 let old = poly.bbox();
495 let new = resize_box(
496 old,
497 (left, right, top, bottom),
498 clamped,
499 region,
500 keep_aspect,
501 );
502 scale_into_box(&poly, old, new)
503 }
504 (
505 tri @ Self::Triangle { .. },
506 ResizeHandle::RectEdges {
507 left,
508 right,
509 top,
510 bottom,
511 },
512 ) => {
513 let old = tri.bbox();
514 let new = resize_box(
515 old,
516 (left, right, top, bottom),
517 clamped,
518 region,
519 keep_aspect,
520 );
521 tri.mapped_between_boxes(old, new)
522 }
523 (shape, _) => shape,
526 }
527 }
528
529 #[must_use]
532 fn mapped_between_boxes(&self, old: Rect, new: Rect) -> Self {
533 let map_x = |v: i32| {
534 new.x
535 + (f64::from(v - old.x) * f64::from(new.w) / f64::from(old.w.max(1))).round() as i32
536 };
537 let map_y = |v: i32| {
538 new.y
539 + (f64::from(v - old.y) * f64::from(new.h) / f64::from(old.h.max(1))).round() as i32
540 };
541 match self.clone() {
542 Self::Triangle {
543 ax,
544 ay,
545 bx,
546 by,
547 cx,
548 cy,
549 } => Self::Triangle {
550 ax: map_x(ax),
551 ay: map_y(ay),
552 bx: map_x(bx),
553 by: map_y(by),
554 cx: map_x(cx),
555 cy: map_y(cy),
556 },
557 other => other,
558 }
559 }
560
561 #[must_use]
564 pub fn translated(&self, dx: i32, dy: i32) -> Self {
565 match *self {
566 Self::Poly { ref points } => Self::Poly {
567 points: points
568 .iter()
569 .map(|p| Point::new(p.x + dx, p.y + dy))
570 .collect(),
571 },
572 Self::Rect(r) => Self::Rect(Rect::new(r.x + dx, r.y + dy, r.w, r.h)),
573 Self::Circle { cx, cy, r } => Self::Circle {
574 cx: cx + dx,
575 cy: cy + dy,
576 r,
577 },
578 Self::Ellipse { cx, cy, rx, ry } => Self::Ellipse {
579 cx: cx + dx,
580 cy: cy + dy,
581 rx,
582 ry,
583 },
584 Self::Triangle {
585 ax,
586 ay,
587 bx,
588 by,
589 cx,
590 cy,
591 } => Self::Triangle {
592 ax: ax + dx,
593 ay: ay + dy,
594 bx: bx + dx,
595 by: by + dy,
596 cx: cx + dx,
597 cy: cy + dy,
598 },
599 }
600 }
601}
602
603pub fn normalize_deg(deg: i32) -> i32 {
605 deg.rem_euclid(360)
606}
607
608fn scale_into_box(shape: &Shape, old: Rect, new: Rect) -> Shape {
611 let map_x = |v: i32| {
612 new.x + (f64::from(v - old.x) * f64::from(new.w) / f64::from(old.w.max(1))).round() as i32
613 };
614 let map_y = |v: i32| {
615 new.y + (f64::from(v - old.y) * f64::from(new.h) / f64::from(old.h.max(1))).round() as i32
616 };
617 match shape {
618 Shape::Poly { points } => Shape::Poly {
619 points: points
620 .iter()
621 .map(|p| Point::new(map_x(p.x), map_y(p.y)))
622 .collect(),
623 },
624 other => other.clone(),
625 }
626}
627
628fn point_in_poly(points: &[Point], p: Point) -> bool {
631 if points.len() < 3 {
632 return false;
633 }
634 let n = points.len();
635 let mut inside = false;
636 for i in 0..n {
637 let a = points[i];
638 let b = points[(i + 1) % n];
639 if on_segment(a, b, p) {
640 return true;
641 }
642 if (a.y > p.y) != (b.y > p.y) {
644 let cross = i64::from(b.x - a.x) * i64::from(p.y - a.y)
645 - i64::from(b.y - a.y) * i64::from(p.x - a.x);
646 let crosses = if b.y > a.y { cross > 0 } else { cross < 0 };
647 if crosses {
648 inside = !inside;
649 }
650 }
651 }
652 inside
653}
654
655fn on_segment(a: Point, b: Point, p: Point) -> bool {
657 let cross =
658 i64::from(b.x - a.x) * i64::from(p.y - a.y) - i64::from(b.y - a.y) * i64::from(p.x - a.x);
659 cross == 0
660 && p.x >= a.x.min(b.x)
661 && p.x <= a.x.max(b.x)
662 && p.y >= a.y.min(b.y)
663 && p.y <= a.y.max(b.y)
664}
665
666fn poly_interior_point(points: &[Point]) -> Point {
671 if points.is_empty() {
672 return Point::new(0, 0);
673 }
674 let n = points.len() as i64;
675 let sx: i64 = points.iter().map(|p| i64::from(p.x)).sum();
676 let sy: i64 = points.iter().map(|p| i64::from(p.y)).sum();
677 let mean = Point::new((sx / n) as i32, (sy / n) as i32);
678 if point_in_poly(points, mean) {
679 return mean;
680 }
681 let shape = Shape::Poly {
682 points: points.to_vec(),
683 };
684 let bb = shape.bbox();
685 for y in bb.y..=bb.y.saturating_add(bb.h) {
686 for x in bb.x..=bb.x.saturating_add(bb.w) {
687 if point_in_poly(points, Point::new(x, y)) {
688 return Point::new(x, y);
689 }
690 }
691 }
692 mean
693}
694
695pub fn regular_polygon(center: Point, toward: Point, sides: u32) -> Shape {
698 let sides = sides.clamp(3, 12) as usize;
699 let r = f64::from(toward.x - center.x).hypot(f64::from(toward.y - center.y));
700 let base = f64::from(toward.y - center.y).atan2(f64::from(toward.x - center.x));
701 let step = std::f64::consts::TAU / sides as f64;
702 let points = (0..sides)
703 .map(|i| {
704 let a = base + step * i as f64;
705 Point::new(
706 f64::from(center.x).mul_add(1.0, r * a.cos()).round() as i32,
707 f64::from(center.y).mul_add(1.0, r * a.sin()).round() as i32,
708 )
709 })
710 .collect();
711 Shape::Poly { points }
712}
713
714pub fn simplify_path(points: &[Point], epsilon: f64) -> Vec<Point> {
718 if points.len() <= 2 {
719 return points.to_vec();
720 }
721 let mut keep = vec![false; points.len()];
722 keep[0] = true;
723 keep[points.len() - 1] = true;
724 let mut stack = vec![(0usize, points.len() - 1)];
725 while let Some((start, end)) = stack.pop() {
726 if end <= start + 1 {
727 continue;
728 }
729 let (mut worst, mut worst_dist) = (start, -1.0f64);
730 for (i, p) in points.iter().enumerate().take(end).skip(start + 1) {
731 let d = point_segment_distance(*p, points[start], points[end]);
732 if d > worst_dist {
733 worst = i;
734 worst_dist = d;
735 }
736 }
737 if worst_dist > epsilon {
738 keep[worst] = true;
739 stack.push((start, worst));
740 stack.push((worst, end));
741 }
742 }
743 points
744 .iter()
745 .zip(&keep)
746 .filter(|(_, k)| **k)
747 .map(|(p, _)| *p)
748 .collect()
749}
750
751fn point_segment_distance(p: Point, a: Point, b: Point) -> f64 {
753 let (px, py) = (f64::from(p.x), f64::from(p.y));
754 let (ax, ay) = (f64::from(a.x), f64::from(a.y));
755 let (bx, by) = (f64::from(b.x), f64::from(b.y));
756 let (dx, dy) = (bx - ax, by - ay);
757 let len2 = dx * dx + dy * dy;
758 if len2 <= f64::EPSILON {
759 return (px - ax).hypot(py - ay);
760 }
761 let t = ((px - ax) * dx + (py - ay) * dy) / len2;
762 let t = t.clamp(0.0, 1.0);
763 (px - (ax + t * dx)).hypot(py - (ay + t * dy))
764}
765
766fn ellipse_in_box(bbox: Rect, lock: bool) -> Shape {
769 let cx = bbox.x + bbox.w / 2;
770 let cy = bbox.y + bbox.h / 2;
771 let (rx, ry) = (bbox.w / 2, bbox.h / 2);
772 if lock {
773 let r = rx.min(ry).max(1);
774 return Shape::Ellipse {
775 cx,
776 cy,
777 rx: r,
778 ry: r,
779 };
780 }
781 Shape::Ellipse {
782 cx,
783 cy,
784 rx: rx.max(1),
785 ry: ry.max(1),
786 }
787}
788
789pub fn rotate_point_about(p: Point, center: Point, deg: i32) -> Point {
792 let rad = f64::from(deg).to_radians();
793 let (sin, cos) = rad.sin_cos();
794 let dx = f64::from(p.x) - f64::from(center.x);
797 let dy = f64::from(p.y) - f64::from(center.y);
798 Point::new(
799 (f64::from(center.x) + (dx * cos - dy * sin).round()) as i32,
800 (f64::from(center.y) + (dx * sin + dy * cos).round()) as i32,
801 )
802}
803
804impl Shape {
805 pub fn pivot(&self) -> Point {
807 let b = self.bbox();
808 Point::new(b.x.saturating_add(b.w / 2), b.y.saturating_add(b.h / 2))
809 }
810
811 pub fn rotated_bbox(&self, deg: i32) -> Rect {
813 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
814 return self.bbox();
815 }
816 let b = self.bbox();
817 let pivot = self.pivot();
818 let (bx1, by1) = (b.x.saturating_add(b.w), b.y.saturating_add(b.h));
819 let corners = [
820 Point::new(b.x, b.y),
821 Point::new(bx1, b.y),
822 Point::new(b.x, by1),
823 Point::new(bx1, by1),
824 ]
825 .map(|c| rotate_point_about(c, pivot, deg));
826 let x0 = corners.iter().map(|c| c.x).min().unwrap_or(b.x);
827 let y0 = corners.iter().map(|c| c.y).min().unwrap_or(b.y);
828 let x1 = corners.iter().map(|c| c.x).max().unwrap_or(bx1);
829 let y1 = corners.iter().map(|c| c.y).max().unwrap_or(by1);
830 Rect::new(x0, y0, x1.saturating_sub(x0), y1.saturating_sub(y0))
831 }
832
833 pub fn hit_test_rotated(&self, deg: i32, p: Point) -> bool {
836 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
837 return self.hit_test(p);
838 }
839 self.hit_test(rotate_point_about(p, self.pivot(), -deg))
840 }
841
842 pub fn resize_grab_rotated(&self, deg: i32, p: Point, tolerance: i32) -> Option<ResizeHandle> {
845 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
846 return self.resize_grab(p, tolerance);
847 }
848 self.resize_grab(rotate_point_about(p, self.pivot(), -deg), tolerance)
849 }
850
851 #[must_use]
853 pub fn resize_to_rotated(
854 &self,
855 deg: i32,
856 handle: ResizeHandle,
857 cursor: Point,
858 region: Rect,
859 keep_aspect: bool,
860 ) -> Self {
861 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
862 return self.resize_to(handle, cursor, region, keep_aspect);
863 }
864 let visual = Point::new(
868 cursor.x.clamp(region.x, region.x + region.w - 1),
869 cursor.y.clamp(region.y, region.y + region.h - 1),
870 );
871 let local = rotate_point_about(visual, self.pivot(), -deg);
872 self.resize_to_local(handle, local, region, keep_aspect)
873 }
874
875 #[must_use]
877 pub fn clamp_move_rotated(
878 &self,
879 deg: i32,
880 grab_offset: Point,
881 cursor: Point,
882 region: Rect,
883 ) -> Self {
884 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
885 return self.clamp_move(grab_offset, cursor, region);
886 }
887 let bb = self.rotated_bbox(deg);
888 let right = region.x + region.w;
889 let bottom = region.y + region.h;
890 let nx = (cursor.x - grab_offset.x).clamp(region.x, (right - bb.w).max(region.x));
891 let ny = (cursor.y - grab_offset.y).clamp(region.y, (bottom - bb.h).max(region.y));
892 self.translated(nx - bb.x, ny - bb.y)
893 }
894
895 pub fn grab_origin_rotated(&self, deg: i32) -> Point {
897 if normalize_deg(deg) == 0 || matches!(self, Self::Circle { .. }) {
898 return self.grab_origin();
899 }
900 let bb = self.rotated_bbox(deg);
901 Point::new(bb.x, bb.y)
902 }
903
904 #[must_use]
908 pub fn with_rotation_baked(&self, deg: i32) -> Self {
909 if let Self::Poly { points } = self {
910 if normalize_deg(deg) == 0 {
911 return self.clone();
912 }
913 let pivot = self.pivot();
914 return Self::Poly {
915 points: points
916 .iter()
917 .map(|p| rotate_point_about(*p, pivot, deg))
918 .collect(),
919 };
920 }
921 match self.clone() {
922 Self::Triangle {
923 ax,
924 ay,
925 bx,
926 by,
927 cx,
928 cy,
929 } if normalize_deg(deg) != 0 => {
930 let pivot = self.pivot();
931 let a = rotate_point_about(Point::new(ax, ay), pivot, deg);
932 let b = rotate_point_about(Point::new(bx, by), pivot, deg);
933 let c = rotate_point_about(Point::new(cx, cy), pivot, deg);
934 Self::Triangle {
935 ax: a.x,
936 ay: a.y,
937 bx: b.x,
938 by: b.y,
939 cx: c.x,
940 cy: c.y,
941 }
942 }
943 other => other,
944 }
945 }
946}
947
948const fn triangle_in_box(bbox: Rect) -> Shape {
950 Shape::Triangle {
951 ax: bbox.x + bbox.w / 2,
952 ay: bbox.y,
953 bx: bbox.x,
954 by: bbox.y + bbox.h,
955 cx: bbox.x + bbox.w,
956 cy: bbox.y + bbox.h,
957 }
958}
959
960const fn cross(px: i32, py: i32, ax: i32, ay: i32, bx: i32, by: i32) -> i64 {
963 let abx = (bx - ax) as i64;
964 let aby = (by - ay) as i64;
965 let apx = (px - ax) as i64;
966 let apy = (py - ay) as i64;
967 abx * apy - aby * apx
968}
969
970const fn min3(a: i32, b: i32, c: i32) -> i32 {
971 if a <= b && a <= c {
972 return a;
973 }
974 if b <= c {
975 return b;
976 }
977 c
978}
979
980const fn max3(a: i32, b: i32, c: i32) -> i32 {
981 if a >= b && a >= c {
982 return a;
983 }
984 if b >= c {
985 return b;
986 }
987 c
988}
989
990fn box_border_grab(rect: Rect, p: Point, tolerance: i32) -> Option<ResizeHandle> {
993 let (x1, y1) = (rect.x + rect.w, rect.y + rect.h);
994 let within_x = p.x >= rect.x - tolerance && p.x <= x1 + tolerance;
995 let within_y = p.y >= rect.y - tolerance && p.y <= y1 + tolerance;
996 let left_d = (p.x - rect.x).abs();
997 let right_d = (p.x - x1).abs();
998 let top_d = (p.y - rect.y).abs();
999 let bottom_d = (p.y - y1).abs();
1000 let mut left = left_d <= tolerance && within_y;
1001 let mut right = right_d <= tolerance && within_y;
1002 let mut top = top_d <= tolerance && within_x;
1003 let mut bottom = bottom_d <= tolerance && within_x;
1004 if left && right {
1007 right = right_d < left_d;
1008 left = !right;
1009 }
1010 if top && bottom {
1011 bottom = bottom_d < top_d;
1012 top = !bottom;
1013 }
1014 let grabbed = left || right || top || bottom;
1015 grabbed.then_some(ResizeHandle::RectEdges {
1016 left,
1017 right,
1018 top,
1019 bottom,
1020 })
1021}
1022
1023fn resize_box(
1027 rect: Rect,
1028 (left, right, top, bottom): (bool, bool, bool, bool),
1029 clamped: Point,
1030 region: Rect,
1031 keep_aspect: bool,
1032) -> Rect {
1033 const MIN: i32 = 2;
1034 let mut x0 = rect.x;
1035 let mut x1 = rect.x + rect.w;
1036 let mut y0 = rect.y;
1037 let mut y1 = rect.y + rect.h;
1038 if left {
1039 x0 = clamped.x.min(x1 - MIN);
1040 }
1041 if right {
1042 x1 = clamped.x.max(x0 + MIN);
1043 }
1044 if top {
1045 y0 = clamped.y.min(y1 - MIN);
1046 }
1047 if bottom {
1048 y1 = clamped.y.max(y0 + MIN);
1049 }
1050 if keep_aspect && rect.w >= MIN && rect.h >= MIN {
1051 let (w0, h0) = (f64::from(rect.w), f64::from(rect.h));
1052 match (left || right, top || bottom) {
1055 (true, true) => {
1056 let mut s = (f64::from(x1 - x0) / w0).max(f64::from(y1 - y0) / h0);
1059 let region_right = region.x + region.w;
1060 let region_bottom = region.y + region.h;
1061 let avail_w = if left {
1062 x1 - region.x
1063 } else {
1064 region_right - x0
1065 };
1066 let avail_h = if top {
1067 y1 - region.y
1068 } else {
1069 region_bottom - y0
1070 };
1071 s = s.min(f64::from(avail_w) / w0).min(f64::from(avail_h) / h0);
1072 let w = ((w0 * s).round() as i32).max(MIN);
1073 let h = ((h0 * s).round() as i32).max(MIN);
1074 (x0, x1) = if left { (x1 - w, x1) } else { (x0, x0 + w) };
1075 (y0, y1) = if top { (y1 - h, y1) } else { (y0, y0 + h) };
1076 }
1077 (true, false) => {
1078 let h = ((f64::from(x1 - x0) * h0 / w0).round() as i32)
1081 .max(MIN)
1082 .min(region.h);
1083 let center_y = rect.y + rect.h / 2;
1084 y0 = (center_y - h / 2).clamp(region.y, region.y + region.h - h);
1085 y1 = y0 + h;
1086 }
1087 (false, _) => {
1088 let w = ((f64::from(y1 - y0) * w0 / h0).round() as i32)
1089 .max(MIN)
1090 .min(region.w);
1091 let center_x = rect.x + rect.w / 2;
1092 x0 = (center_x - w / 2).clamp(region.x, region.x + region.w - w);
1093 x1 = x0 + w;
1094 }
1095 }
1096 }
1097 let (w, h) = (x1 - x0, y1 - y0);
1098 if rect.w >= MIN && rect.h >= MIN {
1099 return Rect::new(x0, y0, w, h);
1104 }
1105 let x0 = x0.clamp(region.x, (region.x + region.w - w).max(region.x));
1108 let y0 = y0.clamp(region.y, (region.y + region.h - h).max(region.y));
1109 Rect::new(x0, y0, w, h)
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114 use super::*;
1115
1116 const BOUNDS: Size = Size::new(1920, 1080);
1117 const BOUNDS_RECT: Rect = Rect::new(0, 0, BOUNDS.w, BOUNDS.h);
1118
1119 #[test]
1120 fn rect_preview_normalizes_inverted_drag() {
1121 let s = Shape::compute_preview(
1122 ToolKind::Rect,
1123 Point::new(100, 200),
1124 Point::new(40, 50),
1125 BOUNDS_RECT,
1126 false,
1127 );
1128 assert_eq!(s, Some(Shape::Rect(Rect::new(40, 50, 60, 150))));
1129 }
1130
1131 #[test]
1132 fn rect_preview_clamps_cursor_to_bounds() {
1133 let s = Shape::compute_preview(
1134 ToolKind::Rect,
1135 Point::new(1900, 1000),
1136 Point::new(5000, 5000),
1137 BOUNDS_RECT,
1138 false,
1139 );
1140 assert_eq!(s, Some(Shape::Rect(Rect::new(1900, 1000, 19, 79))));
1141 }
1142
1143 #[test]
1144 fn rect_preview_degenerate_is_none() {
1145 assert_eq!(
1146 Shape::compute_preview(
1147 ToolKind::Rect,
1148 Point::new(10, 10),
1149 Point::new(10, 300),
1150 BOUNDS_RECT,
1151 false
1152 ),
1153 None
1154 );
1155 assert_eq!(
1156 Shape::compute_preview(
1157 ToolKind::Rect,
1158 Point::new(10, 10),
1159 Point::new(10, 10),
1160 BOUNDS_RECT,
1161 false
1162 ),
1163 None
1164 );
1165 }
1166
1167 #[test]
1168 fn circle_preview_radius_is_distance() {
1169 let s = Shape::compute_preview(
1170 ToolKind::Circle,
1171 Point::new(100, 100),
1172 Point::new(103, 104),
1173 BOUNDS_RECT,
1174 false,
1175 );
1176 assert_eq!(
1177 s,
1178 Some(Shape::Circle {
1179 cx: 100,
1180 cy: 100,
1181 r: 5
1182 })
1183 );
1184 }
1185
1186 #[test]
1187 fn circle_preview_zero_radius_is_none() {
1188 assert_eq!(
1189 Shape::compute_preview(
1190 ToolKind::Circle,
1191 Point::new(7, 7),
1192 Point::new(7, 7),
1193 BOUNDS_RECT,
1194 false
1195 ),
1196 None
1197 );
1198 }
1199
1200 #[test]
1201 fn rect_hit_test_edges() {
1202 let s = Shape::Rect(Rect::new(10, 10, 20, 20));
1203 assert!(s.hit_test(Point::new(10, 10)));
1204 assert!(s.hit_test(Point::new(29, 29)));
1205 assert!(!s.hit_test(Point::new(30, 30)));
1206 assert!(!s.hit_test(Point::new(9, 10)));
1207 }
1208
1209 #[test]
1210 fn circle_hit_test_boundary_inclusive() {
1211 let s = Shape::Circle {
1212 cx: 0,
1213 cy: 0,
1214 r: 10,
1215 };
1216 assert!(s.hit_test(Point::new(10, 0)));
1217 assert!(s.hit_test(Point::new(6, 8)));
1218 assert!(!s.hit_test(Point::new(8, 8)));
1219 }
1220
1221 #[test]
1222 fn circle_hit_test_survives_extreme_coords() {
1223 let s = Shape::Circle { cx: 0, cy: 0, r: 5 };
1224 assert!(!s.hit_test(Point::new(i32::MAX, i32::MAX)));
1225 }
1226
1227 #[test]
1228 fn bbox_of_circle() {
1229 let s = Shape::Circle {
1230 cx: 50,
1231 cy: 60,
1232 r: 10,
1233 };
1234 assert_eq!(s.bbox(), Rect::new(40, 50, 20, 20));
1235 }
1236
1237 #[test]
1238 fn rect_clamp_move_never_escapes_bounds() {
1239 let s = Shape::Rect(Rect::new(0, 0, 300, 200));
1240 let grab = Point::new(0, 0);
1241 for cx in [-500, 0, 960, 5000] {
1242 for cy in [-500, 0, 540, 5000] {
1243 let Shape::Rect(r) = s.clamp_move(grab, Point::new(cx, cy), BOUNDS_RECT) else {
1244 panic!("rect stayed rect");
1245 };
1246 assert!(r.x >= 0 && r.y >= 0, "({cx},{cy}) gave {r:?}");
1247 assert!(
1248 r.x + r.w <= BOUNDS.w && r.y + r.h <= BOUNDS.h,
1249 "({cx},{cy}) gave {r:?}"
1250 );
1251 }
1252 }
1253 }
1254
1255 #[test]
1256 fn circle_clamp_move_never_escapes_bounds() {
1257 let s = Shape::Circle {
1258 cx: 500,
1259 cy: 500,
1260 r: 40,
1261 };
1262 let grab = Point::new(0, 0);
1263 for cx in [-500, 0, 960, 5000] {
1264 for cy in [-500, 0, 540, 5000] {
1265 let Shape::Circle {
1266 cx: ncx,
1267 cy: ncy,
1268 r,
1269 } = s.clamp_move(grab, Point::new(cx, cy), BOUNDS_RECT)
1270 else {
1271 panic!("circle stayed circle");
1272 };
1273 assert!(
1274 ncx - r >= 0 && ncy - r >= 0,
1275 "({cx},{cy}) gave center ({ncx},{ncy})"
1276 );
1277 assert!(
1278 ncx + r <= BOUNDS.w && ncy + r <= BOUNDS.h,
1279 "({cx},{cy}) gave center ({ncx},{ncy})"
1280 );
1281 }
1282 }
1283 }
1284
1285 #[test]
1286 fn oversized_circle_clamp_is_stable() {
1287 let s = Shape::Circle {
1290 cx: 100,
1291 cy: 100,
1292 r: 2000,
1293 };
1294 let moved = s.clamp_move(Point::new(0, 0), Point::new(0, 0), BOUNDS_RECT);
1295 assert_eq!(
1296 moved,
1297 Shape::Circle {
1298 cx: 2000,
1299 cy: 2000,
1300 r: 2000
1301 }
1302 );
1303 }
1304
1305 #[test]
1306 fn translated_shifts_both_kinds() {
1307 assert_eq!(
1308 Shape::Rect(Rect::new(1, 2, 3, 4)).translated(10, 20),
1309 Shape::Rect(Rect::new(11, 22, 3, 4))
1310 );
1311 assert_eq!(
1312 Shape::Circle { cx: 1, cy: 2, r: 3 }.translated(10, 20),
1313 Shape::Circle {
1314 cx: 11,
1315 cy: 22,
1316 r: 3
1317 }
1318 );
1319 }
1320
1321 #[test]
1322 fn circle_rim_grab_within_tolerance_only() {
1323 let s = Shape::Circle {
1324 cx: 100,
1325 cy: 100,
1326 r: 50,
1327 };
1328 assert_eq!(
1329 s.resize_grab(Point::new(153, 100), 5),
1330 Some(ResizeHandle::CircleRadius)
1331 );
1332 assert_eq!(
1333 s.resize_grab(Point::new(147, 100), 5),
1334 Some(ResizeHandle::CircleRadius)
1335 );
1336 assert_eq!(s.resize_grab(Point::new(100, 100), 5), None); assert_eq!(s.resize_grab(Point::new(160, 100), 5), None); }
1339
1340 #[test]
1341 fn rect_edge_and_corner_grabs() {
1342 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1343 assert_eq!(
1344 s.resize_grab(Point::new(100, 150), 5),
1345 Some(ResizeHandle::RectEdges {
1346 left: true,
1347 right: false,
1348 top: false,
1349 bottom: false
1350 })
1351 );
1352 assert_eq!(
1353 s.resize_grab(Point::new(302, 150), 5), Some(ResizeHandle::RectEdges {
1355 left: false,
1356 right: true,
1357 top: false,
1358 bottom: false
1359 })
1360 );
1361 assert_eq!(
1362 s.resize_grab(Point::new(298, 202), 5), Some(ResizeHandle::RectEdges {
1364 left: false,
1365 right: true,
1366 top: false,
1367 bottom: true
1368 })
1369 );
1370 assert_eq!(s.resize_grab(Point::new(200, 150), 5), None); assert_eq!(s.resize_grab(Point::new(90, 150), 5), None); }
1373
1374 #[test]
1375 fn tiny_rect_grabs_nearer_edge_not_both() {
1376 let s = Shape::Rect(Rect::new(100, 100, 6, 6));
1377 let Some(ResizeHandle::RectEdges { left, right, .. }) =
1378 s.resize_grab(Point::new(101, 103), 5)
1379 else {
1380 panic!("expected an edge grab");
1381 };
1382 assert!(left && !right);
1383 }
1384
1385 #[test]
1386 fn circle_resize_follows_cursor_distance() {
1387 let s = Shape::Circle {
1388 cx: 100,
1389 cy: 100,
1390 r: 50,
1391 };
1392 let resized = s.resize_to(
1393 ResizeHandle::CircleRadius,
1394 Point::new(100, 180),
1395 BOUNDS_RECT,
1396 false,
1397 );
1398 assert_eq!(
1399 resized,
1400 Shape::Circle {
1401 cx: 100,
1402 cy: 100,
1403 r: 80
1404 }
1405 );
1406 let tiny = s.resize_to(
1408 ResizeHandle::CircleRadius,
1409 Point::new(100, 100),
1410 BOUNDS_RECT,
1411 false,
1412 );
1413 assert_eq!(
1414 tiny,
1415 Shape::Circle {
1416 cx: 100,
1417 cy: 100,
1418 r: 2
1419 }
1420 );
1421 }
1422
1423 #[test]
1424 fn rect_corner_resize_anchors_opposite_corner() {
1425 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1426 let handle = ResizeHandle::RectEdges {
1427 left: false,
1428 right: true,
1429 top: false,
1430 bottom: true,
1431 };
1432 let resized = s.resize_to(handle, Point::new(400, 300), BOUNDS_RECT, false);
1433 assert_eq!(resized, Shape::Rect(Rect::new(100, 100, 300, 200)));
1434 }
1435
1436 #[test]
1437 fn rect_edge_resize_moves_one_axis_only() {
1438 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1439 let handle = ResizeHandle::RectEdges {
1440 left: true,
1441 right: false,
1442 top: false,
1443 bottom: false,
1444 };
1445 let resized = s.resize_to(handle, Point::new(50, 999), BOUNDS_RECT, false);
1446 assert_eq!(resized, Shape::Rect(Rect::new(50, 100, 250, 100)));
1447 }
1448
1449 #[test]
1450 fn rect_resize_cannot_invert_or_vanish() {
1451 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1452 let handle = ResizeHandle::RectEdges {
1453 left: true,
1454 right: false,
1455 top: false,
1456 bottom: false,
1457 };
1458 let resized = s.resize_to(handle, Point::new(500, 150), BOUNDS_RECT, false);
1460 assert_eq!(resized, Shape::Rect(Rect::new(298, 100, 2, 100)));
1461 }
1462
1463 #[test]
1464 fn resize_cursor_is_clamped_to_bounds() {
1465 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1466 let handle = ResizeHandle::RectEdges {
1467 left: false,
1468 right: true,
1469 top: false,
1470 bottom: false,
1471 };
1472 let resized = s.resize_to(handle, Point::new(99_999, 150), BOUNDS_RECT, false);
1473 assert_eq!(
1474 resized,
1475 Shape::Rect(Rect::new(100, 100, BOUNDS.w - 1 - 100, 100))
1476 );
1477 }
1478
1479 #[test]
1480 fn locked_corner_resize_keeps_ratio_dominant_axis_wins() {
1481 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1484 let corner = ResizeHandle::RectEdges {
1485 left: false,
1486 right: true,
1487 top: false,
1488 bottom: true,
1489 };
1490 let resized = s.resize_to(corner, Point::new(400, 300), BOUNDS_RECT, true);
1491 assert_eq!(resized, Shape::Rect(Rect::new(100, 100, 400, 200)));
1492 }
1493
1494 #[test]
1495 fn locked_corner_resize_anchors_the_opposite_corner() {
1496 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1498 let corner = ResizeHandle::RectEdges {
1499 left: true,
1500 right: false,
1501 top: true,
1502 bottom: false,
1503 };
1504 let resized = s.resize_to(corner, Point::new(0, 80), BOUNDS_RECT, true);
1505 let Shape::Rect(r) = resized else {
1506 panic!("still a rect")
1507 };
1508 assert_eq!((r.x + r.w, r.y + r.h), (300, 200), "anchor moved");
1509 assert_eq!(r.w * 100, r.h * 200, "ratio drifted: {r:?}");
1510 }
1511
1512 #[test]
1513 fn locked_corner_resize_caps_scale_at_bounds() {
1514 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1518 let corner = ResizeHandle::RectEdges {
1519 left: false,
1520 right: true,
1521 top: false,
1522 bottom: true,
1523 };
1524 let resized = s.resize_to(
1525 corner,
1526 Point::new(BOUNDS_RECT.w - 1, BOUNDS_RECT.h - 1),
1527 BOUNDS_RECT,
1528 true,
1529 );
1530 let Shape::Rect(r) = resized else {
1531 panic!("still a rect")
1532 };
1533 assert!(
1534 r.x + r.w <= BOUNDS.w && r.y + r.h <= BOUNDS.h,
1535 "escaped: {r:?}"
1536 );
1537 assert_eq!(r.w, BOUNDS.w - 100);
1538 assert_eq!(r.w, 2 * r.h);
1539 }
1540
1541 #[test]
1542 fn locked_edge_resize_scales_other_axis_centered() {
1543 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
1546 let edge = ResizeHandle::RectEdges {
1547 left: false,
1548 right: true,
1549 top: false,
1550 bottom: false,
1551 };
1552 let resized = s.resize_to(edge, Point::new(500, 150), BOUNDS_RECT, true);
1553 assert_eq!(resized, Shape::Rect(Rect::new(100, 50, 400, 200)));
1554 }
1555
1556 #[test]
1557 fn locked_edge_resize_clamps_centered_axis_to_bounds() {
1558 let s = Shape::Rect(Rect::new(100, 10, 200, 100));
1561 let edge = ResizeHandle::RectEdges {
1562 left: false,
1563 right: true,
1564 top: false,
1565 bottom: false,
1566 };
1567 let resized = s.resize_to(edge, Point::new(500, 60), BOUNDS_RECT, true);
1568 let Shape::Rect(r) = resized else {
1569 panic!("still a rect")
1570 };
1571 assert_eq!((r.w, r.h), (400, 200));
1572 assert_eq!(r.y, 0, "clamped to the top edge");
1573 }
1574
1575 #[test]
1576 fn locked_circle_resize_is_unchanged_by_lock() {
1577 let s = Shape::Circle {
1578 cx: 100,
1579 cy: 100,
1580 r: 50,
1581 };
1582 let unlocked = s.resize_to(
1583 ResizeHandle::CircleRadius,
1584 Point::new(100, 180),
1585 BOUNDS_RECT,
1586 false,
1587 );
1588 let locked = s.resize_to(
1589 ResizeHandle::CircleRadius,
1590 Point::new(100, 180),
1591 BOUNDS_RECT,
1592 true,
1593 );
1594 assert_eq!(unlocked, locked);
1595 }
1596
1597 #[test]
1598 fn mismatched_handle_is_inert() {
1599 let s = Shape::Circle { cx: 5, cy: 5, r: 5 };
1600 let handle = ResizeHandle::RectEdges {
1601 left: true,
1602 right: false,
1603 top: false,
1604 bottom: false,
1605 };
1606 assert_eq!(
1607 s.resize_to(handle, Point::new(50, 50), BOUNDS_RECT, false),
1608 s
1609 );
1610 }
1611
1612 #[test]
1613 fn ellipse_preview_inscribes_the_drag_box_and_shift_locks_a_circle() {
1614 let free = Shape::compute_preview(
1615 ToolKind::Ellipse,
1616 Point::new(10, 10),
1617 Point::new(50, 30),
1618 BOUNDS_RECT,
1619 false,
1620 );
1621 assert_eq!(
1622 free,
1623 Some(Shape::Ellipse {
1624 cx: 30,
1625 cy: 20,
1626 rx: 20,
1627 ry: 10,
1628 })
1629 );
1630 let locked = Shape::compute_preview(
1631 ToolKind::Ellipse,
1632 Point::new(10, 10),
1633 Point::new(50, 30),
1634 BOUNDS_RECT,
1635 true,
1636 );
1637 assert_eq!(
1638 locked,
1639 Some(Shape::Ellipse {
1640 cx: 30,
1641 cy: 20,
1642 rx: 10,
1643 ry: 10,
1644 }),
1645 "Shift inscribes the circle instead"
1646 );
1647 }
1648
1649 #[test]
1650 fn ellipse_hit_test_is_boundary_inclusive_and_excludes_bbox_corners() {
1651 let e = Shape::Ellipse {
1652 cx: 50,
1653 cy: 40,
1654 rx: 30,
1655 ry: 10,
1656 };
1657 assert!(e.hit_test(Point::new(50, 40)));
1658 assert!(e.hit_test(Point::new(80, 40)), "rx vertex inclusive");
1659 assert!(e.hit_test(Point::new(50, 30)), "ry vertex inclusive");
1660 assert!(!e.hit_test(Point::new(80, 30)), "bbox corner outside");
1661 assert!(!e.hit_test(Point::new(81, 40)));
1662 assert_eq!(e.bbox(), Rect::new(20, 30, 60, 20));
1663 }
1664
1665 #[test]
1666 fn ellipse_resize_rides_its_bounding_box() {
1667 let e = Shape::Ellipse {
1668 cx: 50,
1669 cy: 40,
1670 rx: 20,
1671 ry: 10,
1672 };
1673 let handle = e.resize_grab(Point::new(70, 40), 2).expect("edge grab");
1675 let resized = e.resize_to(handle, Point::new(90, 40), BOUNDS_RECT, false);
1676 assert_eq!(
1677 resized,
1678 Shape::Ellipse {
1679 cx: 60,
1680 cy: 40,
1681 rx: 30,
1682 ry: 10,
1683 },
1684 "left edge anchored, rx grew"
1685 );
1686 }
1687
1688 #[test]
1689 fn rotated_ellipse_hit_follows_the_turn() {
1690 let e = Shape::Ellipse {
1691 cx: 50,
1692 cy: 40,
1693 rx: 30,
1694 ry: 8,
1695 };
1696 assert!(e.hit_test_rotated(90, Point::new(50, 65)));
1698 assert!(!e.hit_test_rotated(90, Point::new(75, 40)));
1699 assert!(e.hit_test(Point::new(75, 40)), "unrotated it lies flat");
1700 }
1701
1702 #[test]
1703 fn point_in_poly_handles_concave_shapes_edges_included() {
1704 let u = vec![
1706 Point::new(0, 0),
1707 Point::new(10, 0),
1708 Point::new(10, 30),
1709 Point::new(20, 30),
1710 Point::new(20, 0),
1711 Point::new(30, 0),
1712 Point::new(30, 40),
1713 Point::new(0, 40),
1714 ];
1715 let shape = Shape::Poly { points: u };
1716 assert!(shape.hit_test(Point::new(5, 20)), "left arm");
1717 assert!(shape.hit_test(Point::new(25, 20)), "right arm");
1718 assert!(shape.hit_test(Point::new(15, 35)), "base");
1719 assert!(!shape.hit_test(Point::new(15, 10)), "the notch is outside");
1720 assert!(shape.hit_test(Point::new(0, 0)), "vertex inclusive");
1721 assert!(shape.hit_test(Point::new(5, 0)), "edge inclusive");
1722 assert!(!shape.hit_test(Point::new(-1, 20)));
1723 assert!(shape.hit_test(shape.click_point()));
1725 }
1726
1727 #[test]
1728 fn regular_polygon_puts_the_first_vertex_at_the_cursor() {
1729 let hex = regular_polygon(Point::new(100, 100), Point::new(140, 100), 6);
1730 let Shape::Poly { ref points } = hex else {
1731 panic!("regular polygon is a poly")
1732 };
1733 assert_eq!(points.len(), 6);
1734 assert_eq!(points[0], Point::new(140, 100), "first vertex at cursor");
1735 for p in points {
1736 let d = f64::from(p.x - 100).hypot(f64::from(p.y - 100));
1737 assert!((d - 40.0).abs() < 1.5, "vertex {p:?} off the radius: {d}");
1738 }
1739 let tri = regular_polygon(Point::new(0, 0), Point::new(10, 0), 1);
1741 let Shape::Poly { points } = tri else {
1742 panic!()
1743 };
1744 assert_eq!(points.len(), 3);
1745 }
1746
1747 #[test]
1748 fn simplify_path_drops_jitter_and_keeps_corners() {
1749 let path: Vec<Point> = (0..=20)
1752 .map(|x| Point::new(x * 5, i32::from(x % 2 != 0)))
1753 .chain((1..=10).map(|y| Point::new(100, y * 5)))
1754 .collect();
1755 let simplified = simplify_path(&path, 2.0);
1756 assert!(
1757 simplified.len() <= 5,
1758 "expected a handful of points, got {}",
1759 simplified.len()
1760 );
1761 assert_eq!(*simplified.first().unwrap(), Point::new(0, 0));
1762 assert_eq!(*simplified.last().unwrap(), Point::new(100, 50));
1763 assert!(
1764 simplified.contains(&Point::new(100, 1)) || simplified.contains(&Point::new(100, 0)),
1765 "the corner survives: {simplified:?}"
1766 );
1767 }
1768
1769 #[test]
1770 fn poly_moves_resizes_and_rotates_like_any_shape() {
1771 let square = Shape::Poly {
1772 points: vec![
1773 Point::new(10, 10),
1774 Point::new(30, 10),
1775 Point::new(30, 30),
1776 Point::new(10, 30),
1777 ],
1778 };
1779 assert_eq!(square.bbox(), Rect::new(10, 10, 20, 20));
1780 let moved = square.translated(5, -5);
1781 assert_eq!(moved.bbox(), Rect::new(15, 5, 20, 20));
1782 let handle = square.resize_grab(Point::new(30, 20), 2).expect("edge");
1784 let grown = square.resize_to(handle, Point::new(50, 20), BOUNDS_RECT, false);
1785 assert_eq!(grown.bbox(), Rect::new(10, 10, 40, 20));
1786 let turned = square.with_rotation_baked(90);
1788 assert_eq!(turned.bbox(), square.bbox(), "square is 90-symmetric");
1789 assert!(matches!(turned, Shape::Poly { .. }));
1790 }
1791
1792 #[test]
1793 fn click_point_centers_each_kind() {
1794 assert_eq!(
1795 Shape::Rect(Rect::new(10, 20, 30, 40)).click_point(),
1796 Point::new(25, 40)
1797 );
1798 assert_eq!(
1799 Shape::Circle { cx: 5, cy: 6, r: 7 }.click_point(),
1800 Point::new(5, 6)
1801 );
1802 let tri = Shape::Triangle {
1803 ax: 30,
1804 ay: 0,
1805 bx: 0,
1806 by: 60,
1807 cx: 60,
1808 cy: 60,
1809 };
1810 assert_eq!(tri.click_point(), Point::new(30, 40));
1811 assert!(tri.hit_test(tri.click_point()));
1812 let rect = Shape::Rect(Rect::new(10, 10, 40, 10));
1815 assert!(rect.hit_test_rotated(90, rect.click_point()));
1816 }
1817
1818 #[test]
1819 fn tool_kind_cycles_through_the_drawing_tools() {
1820 assert_eq!(ToolKind::Rect.next(), ToolKind::Ellipse);
1821 assert_eq!(ToolKind::Ellipse.next(), ToolKind::Triangle);
1822 assert_eq!(ToolKind::Triangle.next(), ToolKind::Polygon);
1823 assert_eq!(ToolKind::Polygon.next(), ToolKind::Freehand);
1824 assert_eq!(ToolKind::Freehand.next(), ToolKind::Rect);
1825 assert_eq!(ToolKind::Circle.next(), ToolKind::Triangle);
1827 assert_eq!(ToolKind::Poly.next(), ToolKind::Rect);
1828 }
1829
1830 #[test]
1831 fn triangle_preview_is_apex_top_center_in_drag_box() {
1832 let s = Shape::compute_preview(
1833 ToolKind::Triangle,
1834 Point::new(100, 100),
1835 Point::new(300, 200),
1836 BOUNDS_RECT,
1837 false,
1838 );
1839 assert_eq!(
1840 s,
1841 Some(Shape::Triangle {
1842 ax: 200,
1843 ay: 100,
1844 bx: 100,
1845 by: 200,
1846 cx: 300,
1847 cy: 200,
1848 })
1849 );
1850 }
1851
1852 #[test]
1853 fn triangle_hit_test_excludes_bbox_corners() {
1854 let tri = Shape::Triangle {
1855 ax: 200,
1856 ay: 100,
1857 bx: 100,
1858 by: 200,
1859 cx: 300,
1860 cy: 200,
1861 };
1862 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))); }
1868
1869 #[test]
1870 fn triangle_bbox_and_move_clamp() {
1871 let tri = Shape::Triangle {
1872 ax: 200,
1873 ay: 100,
1874 bx: 100,
1875 by: 200,
1876 cx: 300,
1877 cy: 200,
1878 };
1879 assert_eq!(tri.bbox(), Rect::new(100, 100, 200, 100));
1880 let moved = tri.clamp_move(Point::new(0, 0), Point::new(-500, -500), BOUNDS_RECT);
1883 assert_eq!(moved.bbox(), Rect::new(0, 0, 200, 100));
1884 assert_eq!(
1885 moved,
1886 Shape::Triangle {
1887 ax: 100,
1888 ay: 0,
1889 bx: 0,
1890 by: 100,
1891 cx: 200,
1892 cy: 100,
1893 }
1894 );
1895 }
1896
1897 #[test]
1898 fn triangle_resize_scales_vertices_into_new_bbox() {
1899 let tri = Shape::Triangle {
1900 ax: 200,
1901 ay: 100,
1902 bx: 100,
1903 by: 200,
1904 cx: 300,
1905 cy: 200,
1906 };
1907 let handle = ResizeHandle::RectEdges {
1909 left: false,
1910 right: true,
1911 top: false,
1912 bottom: true,
1913 };
1914 let resized = tri.resize_to(handle, Point::new(500, 300), BOUNDS_RECT, false);
1915 assert_eq!(
1916 resized,
1917 Shape::Triangle {
1918 ax: 300,
1919 ay: 100,
1920 bx: 100,
1921 by: 300,
1922 cx: 500,
1923 cy: 300,
1924 }
1925 );
1926 }
1927
1928 #[test]
1929 fn triangle_resize_grab_is_on_the_bbox_border() {
1930 let tri = Shape::Triangle {
1931 ax: 200,
1932 ay: 100,
1933 bx: 100,
1934 by: 200,
1935 cx: 300,
1936 cy: 200,
1937 };
1938 assert_eq!(
1940 tri.resize_grab(Point::new(150, 100), 5),
1941 Some(ResizeHandle::RectEdges {
1942 left: false,
1943 right: false,
1944 top: true,
1945 bottom: false
1946 })
1947 );
1948 assert_eq!(tri.resize_grab(Point::new(200, 150), 5), None); }
1950
1951 #[test]
1952 fn degenerate_triangles_cover_nothing() {
1953 let point = Shape::Triangle {
1954 ax: 0,
1955 ay: 0,
1956 bx: 0,
1957 by: 0,
1958 cx: 0,
1959 cy: 0,
1960 };
1961 assert!(!point.hit_test(Point::new(500, 500)));
1962 assert!(!point.hit_test(Point::new(0, 0)));
1963 let line = Shape::Triangle {
1964 ax: 0,
1965 ay: 0,
1966 bx: 10,
1967 by: 10,
1968 cx: 20,
1969 cy: 20,
1970 };
1971 assert!(!line.hit_test(Point::new(400, 400)));
1972 assert!(!line.hit_test(Point::new(5, 5)));
1973 }
1974
1975 #[test]
1976 fn extreme_shapes_do_not_panic() {
1977 let huge = Shape::Circle {
1978 cx: 0,
1979 cy: 0,
1980 r: 2_000_000_000,
1981 };
1982 let bb = huge.bbox();
1983 assert!(bb.w > 0);
1984 let far = Shape::Rect(Rect::new(
1985 2_000_000_000,
1986 2_000_000_000,
1987 400_000_000,
1988 400_000_000,
1989 ));
1990 let _ = far.rotated_bbox(45);
1991 }
1992
1993 #[test]
1994 fn resize_of_sub_min_rect_stays_in_bounds() {
1995 let s = Shape::Rect(Rect::new(0, 0, 1, 100));
1998 let handle = ResizeHandle::RectEdges {
1999 left: true,
2000 right: false,
2001 top: false,
2002 bottom: false,
2003 };
2004 let Shape::Rect(r) = s.resize_to(handle, Point::new(0, 50), BOUNDS_RECT, false) else {
2005 panic!("still a rect")
2006 };
2007 assert!(r.x >= 0, "escaped left: {r:?}");
2008 let s = Shape::Rect(Rect::new(BOUNDS.w - 1, 0, 1, 100));
2010 let handle = ResizeHandle::RectEdges {
2011 left: false,
2012 right: true,
2013 top: false,
2014 bottom: false,
2015 };
2016 let Shape::Rect(r) = s.resize_to(
2017 handle,
2018 Point::new(BOUNDS_RECT.w - 1, 50),
2019 BOUNDS_RECT,
2020 false,
2021 ) else {
2022 panic!("still a rect")
2023 };
2024 assert!(r.x + r.w <= BOUNDS.w, "escaped right: {r:?}");
2025 }
2026
2027 #[test]
2028 fn rotated_resize_never_moves_the_anchored_edge() {
2029 let s = Shape::Rect(Rect::new(800, 500, 200, 100));
2033 let handle = ResizeHandle::RectEdges {
2034 left: false,
2035 right: true,
2036 top: false,
2037 bottom: false,
2038 };
2039 let Shape::Rect(r) =
2040 s.resize_to_rotated(45, handle, Point::new(1900, 1000), BOUNDS_RECT, false)
2041 else {
2042 panic!("still a rect")
2043 };
2044 assert_eq!(r.x, 800, "anchored left edge moved");
2045 assert_eq!(r.y, 500, "anchored top edge moved");
2046 }
2047
2048 #[test]
2049 fn resize_of_offscreen_local_box_does_not_teleport() {
2050 let s = Shape::Rect(Rect::new(-90, 0, 200, 20));
2054 let handle = ResizeHandle::RectEdges {
2055 left: false,
2056 right: true,
2057 top: false,
2058 bottom: false,
2059 };
2060 let Shape::Rect(r) = s.resize_to(handle, Point::new(120, 10), BOUNDS_RECT, false) else {
2061 panic!("still a rect")
2062 };
2063 assert_eq!(r.x, -90, "shape teleported");
2064 assert_eq!(r.w, 210);
2065 }
2066
2067 #[test]
2068 fn rotated_resize_tracks_cursor_at_screen_edge() {
2069 let s = Shape::Rect(Rect::new(800, 500, 200, 100));
2073 let handle = ResizeHandle::RectEdges {
2074 left: false,
2075 right: true,
2076 top: false,
2077 bottom: false,
2078 };
2079 let r45 = s.resize_to_rotated(45, handle, Point::new(99_999, 99_999), BOUNDS_RECT, false);
2080 assert_ne!(r45, s);
2082 }
2083
2084 #[test]
2085 fn rotate_point_quarter_turn() {
2086 let center = Point::new(100, 100);
2087 assert_eq!(
2089 rotate_point_about(Point::new(110, 100), center, 90),
2090 Point::new(100, 110)
2091 );
2092 assert_eq!(
2093 rotate_point_about(Point::new(110, 100), center, -90),
2094 Point::new(100, 90)
2095 );
2096 assert_eq!(
2097 rotate_point_about(Point::new(110, 100), center, 360),
2098 Point::new(110, 100)
2099 );
2100 }
2101
2102 #[test]
2103 fn normalize_deg_wraps_into_range() {
2104 assert_eq!(normalize_deg(0), 0);
2105 assert_eq!(normalize_deg(-1), 359);
2106 assert_eq!(normalize_deg(360), 0);
2107 assert_eq!(normalize_deg(725), 5);
2108 }
2109
2110 #[test]
2111 fn rotated_bbox_of_quarter_turned_rect_swaps_dimensions() {
2112 let s = Shape::Rect(Rect::new(100, 100, 200, 100));
2113 let bb = s.rotated_bbox(90);
2114 assert_eq!((bb.w, bb.h), (100, 200));
2115 assert_eq!(bb.x + bb.w / 2, 200);
2117 assert_eq!(bb.y + bb.h / 2, 150);
2118 assert_eq!(s.rotated_bbox(0), s.bbox());
2120 let c = Shape::Circle {
2121 cx: 50,
2122 cy: 50,
2123 r: 20,
2124 };
2125 assert_eq!(c.rotated_bbox(45), c.bbox());
2126 }
2127
2128 #[test]
2129 fn rotated_hit_test_follows_the_turned_shape() {
2130 let s = Shape::Rect(Rect::new(100, 100, 200, 20));
2134 assert!(s.hit_test_rotated(90, Point::new(200, 30)));
2135 assert!(!s.hit_test_rotated(90, Point::new(290, 110)));
2136 assert!(s.hit_test_rotated(0, Point::new(290, 110)));
2137 }
2138
2139 #[test]
2140 fn rotated_resize_grab_finds_the_visual_edge() {
2141 let s = Shape::Rect(Rect::new(100, 100, 200, 20));
2143 assert!(s.resize_grab_rotated(90, Point::new(190, 110), 5).is_some());
2145 assert!(s.resize_grab_rotated(90, Point::new(150, 110), 5).is_none());
2146 }
2147
2148 #[test]
2149 fn baked_triangle_rotates_vertices_others_unchanged() {
2150 let tri = Shape::Triangle {
2151 ax: 200,
2152 ay: 100,
2153 bx: 100,
2154 by: 200,
2155 cx: 300,
2156 cy: 200,
2157 };
2158 let baked = tri.with_rotation_baked(180);
2159 assert_eq!(
2161 baked,
2162 Shape::Triangle {
2163 ax: 200,
2164 ay: 200,
2165 bx: 300,
2166 by: 100,
2167 cx: 100,
2168 cy: 100,
2169 }
2170 );
2171 let rect = Shape::Rect(Rect::new(1, 2, 3, 4));
2172 assert_eq!(rect.with_rotation_baked(90), rect);
2173 assert_eq!(tri.with_rotation_baked(0), tri);
2174 }
2175
2176 #[test]
2177 fn triangle_serde_is_distinct_from_rect_and_circle() {
2178 let tri = Shape::Triangle {
2179 ax: 1,
2180 ay: 2,
2181 bx: 3,
2182 by: 4,
2183 cx: 5,
2184 cy: 6,
2185 };
2186 let json = serde_json::to_string(&tri).unwrap();
2187 let back: Shape = serde_json::from_str(&json).unwrap();
2188 assert_eq!(back, tri);
2189 let rect: Shape = serde_json::from_str(r#"{"x":1,"y":2,"w":3,"h":4}"#).unwrap();
2191 assert_eq!(rect, Shape::Rect(Rect::new(1, 2, 3, 4)));
2192 let circle: Shape = serde_json::from_str(r#"{"cx":1,"cy":2,"r":3}"#).unwrap();
2193 assert_eq!(circle, Shape::Circle { cx: 1, cy: 2, r: 3 });
2194 }
2195
2196 #[test]
2197 fn a_triangle_grabs_from_its_bbox_origin() {
2198 let tri = Shape::Triangle {
2199 ax: 50,
2200 ay: 10,
2201 bx: 20,
2202 by: 70,
2203 cx: 80,
2204 cy: 70,
2205 };
2206 assert_eq!(tri.grab_origin(), Point::new(20, 10));
2207 }
2208
2209 #[test]
2210 fn a_rotated_move_clamps_the_rotated_box_to_bounds() {
2211 let rect = Shape::Rect(Rect::new(10, 10, 40, 20));
2212 let bounds = Size::new(200, 200);
2213 let moved = rect.clamp_move_rotated(
2216 45,
2217 Point::new(0, 0),
2218 Point::new(500, 500),
2219 Rect::new(0, 0, bounds.w, bounds.h),
2220 );
2221 let bb = moved.rotated_bbox(45);
2222 assert!(bb.x >= 0 && bb.y >= 0, "{bb:?}");
2223 assert!(bb.x + bb.w <= bounds.w, "{bb:?}");
2224 assert!(bb.y + bb.h <= bounds.h, "{bb:?}");
2225 }
2226
2227 #[test]
2228 fn a_rotated_grab_references_the_rotated_box_origin() {
2229 let rect = Shape::Rect(Rect::new(10, 10, 40, 20));
2230 assert_eq!(rect.grab_origin_rotated(0), rect.grab_origin());
2231 let rotated = rect.grab_origin_rotated(45);
2232 assert_eq!(
2233 rotated,
2234 Point::new(rect.rotated_bbox(45).x, rect.rotated_bbox(45).y)
2235 );
2236 let circle = Shape::Circle {
2238 cx: 40,
2239 cy: 40,
2240 r: 9,
2241 };
2242 assert_eq!(circle.grab_origin_rotated(30), circle.grab_origin());
2243 }
2244
2245 #[test]
2246 fn min3_and_max3_pick_each_position() {
2247 assert_eq!(min3(1, 2, 3), 1);
2248 assert_eq!(min3(2, 1, 3), 1);
2249 assert_eq!(min3(3, 2, 1), 1);
2250 assert_eq!(max3(3, 2, 1), 3);
2251 assert_eq!(max3(1, 3, 2), 3);
2252 assert_eq!(max3(1, 2, 3), 3);
2253 }
2254
2255 #[test]
2256 fn a_proportional_vertical_edge_resize_keeps_the_aspect() {
2257 let rect = Shape::Rect(Rect::new(20, 20, 40, 20));
2259 let resized = rect.resize_to_rotated(
2260 0,
2261 ResizeHandle::RectEdges {
2262 left: false,
2263 right: false,
2264 top: true,
2265 bottom: false,
2266 },
2267 Point::new(30, 0),
2268 Rect::new(0, 0, 300, 300),
2269 true,
2270 );
2271 let bb = resized.bbox();
2272 assert!(bb.w >= 2 && bb.h >= 2, "{bb:?}");
2273 assert!(bb.x >= 0 && bb.x + bb.w <= 300, "{bb:?}");
2274 }
2275}