1use super::ink_drawing_attributes::validate_affine;
2use super::{InkError, InkPoint, InkPresenter};
3use ferroui_base::media::Color;
4use ferroui_base::{Matrix, Point, Size};
5use std::cell::Cell;
6use std::rc::Weak;
7
8pub(crate) const MAX_STENCIL_SIZE: f64 = 100_000.0;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
11#[repr(i32)]
12pub enum InkPresenterStencilKind {
13 #[default]
14 Other = 0,
15 Ruler = 1,
16 Protractor = 2,
17}
18ferroui_base::ferro_markup_enum!(InkPresenterStencilKind {
19 Other,
20 Ruler,
21 Protractor
22});
23
24pub trait IInkPresenterStencil {
26 fn kind(&self) -> InkPresenterStencilKind;
27 fn is_visible(&self) -> bool;
28 fn set_is_visible(&self, value: bool);
29 fn transform(&self) -> Matrix;
30 fn set_transform(&self, value: Matrix) -> Result<(), InkError>;
31 fn background_color(&self) -> Color;
32 fn set_background_color(&self, value: Color);
33 fn foreground_color(&self) -> Color;
34 fn set_foreground_color(&self, value: Color);
35}
36
37fn validate_dimension(value: f64) -> Result<(), InkError> {
38 if !value.is_finite() || value <= 0.0 || value > MAX_STENCIL_SIZE {
39 Err(InkError::InvalidInput(
40 "stencil dimensions must be positive and at most 100000 DIPs",
41 ))
42 } else {
43 Ok(())
44 }
45}
46fn validate_transform(value: Matrix, size: Size) -> Result<(), InkError> {
47 validate_affine(value)?;
48 if value.try_invert().is_none()
49 || (value.m11 * value.m22 - value.m12 * value.m21).abs() < 1.0e-12
50 || [
51 Point::new(0.0, 0.0),
52 Point::new(size.width, 0.0),
53 Point::new(0.0, size.height),
54 Point::new(size.width, size.height),
55 ]
56 .into_iter()
57 .map(|p| p * value)
58 .any(|p| !p.x.is_finite() || !p.y.is_finite() || p.x.abs() > 1.0e12 || p.y.abs() > 1.0e12)
59 {
60 return Err(InkError::InvalidTransform);
61 }
62 Ok(())
63}
64fn valid_canvas_size(size: Size) -> bool {
65 size.width.is_finite()
66 && size.height.is_finite()
67 && size.width > 0.0
68 && size.height > 0.0
69 && size.width <= 1.0e12
70 && size.height <= 1.0e12
71}
72fn notify(presenter: &Weak<InkPresenter>) {
73 if let Some(p) = presenter.upgrade() {
74 p.invalidate();
75 }
76}
77macro_rules! stencil_contract {
78 ($type:ty, $kind:ident) => {
79 impl IInkPresenterStencil for $type {
80 fn kind(&self) -> InkPresenterStencilKind {
81 InkPresenterStencilKind::$kind
82 }
83 fn is_visible(&self) -> bool {
84 self.is_visible()
85 }
86 fn set_is_visible(&self, value: bool) {
87 self.set_is_visible(value);
88 }
89 fn transform(&self) -> Matrix {
90 self.transform()
91 }
92 fn set_transform(&self, value: Matrix) -> Result<(), InkError> {
93 self.set_transform(value)
94 }
95 fn background_color(&self) -> Color {
96 self.background_color()
97 }
98 fn set_background_color(&self, value: Color) {
99 self.set_background_color(value);
100 }
101 fn foreground_color(&self) -> Color {
102 self.foreground_color()
103 }
104 fn set_foreground_color(&self, value: Color) {
105 self.set_foreground_color(value);
106 }
107 }
108 impl PartialEq for $type {
109 fn eq(&self, other: &Self) -> bool {
110 std::ptr::eq(self, other)
111 }
112 }
113 };
114}
115
116pub struct InkPresenterRuler {
118 presenter: Weak<InkPresenter>,
119 pub(crate) visible: Cell<bool>,
120 transform: Cell<Matrix>,
121 transform_explicit: Cell<bool>,
122 length: Cell<f64>,
123 length_explicit: Cell<bool>,
124 width: Cell<f64>,
125 width_explicit: Cell<bool>,
126 tick_marks: Cell<bool>,
127 compass: Cell<bool>,
128 background: Cell<Color>,
129 foreground: Cell<Color>,
130}
131stencil_contract!(InkPresenterRuler, Ruler);
132impl InkPresenterRuler {
133 pub(crate) fn new(presenter: Weak<InkPresenter>) -> Self {
134 Self {
135 presenter,
136 visible: Cell::new(false),
137 transform: Cell::new(Matrix::IDENTITY),
138 transform_explicit: Cell::new(false),
139 length: Cell::new(1600.0),
140 length_explicit: Cell::new(false),
141 width: Cell::new(120.0),
142 width_explicit: Cell::new(false),
143 tick_marks: Cell::new(true),
144 compass: Cell::new(true),
145 background: Cell::new(Color::from_uint32(0xffe6e6e6)),
146 foreground: Cell::new(Color::from_uint32(0xff404040)),
147 }
148 }
149 pub fn kind(&self) -> InkPresenterStencilKind {
150 InkPresenterStencilKind::Ruler
151 }
152 pub fn is_visible(&self) -> bool {
153 self.visible.get()
154 }
155 pub fn set_is_visible(&self, value: bool) {
156 if let Some(p) = self.presenter.upgrade() {
157 p.set_ruler_visible(value);
158 } else {
159 self.visible.set(value);
160 }
161 }
162 pub fn transform(&self) -> Matrix {
163 self.transform.get()
164 }
165 pub fn set_transform(&self, value: Matrix) -> Result<(), InkError> {
166 validate_transform(value, Size::new(self.length(), self.width()))?;
167 self.transform_explicit.set(true);
168 if self.transform.replace(value) != value {
169 notify(&self.presenter);
170 }
171 Ok(())
172 }
173 pub fn length(&self) -> f64 {
174 self.length.get()
175 }
176 pub fn set_length(&self, value: f64) -> Result<(), InkError> {
177 validate_dimension(value)?;
178 if value < 300.0 {
179 return Err(InkError::InvalidInput(
180 "ruler length must be at least 300 DIPs",
181 ));
182 }
183 validate_transform(self.transform(), Size::new(value, self.width()))?;
184 self.length_explicit.set(true);
185 if self.length.replace(value) != value {
186 notify(&self.presenter);
187 }
188 Ok(())
189 }
190 pub fn width(&self) -> f64 {
191 self.width.get()
192 }
193 pub fn set_width(&self, value: f64) -> Result<(), InkError> {
194 validate_dimension(value)?;
195 if value < 120.0 {
196 return Err(InkError::InvalidInput(
197 "ruler width must be at least 120 DIPs",
198 ));
199 }
200 validate_transform(self.transform(), Size::new(self.length(), value))?;
201 self.width_explicit.set(true);
202 if self.width.replace(value) != value {
203 notify(&self.presenter);
204 }
205 Ok(())
206 }
207 pub fn are_tick_marks_visible(&self) -> bool {
208 self.tick_marks.get()
209 }
210 pub fn set_are_tick_marks_visible(&self, value: bool) {
211 if self.tick_marks.replace(value) != value {
212 notify(&self.presenter);
213 }
214 }
215 pub fn is_compass_visible(&self) -> bool {
216 self.compass.get()
217 }
218 pub fn set_is_compass_visible(&self, value: bool) {
219 if self.compass.replace(value) != value {
220 notify(&self.presenter);
221 }
222 }
223 pub fn background_color(&self) -> Color {
224 self.background.get()
225 }
226 pub fn set_background_color(&self, value: Color) {
227 if self.background.replace(value) != value {
228 notify(&self.presenter);
229 }
230 }
231 pub fn foreground_color(&self) -> Color {
232 self.foreground.get()
233 }
234 pub fn set_foreground_color(&self, value: Color) {
235 if self.foreground.replace(value) != value {
236 notify(&self.presenter);
237 }
238 }
239 pub fn angle(&self) -> f64 {
241 self.transform()
242 .m12
243 .atan2(self.transform().m11)
244 .to_degrees()
245 .rem_euclid(360.0)
246 }
247 pub(crate) fn center(&self) -> Point {
248 Point::new(self.length() * 0.5, self.width() * 0.5)
249 }
250 pub(crate) fn initialize_position(&self, size: Size) {
251 if self.transform_explicit.get() || !valid_canvas_size(size) {
252 return;
253 }
254 let (scale_x, scale_y) = if self.length_explicit.get() || self.width_explicit.get() {
255 (1.0, 1.0)
256 } else {
257 (
258 (size.width * 0.8 / self.length()).min(1.0),
259 (size.height * 0.25 / self.width()).min(1.0),
260 )
261 };
262 let value = Matrix::create_scale(scale_x, scale_y)
263 * Matrix::create_translation(
264 (size.width - self.length() * scale_x) * 0.5,
265 (size.height - self.width() * scale_y) * 0.5,
266 );
267 if validate_transform(value, Size::new(self.length(), self.width())).is_ok() {
268 self.transform.set(value);
269 }
270 }
271 pub(crate) fn hit_test(&self, point: Point) -> bool {
272 self.is_visible()
273 && self.transform().try_invert().is_some_and(|inverse| {
274 let p = point * inverse;
275 p.x >= 0.0 && p.x <= self.length() && p.y >= 0.0 && p.y <= self.width()
276 })
277 }
278 pub(crate) fn blocks_ink(&self, point: Point) -> bool {
279 self.is_visible()
280 && self.transform().try_invert().is_some_and(|inverse| {
281 let p = point * inverse;
282 p.x > 0.0 && p.x < self.length() && p.y > 1.0e-8 && p.y < self.width() - 1.0e-8
283 })
284 }
285 pub(crate) fn constrain(&self, point: Point, tolerance: f64) -> Point {
286 if !self.is_visible() {
287 return point;
288 }
289 let transform = self.transform();
290 let nearest = |y| {
291 let a = Point::new(0.0, y) * transform;
292 let b = Point::new(self.length(), y) * transform;
293 let x = b.x - a.x;
294 let y = b.y - a.y;
295 if x * x + y * y == 0.0 {
296 return a;
297 }
298 let t = (((point.x - a.x) * x + (point.y - a.y) * y) / (x * x + y * y)).clamp(0.0, 1.0);
299 Point::new(a.x + x * t, a.y + y * t)
300 };
301 let a = nearest(0.0);
302 let b = nearest(self.width());
303 let distance = |p: Point| (p.x - point.x).hypot(p.y - point.y);
304 let candidate = if distance(a) < distance(b) { a } else { b };
305 if distance(candidate) <= tolerance {
306 candidate
307 } else {
308 point
309 }
310 }
311}
312
313#[derive(Clone, Copy)]
314struct ArcMeasurement {
315 start: f64,
316 last: f64,
317 sweep: f64,
318}
319
320pub struct InkPresenterProtractor {
323 presenter: Weak<InkPresenter>,
324 pub(crate) visible: Cell<bool>,
325 transform: Cell<Matrix>,
326 transform_explicit: Cell<bool>,
327 radius: Cell<f64>,
328 radius_explicit: Cell<bool>,
329 center_marker: Cell<bool>,
330 angle_readout: Cell<bool>,
331 tick_marks: Cell<bool>,
332 rays: Cell<bool>,
333 resizable: Cell<bool>,
334 background: Cell<Color>,
335 foreground: Cell<Color>,
336 accent: Cell<Color>,
337 measurement: Cell<Option<ArcMeasurement>>,
338}
339stencil_contract!(InkPresenterProtractor, Protractor);
340impl InkPresenterProtractor {
341 pub(crate) fn new(presenter: Weak<InkPresenter>) -> Self {
342 Self {
343 presenter,
344 visible: Cell::new(false),
345 transform: Cell::new(Matrix::IDENTITY),
346 transform_explicit: Cell::new(false),
347 radius: Cell::new(280.0),
348 radius_explicit: Cell::new(false),
349 center_marker: Cell::new(true),
350 angle_readout: Cell::new(true),
351 tick_marks: Cell::new(true),
352 rays: Cell::new(true),
353 resizable: Cell::new(true),
354 background: Cell::new(Color::from_uint32(0xffe6e6e6)),
355 foreground: Cell::new(Color::from_uint32(0xff404040)),
356 accent: Cell::new(Color::from_uint32(0xff0078d4)),
357 measurement: Cell::new(None),
358 }
359 }
360 pub fn kind(&self) -> InkPresenterStencilKind {
361 InkPresenterStencilKind::Protractor
362 }
363 pub fn is_visible(&self) -> bool {
364 self.visible.get()
365 }
366 pub fn set_is_visible(&self, value: bool) {
367 if let Some(p) = self.presenter.upgrade() {
368 p.set_protractor_visible(value);
369 } else {
370 self.visible.set(value);
371 }
372 }
373 pub fn transform(&self) -> Matrix {
374 self.transform.get()
375 }
376 pub fn set_transform(&self, value: Matrix) -> Result<(), InkError> {
377 validate_transform(value, Size::new(self.radius() * 2.0, self.radius() * 2.0))?;
378 self.transform_explicit.set(true);
379 if self.transform.replace(value) != value {
380 self.measurement.set(None);
381 notify(&self.presenter);
382 }
383 Ok(())
384 }
385 pub fn radius(&self) -> f64 {
386 self.radius.get()
387 }
388 pub fn set_radius(&self, value: f64) -> Result<(), InkError> {
389 validate_dimension(value)?;
390 validate_transform(self.transform(), Size::new(value * 2.0, value * 2.0))?;
391 self.radius_explicit.set(true);
392 if self.radius.replace(value) != value {
393 self.measurement.set(None);
394 notify(&self.presenter);
395 }
396 Ok(())
397 }
398 pub fn is_center_marker_visible(&self) -> bool {
399 self.center_marker.get()
400 }
401 pub fn set_is_center_marker_visible(&self, value: bool) {
402 if self.center_marker.replace(value) != value {
403 notify(&self.presenter);
404 }
405 }
406 pub fn is_angle_readout_visible(&self) -> bool {
407 self.angle_readout.get()
408 }
409 pub fn set_is_angle_readout_visible(&self, value: bool) {
410 if self.angle_readout.replace(value) != value {
411 notify(&self.presenter);
412 }
413 }
414 pub fn are_angle_readouts_visible(&self) -> bool {
416 self.is_angle_readout_visible()
417 }
418 pub fn set_are_angle_readouts_visible(&self, value: bool) {
419 self.set_is_angle_readout_visible(value);
420 }
421 pub fn are_tick_marks_visible(&self) -> bool {
422 self.tick_marks.get()
423 }
424 pub fn set_are_tick_marks_visible(&self, value: bool) {
425 if self.tick_marks.replace(value) != value {
426 notify(&self.presenter);
427 }
428 }
429 pub fn are_rays_visible(&self) -> bool {
430 self.rays.get()
431 }
432 pub fn set_are_rays_visible(&self, value: bool) {
433 if self.rays.replace(value) != value {
434 notify(&self.presenter);
435 }
436 }
437 pub fn is_resizable(&self) -> bool {
438 self.resizable.get()
439 }
440 pub fn set_is_resizable(&self, value: bool) {
441 if self.resizable.replace(value) != value {
442 notify(&self.presenter);
443 }
444 }
445 pub fn background_color(&self) -> Color {
446 self.background.get()
447 }
448 pub fn set_background_color(&self, value: Color) {
449 if self.background.replace(value) != value {
450 notify(&self.presenter);
451 }
452 }
453 pub fn foreground_color(&self) -> Color {
454 self.foreground.get()
455 }
456 pub fn set_foreground_color(&self, value: Color) {
457 if self.foreground.replace(value) != value {
458 notify(&self.presenter);
459 }
460 }
461 pub fn accent_color(&self) -> Color {
462 self.accent.get()
463 }
464 pub fn set_accent_color(&self, value: Color) {
465 if self.accent.replace(value) != value {
466 notify(&self.presenter);
467 }
468 }
469 pub fn angle(&self) -> Option<f64> {
471 self.measurement.get().map(|v| v.sweep.abs().to_degrees())
472 }
473 pub(crate) fn rays(&self) -> Option<(f64, f64)> {
474 self.measurement.get().map(|v| (v.start, v.last))
475 }
476 pub(crate) fn begin_ink(&self) {
477 self.measurement.set(None);
478 }
479 pub(crate) fn center(&self) -> Point {
480 Point::new(self.radius(), self.radius())
481 }
482 pub(crate) fn initialize_position(&self, size: Size) {
483 if self.transform_explicit.get() || !valid_canvas_size(size) {
484 return;
485 }
486 let scale = if self.radius_explicit.get() {
487 1.0
488 } else {
489 (size.width.min(size.height) * 0.35 / self.radius()).min(1.0)
490 };
491 let value = Matrix::create_scale(scale, scale)
492 * Matrix::create_translation(
493 size.width * 0.5 - self.radius() * scale,
494 size.height * 0.5 - self.radius() * scale,
495 );
496 if validate_transform(value, Size::new(self.radius() * 2.0, self.radius() * 2.0)).is_ok()
497 && self.transform.replace(value) != value
498 {
499 self.measurement.set(None);
500 }
501 }
502 pub(crate) fn apply_manipulation(
503 &self,
504 transform: Matrix,
505 radius: f64,
506 ) -> Result<(), InkError> {
507 validate_dimension(radius)?;
508 validate_transform(transform, Size::new(radius * 2.0, radius * 2.0))?;
509 self.transform_explicit.set(true);
510 self.radius_explicit.set(true);
511 if self.transform.get() != transform || self.radius.get() != radius {
512 self.transform.set(transform);
513 self.radius.set(radius);
514 self.measurement.set(None);
515 notify(&self.presenter);
516 }
517 Ok(())
518 }
519 pub(crate) fn hit_test(&self, point: Point) -> bool {
520 self.is_visible()
521 && self.transform().try_invert().is_some_and(|inverse| {
522 let p = point * inverse;
523 (p.x - self.radius()).hypot(p.y - self.radius()) <= self.radius()
524 })
525 }
526 pub(crate) fn blocks_ink(&self, point: Point) -> bool {
527 self.is_visible()
528 && self.transform().try_invert().is_some_and(|inverse| {
529 let p = point * inverse;
530 (p.x - self.radius()).hypot(p.y - self.radius()) < self.radius() - 1.0e-8
531 })
532 }
533 pub(crate) fn constrain(&self, point: Point, tolerance: f64) -> Point {
534 if !self.is_visible() {
535 return point;
536 }
537 let Some(inverse) = self.transform().try_invert() else {
538 return point;
539 };
540 let p = point * inverse;
541 let x = p.x - self.radius();
542 let y = p.y - self.radius();
543 let distance = x.hypot(y);
544 if distance <= f64::EPSILON {
545 return point;
546 }
547 let candidate = Point::new(
548 self.radius() + x * self.radius() / distance,
549 self.radius() + y * self.radius() / distance,
550 ) * self.transform();
551 if (candidate.x - point.x).hypot(candidate.y - point.y) > tolerance {
552 return point;
553 }
554 let angle = y.atan2(x);
555 let measurement = match self.measurement.get() {
556 Some(old) => ArcMeasurement {
557 start: old.start,
558 last: angle,
559 sweep: old.sweep
560 + (angle - old.last + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU)
561 - std::f64::consts::PI,
562 },
563 None => ArcMeasurement {
564 start: angle,
565 last: angle,
566 sweep: 0.0,
567 },
568 };
569 self.measurement.set(Some(measurement));
570 candidate
571 }
572}
573
574pub(crate) fn clip_stencil_segment(
578 from: InkPoint,
579 to: InkPoint,
580 ruler: &InkPresenterRuler,
581 protractor: &InkPresenterProtractor,
582) -> Vec<Vec<InkPoint>> {
583 let hidden = if ruler.is_visible() {
584 let Some(inverse) = ruler.transform().try_invert() else {
585 return vec![vec![from, to]];
586 };
587 rectangle_interval(
588 from.position * inverse,
589 to.position * inverse,
590 ruler.length(),
591 ruler.width(),
592 )
593 } else if protractor.is_visible() {
594 let Some(inverse) = protractor.transform().try_invert() else {
595 return vec![vec![from, to]];
596 };
597 let a = from.position * inverse;
598 let b = to.position * inverse;
599 let center = protractor.center();
600 let radius = protractor.radius();
601 let ax = a.x - center.x;
602 let ay = a.y - center.y;
603 let bx = b.x - center.x;
604 let by = b.y - center.y;
605 let epsilon = 1.0e-8 * radius.max(1.0);
606 if (ax.hypot(ay) - radius).abs() <= epsilon && (bx.hypot(by) - radius).abs() <= epsilon {
607 let start = ay.atan2(ax);
608 let sweep = (by.atan2(bx) - start + std::f64::consts::PI)
609 .rem_euclid(std::f64::consts::TAU)
610 - std::f64::consts::PI;
611 let transform = protractor.transform();
612 let world_radius = radius
613 * transform
614 .m11
615 .hypot(transform.m12)
616 .max(transform.m21.hypot(transform.m22));
617 let steps = ((sweep.abs() * world_radius / 2.0).ceil() as usize).clamp(1, 256);
618 let mut points = Vec::with_capacity(steps + 1);
619 points.push(from);
620 for step in 1..steps {
621 let t = step as f64 / steps as f64;
622 let angle = start + sweep * t;
623 let mut point = InkPoint::interpolate(from, to, t);
624 point.position = Point::new(
625 center.x + radius * angle.cos(),
626 center.y + radius * angle.sin(),
627 ) * transform;
628 points.push(point);
629 }
630 points.push(to);
631 return vec![points];
632 }
633 circle_interval(Point::new(ax, ay), Point::new(bx, by), radius)
634 } else {
635 None
636 };
637 let Some((entry, exit)) = hidden else {
638 return vec![vec![from, to]];
639 };
640 let mut pieces = Vec::with_capacity(2);
641 if entry > 0.0 {
642 pieces.push(vec![from, InkPoint::interpolate(from, to, entry)]);
643 }
644 if exit < 1.0 {
645 pieces.push(vec![InkPoint::interpolate(from, to, exit), to]);
646 }
647 pieces
648}
649
650fn rectangle_interval(from: Point, to: Point, width: f64, height: f64) -> Option<(f64, f64)> {
651 let mut entry = 0.0_f64;
652 let mut exit = 1.0_f64;
653 for (origin, delta, maximum) in [
654 (from.x, to.x - from.x, width),
655 (from.y, to.y - from.y, height),
656 ] {
657 if delta == 0.0 {
658 if origin <= 0.0 || origin >= maximum {
660 return None;
661 }
662 } else {
663 let first = -origin / delta;
664 let second = (maximum - origin) / delta;
665 entry = entry.max(first.min(second));
666 exit = exit.min(first.max(second));
667 if exit <= entry {
668 return None;
669 }
670 }
671 }
672 Some((entry, exit))
673}
674
675fn circle_interval(from: Point, to: Point, radius: f64) -> Option<(f64, f64)> {
676 let dx = to.x - from.x;
677 let dy = to.y - from.y;
678 let distance = dx.hypot(dy);
679 if distance <= f64::EPSILON {
680 return (from.x.hypot(from.y) < radius).then_some((0.0, 1.0));
681 }
682 let ux = dx / distance;
683 let uy = dy / distance;
684 let perpendicular = (from.x * uy - from.y * ux).abs();
685 if perpendicular >= radius {
686 return None;
687 }
688 let projection = -(from.x * ux + from.y * uy);
689 let half = ((radius - perpendicular) * (radius + perpendicular)).sqrt();
690 let entry = ((projection - half) / distance).max(0.0);
691 let exit = ((projection + half) / distance).min(1.0);
692 (exit > entry).then_some((entry, exit))
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 #[test]
699 fn dimensions_and_transforms_are_bounded_and_reject_perspective() {
700 let p = InkPresenter::new();
701 assert_eq!(InkPresenterStencilKind::Other as i32, 0);
702 assert_eq!(p.ruler().kind() as i32, 1);
703 assert_eq!(p.protractor().kind() as i32, 2);
704 for invalid in [0.0, -1.0, f64::INFINITY, f64::NAN, MAX_STENCIL_SIZE + 1.0] {
705 assert!(p.ruler().set_length(invalid).is_err());
706 assert!(p.ruler().set_width(invalid).is_err());
707 assert!(p.protractor().set_radius(invalid).is_err());
708 }
709 let mut perspective = Matrix::IDENTITY;
710 perspective.m13 = 0.01;
711 assert!(p.ruler().set_transform(perspective).is_err());
712 assert!(p.protractor().set_transform(Matrix::default()).is_err());
713 assert_eq!(p.ruler().transform(), Matrix::IDENTITY);
714 }
715 #[test]
716 fn default_guides_fit_canvas_but_explicit_transforms_remain_authoritative() {
717 let p = InkPresenter::new();
718 p.ruler().initialize_position(Size::new(300.0, 200.0));
719 p.protractor().initialize_position(Size::new(300.0, 200.0));
720 assert_eq!(
721 p.ruler().center() * p.ruler().transform(),
722 Point::new(150.0, 100.0)
723 );
724 assert_eq!(
725 p.protractor().center() * p.protractor().transform(),
726 Point::new(150.0, 100.0)
727 );
728 assert_eq!(p.ruler().length(), 1600.0);
729 assert_eq!(p.ruler().width(), 120.0);
730 assert_eq!(p.protractor().radius(), 280.0);
731 p.ruler().set_transform(Matrix::IDENTITY).unwrap();
732 p.ruler().initialize_position(Size::new(900.0, 800.0));
733 assert_eq!(p.ruler().transform(), Matrix::IDENTITY);
734 }
735 #[test]
736 fn protractor_tracks_arc_across_angle_wrap_and_resets_after_manipulation() {
737 let p = InkPresenter::new();
738 let guide = p.protractor();
739 guide.set_radius(100.0).unwrap();
740 guide.set_is_visible(true);
741 guide.begin_ink();
742 for degrees in [170.0_f64, 179.0, -179.0, -160.0] {
743 let radians = degrees.to_radians();
744 guide.constrain(
745 Point::new(100.0 + 103.0 * radians.cos(), 100.0 + 103.0 * radians.sin()),
746 12.0,
747 );
748 }
749 assert!((guide.angle().unwrap() - 30.0).abs() < 1e-9);
750 assert!(guide.rays().is_some());
751 guide
752 .set_transform(Matrix::create_translation(10.0, 20.0))
753 .unwrap();
754 assert_eq!(guide.angle(), None);
755 }
756
757 #[test]
758 fn sparse_ruler_crossing_splits_outside_fragments_and_preserves_boundary_tracing() {
759 let p = InkPresenter::new();
760 p.ruler().set_length(300.0).unwrap();
761 p.ruler().set_is_visible(true);
762 let mut from = InkPoint::new(Point::new(-20.0, 60.0), 0.2).unwrap();
763 from.timestamp = 100;
764 let mut to = InkPoint::new(Point::new(320.0, 60.0), 0.8).unwrap();
765 to.timestamp = 440;
766 let clipped = clip_stencil_segment(from, to, &p.ruler(), &p.protractor());
767 assert_eq!(clipped.len(), 2);
768 assert_eq!(clipped[0][1].position, Point::new(0.0, 60.0));
769 assert_eq!(clipped[1][0].position, Point::new(300.0, 60.0));
770 assert_eq!(clipped[0][1].timestamp, 120);
771 assert_eq!(clipped[1][0].timestamp, 420);
772 assert!(clipped[0][1].pressure > from.pressure);
773 from.position.y = 0.0;
774 to.position.y = 0.0;
775 assert_eq!(
776 clip_stencil_segment(from, to, &p.ruler(), &p.protractor()),
777 vec![vec![from, to]]
778 );
779 from.position = Point::new(50.0, 60.0);
780 to.position = Point::new(250.0, 60.0);
781 assert!(clip_stencil_segment(from, to, &p.ruler(), &p.protractor()).is_empty());
782 }
783
784 #[test]
785 fn sparse_circle_crossing_clips_but_snapped_points_follow_the_arc() {
786 let p = InkPresenter::new();
787 p.protractor().set_radius(100.0).unwrap();
788 p.protractor().set_is_visible(true);
789 let point = |x, y| InkPoint::new(Point::new(x, y), 0.5).unwrap();
790 let clipped = clip_stencil_segment(
791 point(-10.0, 100.0),
792 point(210.0, 100.0),
793 &p.ruler(),
794 &p.protractor(),
795 );
796 assert_eq!(clipped.len(), 2);
797 assert_eq!(clipped[0][1].position, Point::new(0.0, 100.0));
798 assert_eq!(clipped[1][0].position, Point::new(200.0, 100.0));
799 let arc = clip_stencil_segment(
800 point(200.0, 100.0),
801 point(100.0, 200.0),
802 &p.ruler(),
803 &p.protractor(),
804 );
805 assert_eq!(arc.len(), 1);
806 assert!(arc[0].len() > 2);
807 assert!(arc[0].len() <= 257);
808 for point in &arc[0] {
809 assert!(
810 ((point.position.x - 100.0).hypot(point.position.y - 100.0) - 100.0).abs() < 1e-9
811 );
812 }
813 }
814}