1use crate::error::Result;
7use crate::graphics::{PathCommand, WindingRule};
8use std::fmt::Write;
9
10#[derive(Debug, Clone)]
12pub struct ClippingPath {
13 commands: Vec<PathCommand>,
15 winding_rule: WindingRule,
17 is_text_clip: bool,
19}
20
21impl ClippingPath {
22 pub fn new() -> Self {
24 Self {
25 commands: Vec::new(),
26 winding_rule: WindingRule::NonZero,
27 is_text_clip: false,
28 }
29 }
30
31 pub fn rect(x: f64, y: f64, width: f64, height: f64) -> Self {
33 let mut path = Self::new();
34 path.add_rect(x, y, width, height);
35 path
36 }
37
38 pub fn circle(cx: f64, cy: f64, radius: f64) -> Self {
40 let mut path = Self::new();
41 path.add_circle(cx, cy, radius);
42 path
43 }
44
45 pub fn ellipse(cx: f64, cy: f64, rx: f64, ry: f64) -> Self {
47 let mut path = Self::new();
48 path.add_ellipse(cx, cy, rx, ry);
49 path
50 }
51
52 pub fn with_winding_rule(mut self, rule: WindingRule) -> Self {
54 self.winding_rule = rule;
55 self
56 }
57
58 pub fn as_text_clip(mut self) -> Self {
60 self.is_text_clip = true;
61 self
62 }
63
64 pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
66 self.commands.push(PathCommand::MoveTo { x, y });
67 self
68 }
69
70 pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
72 self.commands.push(PathCommand::LineTo { x, y });
73 self
74 }
75
76 pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
78 self.commands.push(PathCommand::CurveTo {
79 x1,
80 y1,
81 x2,
82 y2,
83 x3,
84 y3,
85 });
86 self
87 }
88
89 pub fn add_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
91 self.commands.push(PathCommand::Rectangle {
92 x,
93 y,
94 width,
95 height,
96 });
97 self
98 }
99
100 pub fn add_circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
102 const KAPPA: f64 = 0.5522847498307933;
104 let k = radius * KAPPA;
105
106 self.move_to(cx, cy + radius);
108
109 self.curve_to(cx + k, cy + radius, cx + radius, cy + k, cx + radius, cy);
111
112 self.curve_to(cx + radius, cy - k, cx + k, cy - radius, cx, cy - radius);
114
115 self.curve_to(cx - k, cy - radius, cx - radius, cy - k, cx - radius, cy);
117
118 self.curve_to(cx - radius, cy + k, cx - k, cy + radius, cx, cy + radius);
120
121 self.close_path();
122 self
123 }
124
125 pub fn add_ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) -> &mut Self {
127 const KAPPA: f64 = 0.5522847498307933;
128 let kx = rx * KAPPA;
129 let ky = ry * KAPPA;
130
131 self.move_to(cx, cy + ry);
133
134 self.curve_to(cx + kx, cy + ry, cx + rx, cy + ky, cx + rx, cy);
136
137 self.curve_to(cx + rx, cy - ky, cx + kx, cy - ry, cx, cy - ry);
139
140 self.curve_to(cx - kx, cy - ry, cx - rx, cy - ky, cx - rx, cy);
142
143 self.curve_to(cx - rx, cy + ky, cx - kx, cy + ry, cx, cy + ry);
145
146 self.close_path();
147 self
148 }
149
150 pub fn add_rounded_rect(
152 &mut self,
153 x: f64,
154 y: f64,
155 width: f64,
156 height: f64,
157 radius: f64,
158 ) -> &mut Self {
159 let r = radius.min(width / 2.0).min(height / 2.0);
160 const KAPPA: f64 = 0.5522847498307933;
161 let k = r * KAPPA;
162
163 self.move_to(x + r, y);
165
166 self.line_to(x + width - r, y);
168
169 self.curve_to(x + width - r + k, y, x + width, y + r - k, x + width, y + r);
171
172 self.line_to(x + width, y + height - r);
174
175 self.curve_to(
177 x + width,
178 y + height - r + k,
179 x + width - r + k,
180 y + height,
181 x + width - r,
182 y + height,
183 );
184
185 self.line_to(x + r, y + height);
187
188 self.curve_to(
190 x + r - k,
191 y + height,
192 x,
193 y + height - r + k,
194 x,
195 y + height - r,
196 );
197
198 self.line_to(x, y + r);
200
201 self.curve_to(x, y + r - k, x + r - k, y, x + r, y);
203
204 self.close_path();
205 self
206 }
207
208 pub fn add_polygon(&mut self, points: &[(f64, f64)]) -> &mut Self {
210 if let Some((first, rest)) = points.split_first() {
211 self.move_to(first.0, first.1);
212 for point in rest {
213 self.line_to(point.0, point.1);
214 }
215 self.close_path();
216 }
217 self
218 }
219
220 pub fn close_path(&mut self) -> &mut Self {
222 self.commands.push(PathCommand::ClosePath);
223 self
224 }
225
226 pub fn is_empty(&self) -> bool {
228 self.commands.is_empty()
229 }
230
231 pub fn commands(&self) -> &[PathCommand] {
233 &self.commands
234 }
235
236 pub fn to_pdf_operations(&self) -> Result<String> {
238 let mut ops = String::new();
239
240 for cmd in &self.commands {
242 match cmd {
243 PathCommand::MoveTo { x, y } => {
244 writeln!(&mut ops, "{:.3} {:.3} m", x, y)
245 .expect("Writing to string should never fail");
246 }
247 PathCommand::LineTo { x, y } => {
248 writeln!(&mut ops, "{:.3} {:.3} l", x, y)
249 .expect("Writing to string should never fail");
250 }
251 PathCommand::CurveTo {
252 x1,
253 y1,
254 x2,
255 y2,
256 x3,
257 y3,
258 } => {
259 writeln!(
260 &mut ops,
261 "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c",
262 x1, y1, x2, y2, x3, y3
263 )
264 .expect("Writing to string should never fail");
265 }
266 PathCommand::Rectangle {
267 x,
268 y,
269 width,
270 height,
271 } => {
272 writeln!(&mut ops, "{:.3} {:.3} {:.3} {:.3} re", x, y, width, height)
273 .expect("Writing to string should never fail");
274 }
275 PathCommand::ClosePath => {
276 writeln!(&mut ops, "h").expect("Writing to string should never fail");
277 }
278 }
279 }
280
281 match self.winding_rule {
283 WindingRule::NonZero => {
284 writeln!(&mut ops, "W").expect("Writing to string should never fail")
285 }
286 WindingRule::EvenOdd => {
287 writeln!(&mut ops, "W*").expect("Writing to string should never fail")
288 }
289 }
290
291 writeln!(&mut ops, "n").expect("Writing to string should never fail");
293
294 Ok(ops)
295 }
296
297 pub fn intersect(&mut self, other: &ClippingPath) -> &mut Self {
299 self.commands.extend_from_slice(&other.commands);
302 self
303 }
304
305 pub fn clear(&mut self) {
307 self.commands.clear();
308 }
309}
310
311impl Default for ClippingPath {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct ClippingRegion {
320 stack: Vec<ClippingPath>,
322 current: Option<ClippingPath>,
324}
325
326impl ClippingRegion {
327 pub fn new() -> Self {
329 Self {
330 stack: Vec::new(),
331 current: None,
332 }
333 }
334
335 pub fn set_clip(&mut self, path: ClippingPath) {
337 self.current = Some(path);
338 }
339
340 pub fn clear_clip(&mut self) {
342 self.current = None;
343 }
344
345 pub fn save(&mut self) {
347 if let Some(ref current) = self.current {
348 self.stack.push(current.clone());
349 }
350 }
351
352 pub fn restore(&mut self) {
354 if let Some(saved) = self.stack.pop() {
355 self.current = Some(saved);
356 }
357 }
358
359 pub fn current(&self) -> Option<&ClippingPath> {
361 self.current.as_ref()
362 }
363
364 pub fn has_clip(&self) -> bool {
366 self.current.is_some()
367 }
368
369 pub fn to_pdf_operations(&self) -> Result<Option<String>> {
371 if let Some(ref clip) = self.current {
372 Ok(Some(clip.to_pdf_operations()?))
373 } else {
374 Ok(None)
375 }
376 }
377}
378
379impl Default for ClippingRegion {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_clipping_path_creation() {
391 let path = ClippingPath::new();
392 assert!(path.is_empty());
393 assert!(!path.is_text_clip);
394 assert_eq!(path.winding_rule, WindingRule::NonZero);
395 }
396
397 #[test]
398 fn test_rect_clipping_path() {
399 let path = ClippingPath::rect(10.0, 20.0, 100.0, 50.0);
400 assert!(!path.is_empty());
401 assert_eq!(path.commands.len(), 1);
402 }
403
404 #[test]
405 fn test_circle_clipping_path() {
406 let path = ClippingPath::circle(50.0, 50.0, 25.0);
407 assert!(!path.is_empty());
408 assert!(path.commands.len() >= 6);
410 }
411
412 #[test]
413 fn test_ellipse_clipping_path() {
414 let path = ClippingPath::ellipse(50.0, 50.0, 30.0, 20.0);
415 assert!(!path.is_empty());
416 assert!(path.commands.len() >= 6);
417 }
418
419 #[test]
420 fn test_winding_rule() {
421 let path = ClippingPath::new().with_winding_rule(WindingRule::EvenOdd);
422 assert_eq!(path.winding_rule, WindingRule::EvenOdd);
423 }
424
425 #[test]
426 fn test_text_clip() {
427 let path = ClippingPath::new().as_text_clip();
428 assert!(path.is_text_clip);
429 }
430
431 #[test]
432 fn test_path_construction() {
433 let mut path = ClippingPath::new();
434 path.move_to(0.0, 0.0)
435 .line_to(100.0, 0.0)
436 .line_to(100.0, 100.0)
437 .line_to(0.0, 100.0)
438 .close_path();
439
440 assert_eq!(path.commands.len(), 5);
441 }
442
443 #[test]
444 fn test_curve_to() {
445 let mut path = ClippingPath::new();
446 path.move_to(0.0, 0.0)
447 .curve_to(10.0, 10.0, 20.0, 20.0, 30.0, 30.0);
448
449 assert_eq!(path.commands.len(), 2);
450 }
451
452 #[test]
453 fn test_polygon() {
454 let mut path = ClippingPath::new();
455 let points = vec![(0.0, 0.0), (50.0, 0.0), (25.0, 50.0)];
456 path.add_polygon(&points);
457
458 assert_eq!(path.commands.len(), 4);
460 }
461
462 #[test]
463 fn test_rounded_rect() {
464 let mut path = ClippingPath::new();
465 path.add_rounded_rect(10.0, 10.0, 100.0, 50.0, 5.0);
466
467 assert!(path.commands.len() >= 10);
469 }
470
471 #[test]
472 fn test_pdf_operations_nonzero() {
473 let path = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
474 let ops = path
475 .to_pdf_operations()
476 .expect("Writing to string should never fail");
477
478 assert!(ops.contains("0.000 0.000 100.000 100.000 re"));
479 assert!(ops.contains("W")); assert!(ops.contains("n")); }
482
483 #[test]
484 fn test_pdf_operations_evenodd() {
485 let path =
486 ClippingPath::rect(0.0, 0.0, 100.0, 100.0).with_winding_rule(WindingRule::EvenOdd);
487 let ops = path
488 .to_pdf_operations()
489 .expect("Writing to string should never fail");
490
491 assert!(ops.contains("W*")); }
493
494 #[test]
495 fn test_intersect_paths() {
496 let mut path1 = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
497 let path2 = ClippingPath::rect(50.0, 50.0, 100.0, 100.0);
498
499 path1.intersect(&path2);
500 assert_eq!(path1.commands.len(), 2);
501 }
502
503 #[test]
504 fn test_clear_path() {
505 let mut path = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
506 assert!(!path.is_empty());
507
508 path.clear();
509 assert!(path.is_empty());
510 }
511
512 #[test]
513 fn test_clipping_region_creation() {
514 let region = ClippingRegion::new();
515 assert!(!region.has_clip());
516 assert!(region.current().is_none());
517 }
518
519 #[test]
520 fn test_clipping_region_set_clip() {
521 let mut region = ClippingRegion::new();
522 let path = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
523
524 region.set_clip(path);
525 assert!(region.has_clip());
526 assert!(region.current().is_some());
527 }
528
529 #[test]
530 fn test_clipping_region_clear() {
531 let mut region = ClippingRegion::new();
532 region.set_clip(ClippingPath::rect(0.0, 0.0, 100.0, 100.0));
533 assert!(region.has_clip());
534
535 region.clear_clip();
536 assert!(!region.has_clip());
537 }
538
539 #[test]
540 fn test_clipping_region_save_restore() {
541 let mut region = ClippingRegion::new();
542 let path1 = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
543 let path2 = ClippingPath::rect(50.0, 50.0, 50.0, 50.0);
544
545 region.set_clip(path1);
546 region.save();
547 region.set_clip(path2);
548
549 assert!(region.has_clip());
551
552 region.restore();
553 assert!(region.has_clip());
555 }
556
557 #[test]
558 fn test_clipping_region_pdf_operations() {
559 let mut region = ClippingRegion::new();
560
561 let ops = region
563 .to_pdf_operations()
564 .expect("Writing to string should never fail");
565 assert!(ops.is_none());
566
567 region.set_clip(ClippingPath::rect(0.0, 0.0, 100.0, 100.0));
569 let ops = region
570 .to_pdf_operations()
571 .expect("Writing to string should never fail");
572 assert!(ops.is_some());
573 assert!(ops.unwrap().contains("re"));
574 }
575
576 #[test]
577 fn test_complex_clipping_path() {
578 let mut path = ClippingPath::new();
579 path.move_to(10.0, 10.0)
580 .line_to(50.0, 10.0)
581 .curve_to(60.0, 10.0, 70.0, 20.0, 70.0, 30.0)
582 .line_to(70.0, 50.0)
583 .close_path();
584
585 let ops = path
586 .to_pdf_operations()
587 .expect("Writing to string should never fail");
588 assert!(ops.contains("10.000 10.000 m"));
589 assert!(ops.contains("50.000 10.000 l"));
590 assert!(ops.contains("c")); assert!(ops.contains("h")); assert!(ops.contains("W")); assert!(ops.contains("n")); }
595}