Skip to main content

ferroui_controls_inking/
ink_presenter_stencil.rs

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
24/// Shared programmatic contract of the two canvas stencil services.
25pub 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
116/// A movable straightedge. Its transform uses the local top-left corner.
117pub 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    /// Clockwise orientation of the ruler's horizontal edge, in degrees.
240    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
320/// A movable circular guide with a measured arc readout. The transform uses
321/// the bounding circle's top-left corner; its local center is `(radius,radius)`.
322pub 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    /// Compatibility spelling for the original portable API.
415    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    /// The absolute swept angle of the latest snapped arc, in degrees.
470    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
574/// Clips a sampled centerline against the visible guide, splitting sparse
575/// crossings into separate visible polylines. Points already snapped to the
576/// circular edge follow a bounded shortest arc instead of its obscured chord.
577pub(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            // A line on the boundary is visible, so it can trace the guide.
659            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}