1use ferroui_base::Point;
2use std::fmt;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
6pub enum InkError {
7 Platform(String),
8 InvalidOperation(&'static str),
9 InvalidInput(&'static str),
10 InvalidPoint,
11 InvalidDrawingAttributes,
12 InvalidTransform,
13 EmptyStroke,
14 DuplicateStrokeId(u32),
15 StrokeIdExhausted,
16 InvalidData(&'static str),
17 UnsupportedFormat,
18 LimitExceeded,
19}
20
21impl fmt::Display for InkError {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self {
24 Self::Platform(reason) => f.write_str(reason),
25 Self::InvalidOperation(reason) | Self::InvalidInput(reason) => f.write_str(reason),
26 Self::InvalidPoint => f.write_str(
27 "Ink coordinates and device measurements must be finite and within range",
28 ),
29 Self::InvalidDrawingAttributes => f.write_str("Invalid ink drawing attributes"),
30 Self::InvalidTransform => f.write_str("Ink transforms must be finite affine matrices"),
31 Self::EmptyStroke => f.write_str("An ink stroke must contain at least one point"),
32 Self::DuplicateStrokeId(id) => write!(f, "Duplicate ink stroke ID {id}"),
33 Self::StrokeIdExhausted => f.write_str("Ink stroke IDs are exhausted"),
34 Self::InvalidData(reason) => write!(f, "Invalid ink document: {reason}"),
35 Self::UnsupportedFormat => f.write_str("Unsupported ink persistence format"),
36 Self::LimitExceeded => {
37 f.write_str("Ink document exceeds the configured resource limits")
38 }
39 }
40 }
41}
42
43impl std::error::Error for InkError {}
44
45#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct InkPoint {
50 pub position: Point,
51 pub pressure: f32,
52 pub tilt_x: f32,
53 pub tilt_y: f32,
54 pub twist: f32,
55 pub timestamp: u64,
56}
57
58impl InkPoint {
59 pub fn new(position: Point, pressure: f32) -> Result<Self, InkError> {
60 let point = Self {
61 position,
62 pressure,
63 tilt_x: 0.0,
64 tilt_y: 0.0,
65 twist: 0.0,
66 timestamp: 0,
67 };
68 point.validate()?;
69 Ok(point)
70 }
71
72 pub fn validate(&self) -> Result<(), InkError> {
73 if !valid_position(self.position)
74 || !self.pressure.is_finite()
75 || !(0.0..=1.0).contains(&self.pressure)
76 || !self.tilt_x.is_finite()
77 || !(-90.0..=90.0).contains(&self.tilt_x)
78 || !self.tilt_y.is_finite()
79 || !(-90.0..=90.0).contains(&self.tilt_y)
80 || !self.twist.is_finite()
81 || !(0.0..360.0).contains(&self.twist)
82 {
83 return Err(InkError::InvalidPoint);
84 }
85 Ok(())
86 }
87
88 pub(crate) fn interpolate(a: Self, b: Self, t: f64) -> Self {
89 if t <= 0.0 {
93 return a;
94 }
95 if t >= 1.0 {
96 return b;
97 }
98 let mix = |a: f32, b: f32| (f64::from(a) + (f64::from(b) - f64::from(a)) * t) as f32;
99 let twist_delta =
101 (f64::from(b.twist) - f64::from(a.twist) + 540.0).rem_euclid(360.0) - 180.0;
102 let twist = (f64::from(a.twist) + twist_delta * t).rem_euclid(360.0) as f32;
103 Self {
104 position: Point::new(
105 a.position.x + (b.position.x - a.position.x) * t,
106 a.position.y + (b.position.y - a.position.y) * t,
107 ),
108 pressure: mix(a.pressure, b.pressure),
109 tilt_x: mix(a.tilt_x, b.tilt_x),
110 tilt_y: mix(a.tilt_y, b.tilt_y),
111 twist: if twist >= 360.0 { 0.0 } else { twist },
114 timestamp: a
115 .timestamp
116 .saturating_add(((b.timestamp.saturating_sub(a.timestamp)) as f64 * t) as u64)
117 .min(b.timestamp),
118 }
119 }
120}
121
122pub(crate) fn valid_position(value: Point) -> bool {
123 value.x.is_finite() && value.y.is_finite() && value.x.abs() <= 1.0e12 && value.y.abs() <= 1.0e12
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 #[test]
131 fn validates_device_measurements() {
132 assert!(InkPoint::new(Point::new(f64::NAN, 0.0), 0.5).is_err());
133 assert!(InkPoint::new(Point::default(), -0.1).is_err());
134 assert!(InkPoint::new(Point::default(), 0.0).is_ok());
135 let mut p = InkPoint::new(Point::default(), 0.5).unwrap();
136 p.tilt_y = 91.0;
137 assert!(p.validate().is_err());
138 }
139 #[test]
140 fn twist_interpolates_across_zero() {
141 let mut a = InkPoint::new(Point::default(), 0.5).unwrap();
142 let mut b = a;
143 a.twist = 359.0;
144 b.twist = 1.0;
145 assert_eq!(InkPoint::interpolate(a, b, 0.5).twist, 0.0);
146 }
147 #[test]
148 fn interpolation_preserves_original_endpoints_and_large_timestamps() {
149 let mut a = InkPoint::new(Point::new(1.0e12, 1.0e12), 1.0).unwrap();
150 a.timestamp = 1;
151 let mut b = InkPoint::new(Point::new(0.001, 0.001), 1.0e-8).unwrap();
152 b.timestamp = (1u64 << 54) + 3;
153 assert_eq!(InkPoint::interpolate(a, b, 0.0), a);
154 assert_eq!(InkPoint::interpolate(a, b, 1.0), b);
155 let near_end = InkPoint::interpolate(a, b, 1.0 - f64::EPSILON);
156 assert!(near_end.timestamp <= b.timestamp);
157 near_end.validate().unwrap();
158 }
159 #[test]
160 fn twist_rounding_never_generates_an_invalid_360_degree_sample() {
161 let a = InkPoint::new(Point::default(), 0.5).unwrap();
162 let mut b = a;
163 b.twist = 359.99;
164 let sample = InkPoint::interpolate(a, b, 0.001);
165 sample.validate().unwrap();
166 assert_eq!(sample.twist, 0.0);
167 }
168}