1pub mod pucker_bloat;
5pub mod zigzag;
6pub use pucker_bloat::{pucker_bloat_path, pucker_bloat_vector_path};
7pub use zigzag::zigzag_path;
8
9use kurbo::ParamCurveNearest;
10pub use kurbo::{Affine, BezPath, CubicBez, PathEl, Point, Rect, Shape as KurboShape, Vec2};
11
12use glam::DVec2;
13
14pub fn normalize_dash_pattern(pattern: &[f64]) -> Option<Vec<f64>> {
24 if pattern.is_empty()
25 || pattern.iter().any(|x| !x.is_finite() || *x < 0.0)
26 || pattern.iter().sum::<f64>() <= 1e-9
27 {
28 return None;
29 }
30
31 Some(pattern.to_vec())
32}
33
34pub fn dash_bez_path(path: &BezPath, pattern: &[f64], offset: f64) -> Option<BezPath> {
42 let pattern = normalize_dash_pattern(pattern)?;
43
44 if !offset.is_finite() {
45 return None;
46 }
47
48 let elements = kurbo::dash(path.elements().iter().copied(), offset, &pattern).collect();
49
50 Some(BezPath::from_vec(elements))
51}
52
53#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
54pub struct VectorPath {
55 pub anchors: Vec<Anchor>,
56 pub closed: bool,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
60pub struct Anchor {
61 pub pos: DVec2,
62 pub tan_in: DVec2, pub tan_out: DVec2, pub mode: TangentMode,
65}
66
67impl Anchor {
68 pub fn corner(pos: DVec2) -> Self {
69 Self {
70 pos,
71 tan_in: DVec2::ZERO,
72 tan_out: DVec2::ZERO,
73 mode: TangentMode::Corner,
74 }
75 }
76 pub fn symmetric(pos: DVec2, tan_out: DVec2) -> Self {
77 Self {
78 pos,
79 tan_in: -tan_out,
80 tan_out,
81 mode: TangentMode::Symmetric,
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
88pub enum TangentMode {
89 Corner,
90 Smooth,
91 Symmetric,
92}
93
94impl TangentMode {
95 pub fn cycled(self) -> Self {
96 match self {
97 TangentMode::Corner => TangentMode::Smooth,
98 TangentMode::Smooth => TangentMode::Symmetric,
99 TangentMode::Symmetric => TangentMode::Corner,
100 }
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum BooleanOp {
106 Union,
107 Intersection,
108 Difference,
109 Xor,
110}
111
112#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
114pub enum AnchorEdit {
115 SetPos {
116 index: usize,
117 pos: DVec2,
118 },
119 SetTanIn {
120 index: usize,
121 tan: DVec2,
122 },
123 SetTanOut {
124 index: usize,
125 tan: DVec2,
126 },
127 SetMode {
128 index: usize,
129 mode: TangentMode,
130 #[serde(default)]
133 tan_in: Option<DVec2>,
134 #[serde(default)]
135 tan_out: Option<DVec2>,
136 },
137 Delete {
138 index: usize,
139 },
140 Insert {
141 index: usize,
142 anchor: Anchor,
143 },
144 SetClosed {
145 closed: bool,
146 },
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum PathHit {
151 OnPath,
152 Inside,
153}
154
155#[derive(Clone, Copy, Debug, thiserror::Error)]
156pub enum GeometryError {
157 #[error("segment index {0} out of range")]
158 SegmentOutOfRange(usize),
159 #[error("anchor index {0} out of range")]
160 AnchorOutOfRange(usize),
161 #[error("split parameter {0} is not finite or outside [0,1]")]
162 InvalidSplitParam(f64),
163}
164
165fn pt(v: DVec2) -> Point {
166 Point::new(v.x, v.y)
167}
168
169impl VectorPath {
170 pub fn segment_count(&self) -> usize {
171 let n = self.anchors.len();
172 if self.closed { n } else { n.saturating_sub(1) }
173 }
174
175 pub fn to_bez_path(&self) -> BezPath {
176 let mut p = BezPath::new();
177 let n = self.anchors.len();
178 if n == 0 {
179 return p;
180 }
181 p.move_to(pt(self.anchors[0].pos));
182 for i in 0..self.segment_count() {
183 let a = &self.anchors[i];
184 let b = &self.anchors[(i + 1) % n];
185 p.curve_to(pt(a.pos + a.tan_out), pt(b.pos + b.tan_in), pt(b.pos));
186 }
187 if self.closed {
188 p.close_path();
189 }
190 p
191 }
192
193 pub fn from_bez_path(path: &BezPath) -> Self {
194 let mut out = VectorPath::default();
195 let mut start = DVec2::ZERO;
196 for el in path.elements() {
197 match *el {
198 PathEl::MoveTo(p) => {
199 let v = DVec2::new(p.x, p.y);
200 start = v;
201 out.anchors.push(Anchor::corner(v));
202 }
203 PathEl::LineTo(p) => out.anchors.push(Anchor::corner(DVec2::new(p.x, p.y))),
204 PathEl::QuadTo(q1, q2) => {
205 let prev = out.anchors.last().map(|a| a.pos).unwrap_or_default();
207 let q1 = DVec2::new(q1.x, q1.y);
208 let end = DVec2::new(q2.x, q2.y);
209 let c1 = prev + (q1 - prev) * (2.0 / 3.0);
210 let c2 = end + (q1 - end) * (2.0 / 3.0);
211 if let Some(last) = out.anchors.last_mut() {
212 last.tan_out = c1 - last.pos;
213 }
214 let mut a = Anchor::corner(end);
215 a.tan_in = c2 - end;
216 out.anchors.push(a);
217 }
218 PathEl::CurveTo(c1, c2, p) => {
219 let (c1, c2, end) = (
220 DVec2::new(c1.x, c1.y),
221 DVec2::new(c2.x, c2.y),
222 DVec2::new(p.x, p.y),
223 );
224 if let Some(last) = out.anchors.last_mut() {
225 last.tan_out = c1 - last.pos;
226 }
227 let mut a = Anchor::corner(end);
228 a.tan_in = c2 - end;
229 out.anchors.push(a);
230 }
231 PathEl::ClosePath => {
232 out.closed = true;
233 if out.anchors.len() >= 2 {
235 let last = *out.anchors.last().unwrap();
236 if (last.pos - start).length_squared() < 1e-12 {
237 out.anchors[0].tan_in = last.tan_in;
238 out.anchors.pop();
239 }
240 }
241 }
242 }
243 }
244 for a in &mut out.anchors {
245 a.mode = detect_mode(a.tan_in, a.tan_out);
246 }
247 out
248 }
249
250 pub fn nearest_segment(&self, point: DVec2) -> Option<(usize, f64, f64)> {
252 if self.anchors.len() < 2 {
253 return None;
254 }
255
256 let q = pt(point);
257 let n = self.anchors.len();
258 let seg_count = self.segment_count();
259
260 let mut best_seg = 0usize;
261 let mut best_t = 0.0;
262 let mut best_dist = f64::MAX;
263
264 for i in 0..seg_count {
265 let a = &self.anchors[i];
266 let b = &self.anchors[(i + 1) % n];
267 let cubic = CubicBez::new(
268 pt(a.pos),
269 pt(a.pos + a.tan_out),
270 pt(b.pos + b.tan_in),
271 pt(b.pos),
272 );
273 let hit = cubic.nearest(q, 1e-6);
274 let dist = hit.distance_sq.sqrt();
275 if dist < best_dist {
276 best_seg = i;
277 best_t = hit.t;
278 best_dist = dist;
279 }
280 }
281
282 Some((best_seg, best_t, best_dist))
283 }
284
285 pub fn hit_test(&self, p: DVec2, tol: f64) -> Option<PathHit> {
286 let path = self.to_bez_path();
287 let q = pt(p);
288 let mut best_sq = f64::MAX;
289 for seg in path.segments() {
290 best_sq = best_sq.min(seg.nearest(q, 1e-6).distance_sq);
291 }
292 if best_sq.sqrt() <= tol {
293 return Some(PathHit::OnPath);
294 }
295 if self.closed && path.contains(q) {
296 return Some(PathHit::Inside);
297 }
298 None
299 }
300
301 pub fn insert_anchor_at(&mut self, seg: usize, t: f64) -> Result<(), GeometryError> {
303 if seg >= self.segment_count() {
304 return Err(GeometryError::SegmentOutOfRange(seg));
305 }
306 if !t.is_finite() || t < 0.0 || t > 1.0 {
307 return Err(GeometryError::InvalidSplitParam(t));
308 }
309 let n = self.anchors.len();
310 let (i, j) = (seg, (seg + 1) % n);
311 let a = self.anchors[i];
312 let b = self.anchors[j];
313 let (p0, p1, p2, p3) = (a.pos, a.pos + a.tan_out, b.pos + b.tan_in, b.pos);
314 let q0 = p0.lerp(p1, t);
315 let q1 = p1.lerp(p2, t);
316 let q2 = p2.lerp(p3, t);
317 let r0 = q0.lerp(q1, t);
318 let r1 = q1.lerp(q2, t);
319 let s = r0.lerp(r1, t);
320 self.anchors[i].tan_out = q0 - p0;
321 self.anchors[j].tan_in = q2 - p3;
322 self.anchors.insert(
323 i + 1,
324 Anchor {
325 pos: s,
326 tan_in: r0 - s,
327 tan_out: r1 - s,
328 mode: TangentMode::Smooth,
329 },
330 );
331 Ok(())
332 }
333
334 pub fn round_corners(&self, radius: f64) -> VectorPath {
338 if radius <= 1e-9 || self.anchors.len() < 3 {
339 return self.clone();
340 }
341 let n = self.anchors.len();
342 let seg_count = if self.closed { n } else { n.saturating_sub(1) };
343 if seg_count < 2 {
344 return self.clone();
345 }
346
347 let mut out = Vec::with_capacity(n * 2);
348 for i in 0..n {
349 let a = self.anchors[i];
350 if a.mode != TangentMode::Corner {
351 out.push(a);
352 continue;
353 }
354 let has_prev = self.closed || i > 0;
356 let has_next = self.closed || i + 1 < n;
357 if !has_prev || !has_next {
358 out.push(a);
359 continue;
360 }
361 let prev = self.anchors[(i + n - 1) % n];
362 let next = self.anchors[(i + 1) % n];
363
364 let to_prev = prev.pos - a.pos;
365 let to_next = next.pos - a.pos;
366 let (len_prev, len_next) = (to_prev.length(), to_next.length());
367 if len_prev < 1e-9 || len_next < 1e-9 {
368 out.push(a);
369 continue;
370 }
371 let r = radius.min(len_prev * 0.45).min(len_next * 0.45);
374 let dir_prev = to_prev / len_prev;
375 let dir_next = to_next / len_next;
376
377 let p_in = a.pos + dir_prev * r; let p_out = a.pos + dir_next * r; const K: f64 = 0.5523;
383 out.push(Anchor {
384 pos: p_in,
385 tan_in: DVec2::ZERO, tan_out: -dir_prev * (r * K),
387 mode: TangentMode::Smooth,
388 });
389 out.push(Anchor {
390 pos: p_out,
391 tan_in: -dir_next * (r * K),
392 tan_out: DVec2::ZERO,
393 mode: TangentMode::Smooth,
394 });
395 }
396
397 VectorPath {
398 anchors: out,
399 closed: self.closed,
400 }
401 }
402
403 pub fn reverse(&mut self) {
405 self.anchors.reverse();
406 for a in &mut self.anchors {
407 std::mem::swap(&mut a.tan_in, &mut a.tan_out);
408 }
409 }
410
411 pub fn apply_edit(&mut self, edit: &AnchorEdit) -> Option<AnchorEdit> {
413 use AnchorEdit::*;
414 match edit {
415 SetPos { index, pos } => {
416 let a = self.anchors.get_mut(*index)?;
417 let inv = SetPos {
418 index: *index,
419 pos: a.pos,
420 };
421 a.pos = *pos;
422 Some(inv)
423 }
424 SetTanIn { index, tan } => {
425 let a = self.anchors.get_mut(*index)?;
426 let inv = SetTanIn {
427 index: *index,
428 tan: a.tan_in,
429 };
430 a.tan_in = *tan;
431 if a.mode == TangentMode::Symmetric {
432 a.tan_out = -*tan;
433 }
434 Some(inv)
435 }
436 SetTanOut { index, tan } => {
437 let a = self.anchors.get_mut(*index)?;
438 let inv = SetTanOut {
439 index: *index,
440 tan: a.tan_out,
441 };
442 a.tan_out = *tan;
443 if a.mode == TangentMode::Symmetric {
444 a.tan_in = -*tan;
445 }
446 Some(inv)
447 }
448 SetMode {
449 index,
450 mode,
451 tan_in,
452 tan_out,
453 } => {
454 let a = self.anchors.get_mut(*index)?;
455 let inv = SetMode {
456 index: *index,
457 mode: a.mode,
458 tan_in: Some(a.tan_in),
459 tan_out: Some(a.tan_out),
460 };
461 if let (Some(ti), Some(to)) = (*tan_in, *tan_out) {
462 a.mode = *mode;
463 a.tan_in = ti;
464 a.tan_out = to;
465 return Some(inv);
466 }
467 a.mode = *mode;
468 if *mode != TangentMode::Corner
469 && a.tan_in.length_squared() < 1e-12
470 && a.tan_out.length_squared() < 1e-12
471 {
472 a.tan_out = DVec2::new(10.0, 0.0);
473 a.tan_in = -a.tan_out;
474 }
475 Some(inv)
476 }
477 Delete { index } => {
478 if *index >= self.anchors.len() {
479 return None;
480 }
481 let a = self.anchors.remove(*index);
482 Some(Insert {
483 index: *index,
484 anchor: a,
485 })
486 }
487 Insert { index, anchor } => {
488 if *index > self.anchors.len() {
489 return None;
490 }
491 self.anchors.insert(*index, *anchor);
492 Some(Delete { index: *index })
493 }
494 SetClosed { closed } => {
495 let inv = SetClosed {
496 closed: self.closed,
497 };
498 self.closed = *closed;
499 Some(inv)
500 }
501 }
502 }
503}
504
505fn detect_mode(tin: DVec2, tout: DVec2) -> TangentMode {
506 let (li, lo) = (tin.length(), tout.length());
507 if li < 1e-9 || lo < 1e-9 {
508 return TangentMode::Corner;
509 }
510 let cross = tin.x * tout.y - tin.y * tout.x;
511 let colinear_opposed = cross.abs() <= 1e-6 * li * lo && tin.dot(tout) < 0.0;
512 if !colinear_opposed {
513 TangentMode::Corner
514 } else if (li - lo).abs() < 1e-6 {
515 TangentMode::Symmetric
516 } else {
517 TangentMode::Smooth
518 }
519}
520
521#[derive(Debug, thiserror::Error)]
523pub enum PathOpError {
524 #[error("path operation requires closed contours")]
525 OpenPath,
526 #[error("path operation produced no geometry")]
527 Empty,
528 #[error("boolean operation failed: {0}")]
529 Boolean(#[from] linesweeper::Error),
530}
531
532fn map_boolean_op(op: BooleanOp) -> linesweeper::BinaryOp {
533 match op {
534 BooleanOp::Union => linesweeper::BinaryOp::Union,
535 BooleanOp::Intersection => linesweeper::BinaryOp::Intersection,
536 BooleanOp::Difference => linesweeper::BinaryOp::Difference,
537 BooleanOp::Xor => linesweeper::BinaryOp::Xor,
538 }
539}
540
541pub fn contours_to_bez(contours: &[VectorPath]) -> BezPath {
544 let mut out = BezPath::new();
545 for contour in contours {
546 out.extend(contour.to_bez_path().elements().iter().copied());
547 }
548 out
549}
550
551pub fn boolean_bez(
557 a: &BezPath,
558 b: &BezPath,
559 op: BooleanOp,
560) -> Result<Vec<VectorPath>, PathOpError> {
561 let contours =
562 linesweeper::binary_op(a, b, linesweeper::FillRule::NonZero, map_boolean_op(op))?;
563
564 Ok(contours
565 .contours()
566 .filter_map(|contour| {
567 let path = VectorPath::from_bez_path(&contour.path);
568 (path.closed && path.anchors.len() >= 3).then_some(path)
569 })
570 .collect())
571}
572
573pub fn boolean_op(
576 a: &VectorPath,
577 b: &VectorPath,
578 op: BooleanOp,
579) -> Result<Vec<VectorPath>, PathOpError> {
580 if !a.closed || !b.closed {
581 return Err(PathOpError::OpenPath);
582 }
583 boolean_bez(&a.to_bez_path(), &b.to_bez_path(), op)
584}
585
586pub fn split_bez_subpaths(path: &BezPath) -> Vec<VectorPath> {
590 let mut output = Vec::new();
591 let mut current = BezPath::new();
592
593 for element in path.elements().iter().copied() {
594 if matches!(element, PathEl::MoveTo(_)) && !current.is_empty() {
595 let sub = VectorPath::from_bez_path(¤t);
596 if sub.anchors.len() >= 2 {
597 output.push(sub);
598 }
599 current = BezPath::new();
600 }
601 current.push(element);
602 }
603
604 if !current.is_empty() {
605 let sub = VectorPath::from_bez_path(¤t);
606 if sub.anchors.len() >= 2 {
607 output.push(sub);
608 }
609 }
610
611 output
612}
613
614pub fn stroke_to_paths(
619 path: &VectorPath,
620 width: f64,
621 cap: kurbo::Cap,
622 join: kurbo::Join,
623 miter_limit: f64,
624 dash: Option<(&[f64], f64)>,
625 tolerance: f64,
626) -> Result<Vec<VectorPath>, PathOpError> {
627 if !width.is_finite() || width <= 0.0 {
628 return Err(PathOpError::Empty);
629 }
630
631 let original = path.to_bez_path();
632
633 let source = match dash {
634 Some((pattern, offset)) => dash_bez_path(&original, pattern, offset).unwrap_or(original),
635 None => original,
636 };
637
638 let stroke = kurbo::Stroke::new(width)
639 .with_start_cap(cap)
640 .with_end_cap(cap)
641 .with_join(join)
642 .with_miter_limit(miter_limit);
643
644 let outline = kurbo::stroke(
645 source.elements().iter().copied(),
646 &stroke,
647 &kurbo::StrokeOpts::default(),
648 tolerance.max(1e-4),
649 );
650
651 let result = split_bez_subpaths(&outline);
652
653 if result.is_empty() {
654 Err(PathOpError::Empty)
655 } else {
656 Ok(result)
657 }
658}
659
660pub fn simplify_path(path: &VectorPath, tolerance: f64) -> VectorPath {
663 let simplified = kurbo::simplify::simplify_bezpath(
664 path.to_bez_path(),
665 tolerance.max(1e-4),
666 &kurbo::simplify::SimplifyOptions::default(),
667 );
668
669 VectorPath::from_bez_path(&simplified)
670}
671
672pub fn offset_bez_path(path: &BezPath, amount: f64, tolerance: f64) -> Option<BezPath> {
681 if !amount.is_finite() {
682 return None;
683 }
684
685 if amount.abs() <= 1e-9 {
686 return Some(path.clone());
687 }
688
689 let contours = flatten_to_contours(path, tolerance.max(0.01));
690 if contours.is_empty() {
691 return None;
692 }
693
694 let mut out = BezPath::new();
695
696 for contour in contours {
697 let Some(offset) = offset_contour(&contour.points, contour.closed, amount) else {
698 continue;
699 };
700
701 if offset.len() < 2 {
702 continue;
703 }
704
705 out.move_to(pt(offset[0]));
706
707 for p in offset.iter().skip(1) {
708 out.line_to(pt(*p));
709 }
710
711 if contour.closed {
712 out.close_path();
713 }
714 }
715
716 if out.elements().is_empty() {
717 None
718 } else {
719 Some(out)
720 }
721}
722
723#[derive(Clone, Debug)]
724struct FlatContour {
725 points: Vec<DVec2>,
726 closed: bool,
727}
728
729fn flatten_to_contours(path: &BezPath, tolerance: f64) -> Vec<FlatContour> {
730 use kurbo::{ParamCurve, ParamCurveArclen};
731
732 let mut contours = Vec::new();
733 let mut current: Vec<DVec2> = Vec::new();
734 let mut cursor = DVec2::ZERO;
735 let mut start = DVec2::ZERO;
736
737 let flush = |contours: &mut Vec<FlatContour>, current: &mut Vec<DVec2>, closed: bool| {
738 dedupe_points(current);
739
740 if current.len() >= 2 {
741 contours.push(FlatContour {
742 points: std::mem::take(current),
743 closed,
744 });
745 } else {
746 current.clear();
747 }
748 };
749
750 for element in path.elements() {
751 match *element {
752 PathEl::MoveTo(p) => {
753 flush(&mut contours, &mut current, false);
754 cursor = DVec2::new(p.x, p.y);
755 start = cursor;
756 current.push(cursor);
757 }
758
759 PathEl::LineTo(p) => {
760 cursor = DVec2::new(p.x, p.y);
761 current.push(cursor);
762 }
763
764 PathEl::QuadTo(c, p) => {
765 let seg = kurbo::QuadBez::new(pt(cursor), c, p);
766
767 let len = seg.arclen(tolerance);
768 let steps = (len / tolerance).ceil().max(2.0) as usize;
769
770 for i in 1..=steps {
771 let t = i as f64 / steps as f64;
772 let q = seg.eval(t);
773 current.push(DVec2::new(q.x, q.y));
774 }
775
776 cursor = DVec2::new(p.x, p.y);
777 }
778
779 PathEl::CurveTo(c1, c2, p) => {
780 let seg = CubicBez::new(pt(cursor), c1, c2, p);
781
782 let len = seg.arclen(tolerance);
783 let steps = (len / tolerance).ceil().max(3.0) as usize;
784
785 for i in 1..=steps {
786 let t = i as f64 / steps as f64;
787 let q = seg.eval(t);
788 current.push(DVec2::new(q.x, q.y));
789 }
790
791 cursor = DVec2::new(p.x, p.y);
792 }
793
794 PathEl::ClosePath => {
795 if (cursor - start).length_squared() > 1e-12 {
796 current.push(start);
797 }
798
799 if current.len() >= 2
801 && (current[0] - *current.last().unwrap()).length_squared() <= 1e-12
802 {
803 current.pop();
804 }
805
806 flush(&mut contours, &mut current, true);
807 cursor = start;
808 }
809 }
810 }
811
812 flush(&mut contours, &mut current, false);
813
814 contours
815}
816
817fn dedupe_points(points: &mut Vec<DVec2>) {
818 let mut out = Vec::with_capacity(points.len());
819
820 for p in points.drain(..) {
821 if out
822 .last()
823 .map(|last: &DVec2| (*last - p).length_squared() > 1e-12)
824 .unwrap_or(true)
825 {
826 out.push(p);
827 }
828 }
829
830 *points = out;
831}
832
833fn offset_contour(points: &[DVec2], closed: bool, amount: f64) -> Option<Vec<DVec2>> {
834 if points.len() < 2 {
835 return None;
836 }
837
838 if closed && points.len() < 3 {
839 return None;
840 }
841
842 if closed {
843 offset_closed_contour(points, amount)
844 } else {
845 offset_open_contour(points, amount)
846 }
847}
848
849fn offset_open_contour(points: &[DVec2], amount: f64) -> Option<Vec<DVec2>> {
850 let n = points.len();
851
852 let mut out = Vec::with_capacity(n);
853
854 for i in 0..n {
855 if i == 0 {
856 let dir = unit(points[1] - points[0])?;
857 out.push(points[0] + left_normal(dir) * amount);
858 } else if i == n - 1 {
859 let dir = unit(points[n - 1] - points[n - 2])?;
860 out.push(points[n - 1] + left_normal(dir) * amount);
861 } else {
862 let prev = unit(points[i] - points[i - 1])?;
863 let next = unit(points[i + 1] - points[i])?;
864 let n0 = left_normal(prev);
865 let n1 = left_normal(next);
866 out.push(join_point(points[i], prev, next, n0, n1, amount));
867 }
868 }
869
870 Some(out)
871}
872
873fn offset_closed_contour(points: &[DVec2], amount: f64) -> Option<Vec<DVec2>> {
874 let n = points.len();
875 let area = signed_area(points);
876
877 let outward_right = area >= 0.0;
881
882 let mut out = Vec::with_capacity(n);
883
884 for i in 0..n {
885 let prev_i = (i + n - 1) % n;
886 let next_i = (i + 1) % n;
887
888 let prev_dir = unit(points[i] - points[prev_i])?;
889 let next_dir = unit(points[next_i] - points[i])?;
890
891 let n0 = if outward_right {
892 right_normal(prev_dir)
893 } else {
894 left_normal(prev_dir)
895 };
896
897 let n1 = if outward_right {
898 right_normal(next_dir)
899 } else {
900 left_normal(next_dir)
901 };
902
903 out.push(join_point(points[i], prev_dir, next_dir, n0, n1, amount));
904 }
905
906 Some(out)
907}
908
909fn signed_area(points: &[DVec2]) -> f64 {
910 let mut area = 0.0;
911
912 for i in 0..points.len() {
913 let a = points[i];
914 let b = points[(i + 1) % points.len()];
915 area += a.x * b.y - b.x * a.y;
916 }
917
918 area * 0.5
919}
920
921fn unit(v: DVec2) -> Option<DVec2> {
922 let len = v.length();
923
924 if len <= 1e-12 || !len.is_finite() {
925 None
926 } else {
927 Some(v / len)
928 }
929}
930
931fn left_normal(v: DVec2) -> DVec2 {
932 DVec2::new(-v.y, v.x)
933}
934
935fn right_normal(v: DVec2) -> DVec2 {
936 DVec2::new(v.y, -v.x)
937}
938
939fn join_point(
940 p: DVec2,
941 prev_dir: DVec2,
942 next_dir: DVec2,
943 prev_normal: DVec2,
944 next_normal: DVec2,
945 amount: f64,
946) -> DVec2 {
947 let a0 = p + prev_normal * amount;
948 let a1 = p + next_normal * amount;
949
950 match line_intersection(a0, prev_dir, a1, next_dir) {
951 Some(miter) => {
952 let miter_len = (miter - p).length();
953 let limit = amount.abs() * 8.0 + 1e-6;
954
955 if miter_len.is_finite() && miter_len <= limit {
956 miter
957 } else {
958 (a0 + a1) * 0.5
960 }
961 }
962
963 None => (a0 + a1) * 0.5,
964 }
965}
966
967fn line_intersection(p: DVec2, r: DVec2, q: DVec2, s: DVec2) -> Option<DVec2> {
968 let cross = r.x * s.y - r.y * s.x;
969
970 if cross.abs() <= 1e-12 {
971 return None;
972 }
973
974 let qp = q - p;
975 let t = (qp.x * s.y - qp.y * s.x) / cross;
976
977 Some(p + r * t)
978}