1#[derive(Clone, Copy, Debug, Default, PartialEq)]
5pub struct Point2D {
6 pub x: f64,
7 pub y: f64,
8}
9
10impl Point2D {
11 #[inline]
12 pub const fn new(x: f64, y: f64) -> Self {
13 Self { x, y }
14 }
15
16 #[inline]
17 pub fn distance_to(&self, other: &Point2D) -> f64 {
18 ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
19 }
20}
21
22#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct Transform2D {
30 pub a: f64,
31 pub b: f64,
32 pub c: f64,
33 pub d: f64,
34 pub e: f64,
35 pub f: f64,
36}
37
38impl Default for Transform2D {
39 fn default() -> Self {
40 Self::identity()
41 }
42}
43
44impl Transform2D {
45 pub const fn identity() -> Self {
46 Self {
47 a: 1.0,
48 b: 0.0,
49 c: 0.0,
50 d: 1.0,
51 e: 0.0,
52 f: 0.0,
53 }
54 }
55
56 #[inline]
57 pub fn is_identity(&self) -> bool {
58 (self.a - 1.0).abs() < 1e-9
59 && self.b.abs() < 1e-9
60 && self.c.abs() < 1e-9
61 && (self.d - 1.0).abs() < 1e-9
62 && self.e.abs() < 1e-9
63 && self.f.abs() < 1e-9
64 }
65
66 #[inline]
67 pub fn apply(&self, p: Point2D) -> Point2D {
68 Point2D::new(
69 self.a * p.x + self.c * p.y + self.e,
70 self.b * p.x + self.d * p.y + self.f,
71 )
72 }
73
74 pub fn multiply(&self, rhs: &Self) -> Self {
75 Self {
76 a: self.a * rhs.a + self.c * rhs.b,
77 b: self.b * rhs.a + self.d * rhs.b,
78 c: self.a * rhs.c + self.c * rhs.d,
79 d: self.b * rhs.c + self.d * rhs.d,
80 e: self.a * rhs.e + self.c * rhs.f + self.e,
81 f: self.b * rhs.e + self.d * rhs.f + self.f,
82 }
83 }
84
85 pub fn translate(tx: f64, ty: f64) -> Self {
86 Self {
87 a: 1.0,
88 b: 0.0,
89 c: 0.0,
90 d: 1.0,
91 e: tx,
92 f: ty,
93 }
94 }
95
96 pub fn scale(sx: f64, sy: f64) -> Self {
97 Self {
98 a: sx,
99 b: 0.0,
100 c: 0.0,
101 d: sy,
102 e: 0.0,
103 f: 0.0,
104 }
105 }
106
107 pub fn rotate(angle_rad: f64) -> Self {
108 let cos = angle_rad.cos();
109 let sin = angle_rad.sin();
110 Self {
111 a: cos,
112 b: sin,
113 c: -sin,
114 d: cos,
115 e: 0.0,
116 f: 0.0,
117 }
118 }
119}
120
121pub fn parse_transform(s: &str) -> Transform2D {
123 let mut tf = Transform2D::identity();
124 let s = s.trim();
125 if s.is_empty() {
126 return tf;
127 }
128
129 let mut remaining = s;
130 while let Some(open_paren) = remaining.find('(') {
131 let op_name = remaining[..open_paren].trim();
132 let close_paren = match remaining[open_paren + 1..].find(')') {
133 Some(idx) => open_paren + 1 + idx,
134 None => break,
135 };
136 let args_str = &remaining[open_paren + 1..close_paren];
137
138 let mut args = [0.0f64; 6];
140 let mut argc = 0;
141 for part in args_str.split([' ', ',']) {
142 let part = part.trim();
143 if !part.is_empty()
144 && let Ok(v) = part.parse::<f64>()
145 && argc < 6
146 {
147 args[argc] = v;
148 argc += 1;
149 }
150 }
151
152 let op = op_name.split_whitespace().last().unwrap_or(op_name);
153
154 let cur = match op {
155 "matrix" if argc >= 6 => Transform2D {
156 a: args[0],
157 b: args[1],
158 c: args[2],
159 d: args[3],
160 e: args[4],
161 f: args[5],
162 },
163 "translate" if argc > 0 => {
164 let tx = args[0];
165 let ty = if argc > 1 { args[1] } else { 0.0 };
166 Transform2D::translate(tx, ty)
167 }
168 "scale" if argc > 0 => {
169 let sx = args[0];
170 let sy = if argc > 1 { args[1] } else { sx };
171 Transform2D::scale(sx, sy)
172 }
173 "rotate" if argc > 0 => {
174 let rad = args[0].to_radians();
175 if argc >= 3 {
176 let cx = args[1];
177 let cy = args[2];
178 Transform2D::translate(cx, cy)
179 .multiply(&Transform2D::rotate(rad))
180 .multiply(&Transform2D::translate(-cx, -cy))
181 } else {
182 Transform2D::rotate(rad)
183 }
184 }
185 "skewx" | "skewX" if argc > 0 => {
186 let tan = args[0].to_radians().tan();
187 Transform2D {
188 a: 1.0,
189 b: 0.0,
190 c: tan,
191 d: 1.0,
192 e: 0.0,
193 f: 0.0,
194 }
195 }
196 "skewy" | "skewY" if argc > 0 => {
197 let tan = args[0].to_radians().tan();
198 Transform2D {
199 a: 1.0,
200 b: tan,
201 c: 0.0,
202 d: 1.0,
203 e: 0.0,
204 f: 0.0,
205 }
206 }
207 _ => Transform2D::identity(),
208 };
209
210 tf = tf.multiply(&cur);
211 remaining = &remaining[close_paren + 1..];
212 }
213
214 tf
215}
216
217#[inline]
219pub fn sample_cubic_bezier<F>(
220 p0: Point2D,
221 p1: Point2D,
222 p2: Point2D,
223 p3: Point2D,
224 steps: usize,
225 mut on_point: F,
226) where
227 F: FnMut(Point2D),
228{
229 let n = steps.max(1);
230 for step in 1..=n {
231 let t = (step as f64) / (n as f64);
232 let u = 1.0 - t;
233 let x =
234 u * u * u * p0.x + 3.0 * u * u * t * p1.x + 3.0 * u * t * t * p2.x + t * t * t * p3.x;
235 let y =
236 u * u * u * p0.y + 3.0 * u * u * t * p1.y + 3.0 * u * t * t * p2.y + t * t * t * p3.y;
237 on_point(Point2D::new(x, y));
238 }
239}
240
241#[inline]
243pub fn sample_quad_bezier<F>(p0: Point2D, p1: Point2D, p2: Point2D, steps: usize, mut on_point: F)
244where
245 F: FnMut(Point2D),
246{
247 let n = steps.max(1);
248 for step in 1..=n {
249 let t = (step as f64) / (n as f64);
250 let u = 1.0 - t;
251 let x = u * u * p0.x + 2.0 * u * t * p1.x + t * t * p2.x;
252 let y = u * u * p0.y + 2.0 * u * t * p1.y + t * t * p2.y;
253 on_point(Point2D::new(x, y));
254 }
255}
256
257#[allow(clippy::too_many_arguments)]
259pub fn sample_elliptical_arc<F>(
260 p0: Point2D,
261 p1: Point2D,
262 mut rx: f64,
263 mut ry: f64,
264 x_axis_rotation_deg: f64,
265 large_arc_flag: bool,
266 sweep_flag: bool,
267 steps: usize,
268 mut on_point: F,
269) where
270 F: FnMut(Point2D),
271{
272 if (p0.x - p1.x).abs() < 1e-9 && (p0.y - p1.y).abs() < 1e-9 {
273 return;
274 }
275 if rx.abs() < 1e-9 || ry.abs() < 1e-9 {
276 on_point(p1);
277 return;
278 }
279
280 rx = rx.abs();
281 ry = ry.abs();
282 let phi = x_axis_rotation_deg.to_radians();
283 let cos_phi = phi.cos();
284 let sin_phi = phi.sin();
285
286 let dx = (p0.x - p1.x) / 2.0;
288 let dy = (p0.y - p1.y) / 2.0;
289 let x1_prime = cos_phi * dx + sin_phi * dy;
290 let y1_prime = -sin_phi * dx + cos_phi * dy;
291
292 let lambda = (x1_prime * x1_prime) / (rx * rx) + (y1_prime * y1_prime) / (ry * ry);
294 if lambda > 1.0 {
295 let scale = lambda.sqrt();
296 rx *= scale;
297 ry *= scale;
298 }
299
300 let sign = if large_arc_flag != sweep_flag {
302 1.0
303 } else {
304 -1.0
305 };
306 let rx_sq = rx * rx;
307 let ry_sq = ry * ry;
308 let x1_sq = x1_prime * x1_prime;
309 let y1_sq = y1_prime * y1_prime;
310
311 let numerator = (rx_sq * ry_sq - rx_sq * y1_sq - ry_sq * x1_sq).max(0.0);
312 let denominator = rx_sq * y1_sq + ry_sq * x1_sq;
313 let sq = if denominator > 0.0 {
314 (numerator / denominator).sqrt()
315 } else {
316 0.0
317 };
318 let cx_prime = sign * sq * (rx * y1_prime / ry);
319 let cy_prime = sign * sq * -(ry * x1_prime / rx);
320
321 let cx = cos_phi * cx_prime - sin_phi * cy_prime + (p0.x + p1.x) / 2.0;
323 let cy = sin_phi * cx_prime + cos_phi * cy_prime + (p0.y + p1.y) / 2.0;
324
325 let ux = (x1_prime - cx_prime) / rx;
327 let uy = (y1_prime - cy_prime) / ry;
328 let vx = (-x1_prime - cx_prime) / rx;
329 let vy = (-y1_prime - cy_prime) / ry;
330
331 let vector_angle = |u_x: f64, u_y: f64, v_x: f64, v_y: f64| -> f64 {
332 let dot = u_x * v_x + u_y * v_y;
333 let len = (u_x * u_x + u_y * u_y).sqrt() * (v_x * v_x + v_y * v_y).sqrt();
334 let val = if len > 0.0 {
335 (dot / len).clamp(-1.0, 1.0)
336 } else {
337 0.0
338 };
339 let mut ang = val.acos();
340 if u_x * v_y - u_y * v_x < 0.0 {
341 ang = -ang;
342 }
343 ang
344 };
345
346 let theta1 = vector_angle(1.0, 0.0, ux, uy);
347 let mut d_theta = vector_angle(ux, uy, vx, vy);
348
349 let two_pi = std::f64::consts::PI * 2.0;
350 if !sweep_flag && d_theta > 0.0 {
351 d_theta -= two_pi;
352 } else if sweep_flag && d_theta < 0.0 {
353 d_theta += two_pi;
354 }
355
356 let n = steps.max(4);
357 for i in 1..=n {
358 let t = i as f64 / n as f64;
359 let angle = theta1 + t * d_theta;
360 let cos_a = angle.cos();
361 let sin_a = angle.sin();
362 let ex = rx * cos_a;
363 let ey = ry * sin_a;
364 let x = cos_phi * ex - sin_phi * ey + cx;
365 let y = sin_phi * ex + cos_phi * ey + cy;
366 on_point(Point2D::new(x, y));
367 }
368}