1use core::f64::consts::{FRAC_PI_2, TAU};
4use std::borrow::Cow;
5
6use serde::{Deserialize, Serialize};
7
8use crate::font_metrics as fm;
9use crate::{
10 Aabb, Affine, Arc, Circle, CubicBez, Curve, FlattenTolerance, GeoResult, GeometryError, LinearKind, ModelTolerance,
11 Orientation, Point, QuadBez, Segment, Vector, curve::SIMILARITY_REL, intersect, orientation,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
16pub struct PointShape {
17 pub at: Point,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct Polyline {
24 pub points: Vec<Point>,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub bulges: Vec<f64>,
29 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
31 pub closed: bool,
32}
33
34impl Polyline {
35 #[must_use]
37 pub fn open(points: Vec<Point>) -> Self {
38 Self { points, bulges: Vec::new(), closed: false }
39 }
40
41 #[must_use]
43 pub fn closed(points: Vec<Point>) -> Self {
44 Self { points, bulges: Vec::new(), closed: true }
45 }
46
47 #[must_use]
49 pub fn segment_count(&self) -> usize {
50 let n = self.points.len();
51 if n < 2 {
52 0
53 } else if self.closed {
54 n
55 } else {
56 n - 1
57 }
58 }
59
60 #[must_use]
62 pub fn bulge(&self, i: usize) -> f64 {
63 self.bulges.get(i).copied().unwrap_or(0.0)
64 }
65
66 #[must_use]
68 pub fn has_arcs(&self) -> bool {
69 self.bulges.iter().any(|b| *b != 0.0)
70 }
71
72 #[must_use]
74 pub fn segment(&self, i: usize) -> Option<Curve> {
75 let n = self.points.len();
76 if i >= self.segment_count() {
77 return None;
78 }
79 let a = *self.points.get(i)?;
80 let b = *self.points.get((i + 1) % n)?;
81 let bulge = self.bulge(i);
82 if bulge != 0.0
83 && let Ok(arc) = Arc::from_bulge(a, b, bulge)
84 {
85 return Some(Curve::Arc(arc));
86 }
87 Some(Curve::Line(Segment::new(a, b)))
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
93pub struct Rect {
94 pub origin: Point,
96 pub width: f64,
98 pub height: f64,
100}
101
102impl Rect {
103 #[must_use]
105 pub fn from_corners(a: Point, b: Point) -> Self {
106 Self { origin: Point::new(a.x.min(b.x), a.y.min(b.y)), width: (a.x - b.x).abs(), height: (a.y - b.y).abs() }
107 }
108
109 #[must_use]
111 pub fn corners(&self) -> [Point; 4] {
112 let o = self.origin;
113 [
114 o,
115 Point::new(o.x + self.width, o.y),
116 Point::new(o.x + self.width, o.y + self.height),
117 Point::new(o.x, o.y + self.height),
118 ]
119 }
120
121 #[must_use]
123 pub fn center(&self) -> Point {
124 Point::new(self.origin.x + self.width * 0.5, self.origin.y + self.height * 0.5)
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct Polygon {
131 pub outer: Vec<Point>,
133 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub holes: Vec<Vec<Point>>,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
140pub enum PathEl {
141 #[serde(rename = "M")]
143 MoveTo(Point),
144 #[serde(rename = "L")]
146 LineTo(Point),
147 #[serde(rename = "Q")]
149 QuadTo(Point, Point),
150 #[serde(rename = "C")]
152 CubicTo(Point, Point, Point),
153 #[serde(rename = "Z")]
155 Close,
156}
157
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct Path {
161 pub elements: Vec<PathEl>,
163}
164
165#[derive(Debug, Clone, PartialEq)]
167pub struct SubPath {
168 pub curves: Vec<Curve>,
170 pub closed: bool,
172}
173
174impl Path {
175 #[must_use]
177 pub fn subpaths(&self) -> Vec<SubPath> {
178 let mut out = Vec::new();
179 let mut cur: Vec<Curve> = Vec::new();
180 let mut start = Point::ORIGIN;
181 let mut pen = Point::ORIGIN;
182 let mut open = false;
183 for el in &self.elements {
184 match *el {
185 PathEl::MoveTo(p) => {
186 if open && !cur.is_empty() {
187 out.push(SubPath { curves: core::mem::take(&mut cur), closed: false });
188 }
189 cur.clear();
190 start = p;
191 pen = p;
192 open = true;
193 }
194 PathEl::LineTo(p) => {
195 cur.push(Curve::Line(Segment::new(pen, p)));
196 pen = p;
197 }
198 PathEl::QuadTo(c, p) => {
199 cur.push(Curve::Cubic(QuadBez { p0: pen, p1: c, p2: p }.to_cubic()));
200 pen = p;
201 }
202 PathEl::CubicTo(c1, c2, p) => {
203 cur.push(Curve::Cubic(CubicBez { p0: pen, p1: c1, p2: c2, p3: p }));
204 pen = p;
205 }
206 PathEl::Close => {
207 if pen != start {
208 cur.push(Curve::Line(Segment::new(pen, start)));
209 }
210 if !cur.is_empty() {
211 out.push(SubPath { curves: core::mem::take(&mut cur), closed: true });
212 }
213 pen = start;
214 open = false;
215 }
216 }
217 }
218 if open && !cur.is_empty() {
219 out.push(SubPath { curves: cur, closed: false });
220 }
221 out
222 }
223
224 fn points_mut(&mut self) -> impl Iterator<Item = &mut Point> {
225 self.elements.iter_mut().flat_map(|el| -> Vec<&mut Point> {
226 match el {
227 PathEl::MoveTo(p) | PathEl::LineTo(p) => vec![p],
228 PathEl::QuadTo(a, b) => vec![a, b],
229 PathEl::CubicTo(a, b, c) => vec![a, b, c],
230 PathEl::Close => vec![],
231 }
232 })
233 }
234
235 #[must_use]
237 pub fn on_curve_points(&self) -> Vec<Point> {
238 self.elements
239 .iter()
240 .filter_map(|el| match *el {
241 PathEl::MoveTo(p) | PathEl::LineTo(p) | PathEl::QuadTo(_, p) | PathEl::CubicTo(_, _, p) => Some(p),
242 PathEl::Close => None,
243 })
244 .collect()
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub enum HAlign {
252 #[default]
254 Left,
255 Center,
257 Right,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub enum VAlign {
265 #[default]
267 Baseline,
268 Middle,
270 Top,
272 Bottom,
274}
275
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
278pub struct Text {
279 pub position: Point,
281 pub content: String,
283 pub height: f64,
285 #[serde(default)]
287 pub rotation: f64,
288 #[serde(default)]
290 pub halign: HAlign,
291 #[serde(default)]
293 pub valign: VAlign,
294}
295
296impl Text {
297 pub const LINE_SPACING: f64 = 1.2;
299 pub const TOP_ABOVE_BASELINE: f64 = 0.8;
302
303 fn em(height: f64) -> f64 {
305 height / ((fm::CAP_HEIGHT + fm::DESCENT) / fm::UNITS_PER_EM)
306 }
307
308 pub const FONT_UNITS_PER_EM: f64 = fm::UNITS_PER_EM;
311
312 #[must_use]
315 pub fn em_size(height: f64) -> f64 {
316 Self::em(height)
317 }
318
319 #[must_use]
326 pub fn line_pens(line: &str) -> (Vec<(char, f64)>, f64) {
327 let mut pens = Vec::with_capacity(line.len());
328 let mut pen = 0.0;
329 let mut prev: Option<usize> = None;
330 for c in line.trim_end_matches('\r').chars() {
331 let Some(c) = substitute(c) else { continue };
332 let idx = fm::ADVANCES.binary_search_by_key(&u32::from(c), |e| e.0).ok();
333 if let (Some(a), Some(b)) = (prev, idx) {
334 pen += kerning(a, b);
335 }
336 pens.push((c, pen));
337 pen += f64::from(idx.and_then(|i| fm::ADVANCES.get(i)).map_or(fm::NOTDEF_ADVANCE, |e| e.1));
338 prev = idx;
339 }
340 (pens, pen)
341 }
342
343 #[must_use]
347 pub fn line_width(line: &str, height: f64) -> f64 {
348 Self::line_pens(line).1 / fm::UNITS_PER_EM * Self::em(height)
349 }
350
351 #[must_use]
355 pub fn layout_box(&self) -> Aabb {
356 let h = self.height;
357 let em = Self::em(h);
358 let lines: Vec<&str> = self.content.split('\n').collect();
359 let n = lines.len().max(1) as f64;
360 let block_h = h * (1.0 + (n - 1.0) * Self::LINE_SPACING);
361 let first_baseline = match self.valign {
362 VAlign::Baseline => 0.0,
363 VAlign::Top => -Self::TOP_ABOVE_BASELINE * h,
364 VAlign::Middle => block_h * 0.5 - Self::TOP_ABOVE_BASELINE * h,
365 VAlign::Bottom => block_h - Self::TOP_ABOVE_BASELINE * h,
366 };
367 let (mut x0, mut x1) = (0.0_f64, 0.0_f64);
368 for line in &lines {
369 let w = Self::line_width(line, h);
370 let start = match self.halign {
371 HAlign::Left => 0.0,
372 HAlign::Center => -w * 0.5,
373 HAlign::Right => -w,
374 };
375 x0 = x0.min(start);
376 x1 = x1.max(start + w);
377 }
378 let top = first_baseline + fm::ASCENT / fm::UNITS_PER_EM * em;
379 let bottom = first_baseline - (n - 1.0) * Self::LINE_SPACING * h - fm::DESCENT / fm::UNITS_PER_EM * em;
380 Aabb::from_corners(Point::new(x0, top), Point::new(x1, bottom))
381 }
382
383 #[must_use]
385 pub fn layout_corners(&self) -> [Point; 4] {
386 let t = Affine::rotate(self.rotation).then(Affine::translate(self.position.to_vector()));
387 self.layout_box().corners().map(|c| t.apply(c))
388 }
389}
390
391fn substitute(c: char) -> Option<char> {
395 match c {
396 '\t' => return Some(' '),
397 c if c.is_control() => return None,
398 _ => {}
399 }
400 if fm::ADVANCES.binary_search_by_key(&u32::from(c), |e| e.0).is_ok() {
401 return Some(c);
402 }
403 Some(match c {
404 '\u{00a0}' | '\u{2007}' | '\u{202f}' => ' ',
405 '\u{2300}' => '\u{2205}',
406 c => c,
407 })
408}
409
410fn kerning(a: usize, b: usize) -> f64 {
414 let key = (u16::try_from(a).unwrap_or(u16::MAX), u16::try_from(b).unwrap_or(u16::MAX));
415 let mut units = 0i32;
416 for k in &fm::KERN {
417 if let Ok(i) = k.pairs.binary_search_by_key(&key, |p| (p.0, p.1)) {
418 units += k.pairs.get(i).map_or(0, |p| i32::from(p.2));
419 continue;
420 }
421 let (Some(&left), Some(&right)) = (k.left.get(a), k.right.get(b)) else { continue };
422 if left == u8::MAX {
423 continue;
424 }
425 units += k.matrix.get(usize::from(left) * k.columns + usize::from(right)).map_or(0, |v| i32::from(*v));
426 }
427 f64::from(units)
428}
429
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub enum TransformPolicy {
434 #[default]
436 Strict,
437 Convert,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
444#[serde(rename_all = "camelCase")]
445pub enum AnchorKind {
446 Endpoint,
448 Midpoint,
450 Center,
452 Vertex,
454 Quadrant,
456 Corner,
458 Insert,
460 Centroid,
462 Node,
464}
465
466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
468pub struct Anchor {
469 pub name: Cow<'static, str>,
473 pub kind: AnchorKind,
475 pub point: Point,
477}
478
479impl Anchor {
480 fn new(name: impl Into<Cow<'static, str>>, kind: AnchorKind, point: Point) -> Self {
481 Self { name: name.into(), kind, point }
482 }
483}
484
485fn indexed(prefix: char, i: usize) -> Cow<'static, str> {
487 const N: usize = 32;
488 macro_rules! table {
489 ($p:literal) => {
490 [
491 concat!($p, "0"),
492 concat!($p, "1"),
493 concat!($p, "2"),
494 concat!($p, "3"),
495 concat!($p, "4"),
496 concat!($p, "5"),
497 concat!($p, "6"),
498 concat!($p, "7"),
499 concat!($p, "8"),
500 concat!($p, "9"),
501 concat!($p, "10"),
502 concat!($p, "11"),
503 concat!($p, "12"),
504 concat!($p, "13"),
505 concat!($p, "14"),
506 concat!($p, "15"),
507 concat!($p, "16"),
508 concat!($p, "17"),
509 concat!($p, "18"),
510 concat!($p, "19"),
511 concat!($p, "20"),
512 concat!($p, "21"),
513 concat!($p, "22"),
514 concat!($p, "23"),
515 concat!($p, "24"),
516 concat!($p, "25"),
517 concat!($p, "26"),
518 concat!($p, "27"),
519 concat!($p, "28"),
520 concat!($p, "29"),
521 concat!($p, "30"),
522 concat!($p, "31"),
523 ]
524 };
525 }
526 static V: [&str; N] = table!("v");
527 static M: [&str; N] = table!("m");
528 static C: [&str; N] = table!("c");
529 static E: [&str; N] = table!("e");
530 static Q: [&str; N] = table!("q");
531 let table: Option<&[&'static str; N]> = match prefix {
532 'v' => Some(&V),
533 'm' => Some(&M),
534 'c' => Some(&C),
535 'e' => Some(&E),
536 'q' => Some(&Q),
537 _ => None,
538 };
539 match table.and_then(|t| t.get(i)) {
540 Some(name) => Cow::Borrowed(name),
541 None => Cow::Owned(format!("{prefix}{i}")),
542 }
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
547#[serde(rename_all = "camelCase")]
548pub enum ShapeKind {
549 Point,
551 Line,
553 Polyline,
555 Rect,
557 Circle,
559 Arc,
561 Path,
563 Polygon,
565 Text,
567}
568
569impl ShapeKind {
570 #[must_use]
572 pub const fn name(self) -> &'static str {
573 match self {
574 Self::Point => "point",
575 Self::Line => "line",
576 Self::Polyline => "polyline",
577 Self::Rect => "rect",
578 Self::Circle => "circle",
579 Self::Arc => "arc",
580 Self::Path => "path",
581 Self::Polygon => "polygon",
582 Self::Text => "text",
583 }
584 }
585}
586
587#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
589#[serde(tag = "type", rename_all = "camelCase")]
590pub enum Shape {
591 Point(PointShape),
593 Line(Segment),
595 Polyline(Polyline),
597 Rect(Rect),
599 Circle(Circle),
601 Arc(Arc),
603 Path(Path),
605 Polygon(Polygon),
607 Text(Text),
609}
610
611#[derive(Debug, Clone, PartialEq)]
613pub struct FlatPath {
614 pub points: Vec<Point>,
616 pub closed: bool,
618}
619
620pub const MAX_SHAPE_POINTS: usize = 1_000_000;
622pub const MAX_TEXT_CHARS: usize = 100_000;
624
625impl Shape {
626 #[must_use]
628 pub const fn kind(&self) -> ShapeKind {
629 match self {
630 Self::Point(_) => ShapeKind::Point,
631 Self::Line(_) => ShapeKind::Line,
632 Self::Polyline(_) => ShapeKind::Polyline,
633 Self::Rect(_) => ShapeKind::Rect,
634 Self::Circle(_) => ShapeKind::Circle,
635 Self::Arc(_) => ShapeKind::Arc,
636 Self::Path(_) => ShapeKind::Path,
637 Self::Polygon(_) => ShapeKind::Polygon,
638 Self::Text(_) => ShapeKind::Text,
639 }
640 }
641
642 pub fn validate(&self) -> GeoResult<()> {
644 let all_finite = |pts: &[Point], what: &'static str| -> GeoResult<()> {
645 if pts.len() > MAX_SHAPE_POINTS {
646 return Err(GeometryError::InvalidArgument("too many points in shape"));
647 }
648 if pts.iter().all(|p| p.is_finite()) { Ok(()) } else { Err(GeometryError::NonFinite(what)) }
649 };
650 match self {
651 Self::Point(p) => all_finite(&[p.at], "point"),
652 Self::Line(s) => all_finite(&[s.a, s.b], "line"),
653 Self::Polyline(p) => {
654 all_finite(&p.points, "polyline")?;
655 if p.points.len() < 2 {
656 return Err(GeometryError::Degenerate("polyline needs at least 2 points"));
657 }
658 if !p.bulges.is_empty() && p.bulges.len() != p.segment_count() {
659 return Err(GeometryError::InvalidArgument("bulge count must equal segment count"));
660 }
661 if p.bulges.iter().any(|b| !b.is_finite()) {
662 return Err(GeometryError::NonFinite("polyline bulge"));
663 }
664 Ok(())
665 }
666 Self::Rect(r) => {
667 all_finite(&[r.origin], "rect")?;
668 if !(r.width.is_finite() && r.height.is_finite()) {
669 return Err(GeometryError::NonFinite("rect size"));
670 }
671 if r.width < 0.0 || r.height < 0.0 {
672 return Err(GeometryError::InvalidArgument("rect size must be non-negative"));
673 }
674 Ok(())
675 }
676 Self::Circle(c) => Circle::new(c.center, c.radius).map(|_| ()),
677 Self::Arc(a) => {
678 Arc::new(a.center, a.radius, a.start, a.sweep)?;
679 if a.sweep.abs() > TAU {
680 return Err(GeometryError::InvalidArgument("arc sweep exceeds 2π"));
681 }
682 Ok(())
683 }
684 Self::Path(p) => {
685 if p.elements.len() > MAX_SHAPE_POINTS {
686 return Err(GeometryError::InvalidArgument("too many path elements"));
687 }
688 if !matches!(p.elements.first(), Some(PathEl::MoveTo(_))) {
689 return Err(GeometryError::InvalidArgument("path must start with MoveTo"));
690 }
691 let mut q = p.clone();
692 if q.points_mut().all(|p| p.is_finite()) { Ok(()) } else { Err(GeometryError::NonFinite("path")) }
693 }
694 Self::Polygon(p) => {
695 all_finite(&p.outer, "polygon")?;
696 if p.outer.len() < 3 {
697 return Err(GeometryError::Degenerate("polygon needs at least 3 points"));
698 }
699 for h in &p.holes {
700 all_finite(h, "polygon hole")?;
701 if h.len() < 3 {
702 return Err(GeometryError::Degenerate("polygon hole needs at least 3 points"));
703 }
704 }
705 Ok(())
706 }
707 Self::Text(t) => {
708 all_finite(&[t.position], "text")?;
709 if !(t.height.is_finite() && t.rotation.is_finite()) {
710 return Err(GeometryError::NonFinite("text metrics"));
711 }
712 if t.height <= 0.0 {
713 return Err(GeometryError::InvalidArgument("text height must be > 0"));
714 }
715 if t.content.chars().count() > MAX_TEXT_CHARS {
716 return Err(GeometryError::InvalidArgument("text too long"));
717 }
718 Ok(())
719 }
720 }
721 }
722
723 #[must_use]
725 pub fn curves(&self) -> Vec<Curve> {
726 match self {
727 Self::Point(_) | Self::Text(_) => Vec::new(),
728 Self::Line(s) => vec![Curve::Line(*s)],
729 Self::Polyline(p) => (0..p.segment_count()).filter_map(|i| p.segment(i)).collect(),
730 Self::Rect(r) => ring_lines(&r.corners()),
731 Self::Circle(c) => vec![Curve::Circle(*c)],
732 Self::Arc(a) => vec![Curve::Arc(*a)],
733 Self::Path(p) => p.subpaths().into_iter().flat_map(|s| s.curves).collect(),
734 Self::Polygon(p) => {
735 let mut v = ring_lines(&p.outer);
736 for h in &p.holes {
737 v.extend(ring_lines(h));
738 }
739 v
740 }
741 }
742 }
743
744 #[must_use]
746 pub fn bbox(&self) -> Aabb {
747 match self {
748 Self::Point(p) => Aabb::from_corners(p.at, p.at),
749 Self::Text(t) => Aabb::from_points(t.layout_corners()),
750 Self::Rect(r) => Aabb::from_points(r.corners()),
751 Self::Polygon(p) => Aabb::from_points(p.outer.iter().copied()),
752 _ => self.curves().iter().fold(Aabb::EMPTY, |b, c| b.union(c.bbox())),
753 }
754 }
755
756 #[must_use]
758 pub fn is_region(&self) -> bool {
759 match self {
760 Self::Rect(_) | Self::Circle(_) | Self::Polygon(_) => true,
761 Self::Polyline(p) => p.closed && p.points.len() >= 3,
762 Self::Path(p) => {
763 let subs = p.subpaths();
764 !subs.is_empty() && subs.iter().all(|s| s.closed)
765 }
766 _ => false,
767 }
768 }
769
770 #[must_use]
772 pub fn distance_to(&self, p: Point) -> f64 {
773 match self {
774 Self::Point(s) => s.at.distance(p),
775 Self::Text(t) => {
776 if point_in_ring(p, &t.layout_corners()) {
777 0.0
778 } else {
779 ring_lines(&t.layout_corners()).iter().map(|c| c.distance_to_point(p)).fold(f64::INFINITY, f64::min)
780 }
781 }
782 _ => self.curves().iter().map(|c| c.distance_to_point(p)).fold(f64::INFINITY, f64::min),
783 }
784 }
785
786 #[must_use]
790 pub fn contains_point(&self, p: Point, tol: FlattenTolerance) -> bool {
791 if !self.is_region() || !self.bbox().contains_point(p) {
792 return false;
793 }
794 match self {
795 Self::Rect(r) => point_in_ring(p, &r.corners()),
796 Self::Circle(c) => p.distance(c.center) <= c.radius,
797 Self::Polygon(poly) => {
798 let mut inside = point_in_ring(p, &poly.outer);
799 for h in &poly.holes {
800 if point_in_ring(p, h) {
801 inside = !inside;
802 }
803 }
804 inside
805 }
806 _ => {
807 let mut inside = false;
808 for ring in self.flatten(tol) {
809 if ring.closed && point_in_ring(p, &ring.points) {
810 inside = !inside;
811 }
812 }
813 inside
814 }
815 }
816 }
817
818 #[must_use]
820 pub fn hit(&self, p: Point, radius: f64, fill: bool) -> bool {
821 if self.bbox().distance_to_point(p) > radius {
822 return false;
823 }
824 if fill && self.contains_point(p, FlattenTolerance(radius.max(1e-9) * 0.25)) {
825 return true;
826 }
827 self.distance_to(p) <= radius
828 }
829
830 #[must_use]
832 pub fn inside_rect(&self, r: Aabb) -> bool {
833 r.contains(self.bbox())
834 }
835
836 #[must_use]
839 pub fn intersects_rect(&self, r: Aabb, tol: ModelTolerance) -> bool {
840 let bb = self.bbox();
841 if !bb.intersects(r) {
842 return false;
843 }
844 if r.contains(bb) {
845 return true;
846 }
847 match self {
848 Self::Point(p) => r.contains_point(p.at),
849 Self::Text(t) => {
850 let corners = t.layout_corners();
851 corners.iter().any(|c| r.contains_point(*c))
852 || rect_edges(r).iter().any(|e| {
853 ring_lines(&corners).iter().any(|c| !intersect::intersect(e, c, tol).points.is_empty())
854 })
855 || point_in_ring(r.center(), &corners)
856 }
857 _ => {
858 let curves = self.curves();
859 if curves.iter().any(|c| r.contains_point(c.start()) || r.contains_point(c.end())) {
860 return true;
861 }
862 let edges = rect_edges(r);
863 if curves.iter().any(|c| edges.iter().any(|e| !intersect::intersect(e, c, tol).points.is_empty())) {
864 return true;
865 }
866 self.is_region() && self.contains_point(r.center(), FlattenTolerance::default())
869 }
870 }
871 }
872
873 #[must_use]
875 pub fn flatten(&self, tol: FlattenTolerance) -> Vec<FlatPath> {
876 let tol = tol.value();
877 let curves_to_flat = |curves: &[Curve], closed: bool| -> FlatPath {
878 let mut points = Vec::new();
879 if let Some(first) = curves.first() {
880 points.push(first.start());
881 }
882 for c in curves {
883 c.flatten_into(tol, &mut points);
884 }
885 if closed && points.len() > 1 && points.first() == points.last() {
886 points.pop();
887 }
888 FlatPath { points, closed }
889 };
890 match self {
891 Self::Point(p) => vec![FlatPath { points: vec![p.at], closed: false }],
892 Self::Text(t) => vec![FlatPath { points: t.layout_corners().to_vec(), closed: true }],
893 Self::Line(s) => vec![FlatPath { points: vec![s.a, s.b], closed: false }],
894 Self::Rect(r) => vec![FlatPath { points: r.corners().to_vec(), closed: true }],
895 Self::Polygon(p) => core::iter::once(&p.outer)
896 .chain(p.holes.iter())
897 .map(|ring| FlatPath { points: ring.clone(), closed: true })
898 .collect(),
899 Self::Circle(c) => {
900 let mut f = curves_to_flat(&[Curve::Circle(*c)], true);
901 if f.points.len() > 1
902 && f.points.first().zip(f.points.last()).is_some_and(|(a, b)| a.distance(*b) < tol)
903 {
904 f.points.pop();
905 }
906 vec![f]
907 }
908 Self::Arc(a) => vec![curves_to_flat(&[Curve::Arc(*a)], false)],
909 Self::Polyline(p) => vec![curves_to_flat(&self.curves(), p.closed)],
910 Self::Path(p) => p.subpaths().iter().map(|s| curves_to_flat(&s.curves, s.closed)).collect(),
911 }
912 }
913
914 #[must_use]
916 pub fn anchors(&self) -> Vec<Anchor> {
917 use AnchorKind as K;
918 match self {
919 Self::Point(p) => vec![Anchor::new("point", K::Node, p.at)],
920 Self::Line(s) => vec![
921 Anchor::new("start", K::Endpoint, s.a),
922 Anchor::new("end", K::Endpoint, s.b),
923 Anchor::new("mid", K::Midpoint, s.midpoint()),
924 ],
925 Self::Polyline(p) => {
926 let mut v: Vec<Anchor> =
927 p.points.iter().enumerate().map(|(i, q)| Anchor::new(indexed('v', i), K::Vertex, *q)).collect();
928 for i in 0..p.segment_count() {
929 if let Some(c) = p.segment(i) {
930 v.push(Anchor::new(indexed('m', i), K::Midpoint, c.point_at(0.5)));
931 }
932 }
933 if !p.closed {
934 if let Some(f) = p.points.first() {
935 v.push(Anchor::new("start", K::Endpoint, *f));
936 }
937 if let Some(l) = p.points.last() {
938 v.push(Anchor::new("end", K::Endpoint, *l));
939 }
940 }
941 v
942 }
943 Self::Rect(r) => {
944 let c = r.corners();
945 let mut v: Vec<Anchor> =
946 c.iter().enumerate().map(|(i, q)| Anchor::new(indexed('c', i), K::Corner, *q)).collect();
947 for i in 0..4 {
948 v.push(Anchor::new(indexed('e', i), K::Midpoint, c[i].midpoint(c[(i + 1) % 4])));
949 }
950 v.push(Anchor::new("center", K::Center, r.center()));
951 v
952 }
953 Self::Circle(c) => {
954 let mut v = vec![Anchor::new("center", K::Center, c.center)];
955 for i in 0..4u8 {
956 v.push(Anchor::new(
957 indexed('q', usize::from(i)),
958 K::Quadrant,
959 c.point_at_angle(f64::from(i) * FRAC_PI_2),
960 ));
961 }
962 v
963 }
964 Self::Arc(a) => vec![
965 Anchor::new("start", K::Endpoint, a.start_point()),
966 Anchor::new("end", K::Endpoint, a.end_point()),
967 Anchor::new("mid", K::Midpoint, a.mid_point()),
968 Anchor::new("center", K::Center, a.center),
969 ],
970 Self::Path(p) => {
971 let pts = p.on_curve_points();
972 let mut v: Vec<Anchor> =
973 pts.iter().enumerate().map(|(i, q)| Anchor::new(indexed('v', i), K::Vertex, *q)).collect();
974 if let Some(f) = pts.first() {
975 v.push(Anchor::new("start", K::Endpoint, *f));
976 }
977 if let Some(l) = pts.last() {
978 v.push(Anchor::new("end", K::Endpoint, *l));
979 }
980 v
981 }
982 Self::Polygon(p) => {
983 let mut v: Vec<Anchor> =
984 p.outer.iter().enumerate().map(|(i, q)| Anchor::new(indexed('v', i), K::Vertex, *q)).collect();
985 if let Some(c) = ring_centroid(&p.outer) {
986 v.push(Anchor::new("centroid", K::Centroid, c));
987 }
988 v
989 }
990 Self::Text(t) => vec![Anchor::new("insert", K::Insert, t.position)],
991 }
992 }
993
994 #[must_use]
996 pub fn anchor(&self, name: &str) -> Option<Point> {
997 self.anchors().into_iter().find(|a| a.name == name).map(|a| a.point)
998 }
999
1000 #[must_use]
1002 pub fn length(&self) -> f64 {
1003 self.curves().iter().map(Curve::length).sum()
1004 }
1005
1006 #[must_use]
1008 pub fn area(&self) -> f64 {
1009 match self {
1010 Self::Rect(r) => r.width * r.height,
1011 Self::Circle(c) => core::f64::consts::PI * c.radius * c.radius,
1012 Self::Polygon(p) => {
1013 let mut a = ring_signed_area(&p.outer).abs();
1014 for h in &p.holes {
1015 a -= ring_signed_area(h).abs();
1016 }
1017 a.max(0.0)
1018 }
1019 Self::Polyline(p) if p.closed => polyline_signed_area(p).abs(),
1020 Self::Path(_) if self.is_region() => self
1021 .flatten(FlattenTolerance(self.bbox().size().length() * 1e-6))
1022 .iter()
1023 .map(|r| ring_signed_area(&r.points))
1024 .sum::<f64>()
1025 .abs(),
1026 _ => 0.0,
1027 }
1028 }
1029
1030 pub fn transform(&self, t: Affine, policy: TransformPolicy) -> GeoResult<Self> {
1032 if !t.is_finite() {
1033 return Err(GeometryError::NonFinite("transform"));
1034 }
1035 let kind = t.linear_kind(SIMILARITY_REL);
1036 if kind == LinearKind::Singular {
1037 return Err(GeometryError::SingularTransform { determinant: t.determinant() });
1038 }
1039 let unsupported =
1040 |shape: &'static str, reason: &'static str| GeometryError::UnsupportedTransform { shape, reason };
1041 Ok(match self {
1042 Self::Point(p) => Self::Point(PointShape { at: t.apply(p.at) }),
1043 Self::Line(s) => Self::Line(s.transform(t)),
1044 Self::Polyline(p) => {
1045 if p.has_arcs() && !matches!(kind, LinearKind::Similarity { .. }) {
1046 if policy == TransformPolicy::Strict {
1047 return Err(unsupported("polyline", "arc segments need a similarity transform"));
1048 }
1049 return Self::Path(curves_to_path(&self.curves(), p.closed)).transform(t, policy);
1050 }
1051 let reflected = matches!(kind, LinearKind::Similarity { reflected: true, .. });
1052 Self::Polyline(Polyline {
1053 points: p.points.iter().map(|q| t.apply(*q)).collect(),
1054 bulges: if reflected { p.bulges.iter().map(|b| -b).collect() } else { p.bulges.clone() },
1055 closed: p.closed,
1056 })
1057 }
1058 Self::Rect(r) => {
1059 if t.is_axis_aligned(1e-12) {
1060 let c = r.corners();
1061 Self::Rect(Rect::from_corners(t.apply(c[0]), t.apply(c[2])))
1062 } else if policy == TransformPolicy::Convert {
1063 Self::Polygon(Polygon {
1064 outer: r.corners().iter().map(|q| t.apply(*q)).collect(),
1065 holes: Vec::new(),
1066 })
1067 } else {
1068 return Err(unsupported("rect", "rotation/shear must be applied through the entity transform"));
1069 }
1070 }
1071 Self::Circle(c) => match c.transform(t) {
1072 Ok(c) => Self::Circle(c),
1073 Err(e) if policy == TransformPolicy::Strict => return Err(e),
1074 Err(_) => {
1075 let arc = Arc::new(c.center, c.radius, 0.0, TAU)?;
1076 Self::Path(curves_to_path(&arc_to_cubics(arc), true)).transform(t, policy)?
1077 }
1078 },
1079 Self::Arc(a) => match a.transform(t) {
1080 Ok(a) => Self::Arc(a),
1081 Err(e) if policy == TransformPolicy::Strict => return Err(e),
1082 Err(_) => Self::Path(curves_to_path(&arc_to_cubics(*a), false)).transform(t, policy)?,
1083 },
1084 Self::Path(p) => {
1085 let mut q = p.clone();
1086 for pt in q.points_mut() {
1087 *pt = t.apply(*pt);
1088 }
1089 Self::Path(q)
1090 }
1091 Self::Polygon(p) => Self::Polygon(Polygon {
1092 outer: p.outer.iter().map(|q| t.apply(*q)).collect(),
1093 holes: p.holes.iter().map(|h| h.iter().map(|q| t.apply(*q)).collect()).collect(),
1094 }),
1095 Self::Text(tx) => match kind {
1096 LinearKind::Similarity { scale, .. } => {
1097 let dir = t.apply_vector(Vector::from_angle(tx.rotation));
1098 Self::Text(Text {
1099 position: t.apply(tx.position),
1100 height: tx.height * scale,
1101 rotation: dir.angle(),
1102 ..tx.clone()
1103 })
1104 }
1105 _ => return Err(unsupported("text", "text only supports similarity transforms")),
1106 },
1107 })
1108 }
1109}
1110
1111fn ring_lines(pts: &[Point]) -> Vec<Curve> {
1112 let n = pts.len();
1113 (0..n)
1114 .filter_map(|i| {
1115 let a = *pts.get(i)?;
1116 let b = *pts.get((i + 1) % n)?;
1117 Some(Curve::Line(Segment::new(a, b)))
1118 })
1119 .collect()
1120}
1121
1122fn rect_edges(r: Aabb) -> Vec<Curve> {
1123 ring_lines(&r.corners())
1124}
1125
1126#[must_use]
1128pub fn point_in_ring(p: Point, ring: &[Point]) -> bool {
1129 let n = ring.len();
1130 if n < 3 {
1131 return false;
1132 }
1133 let mut inside = false;
1134 let mut j = n - 1;
1135 for i in 0..n {
1136 let (Some(&a), Some(&b)) = (ring.get(i), ring.get(j)) else {
1137 break;
1138 };
1139 if (a.y > p.y) != (b.y > p.y) {
1140 let o = orientation(b, a, p);
1142 let upward = a.y > b.y;
1143 let left = if upward { o == Orientation::CounterClockwise } else { o == Orientation::Clockwise };
1144 if left {
1145 inside = !inside;
1146 }
1147 }
1148 j = i;
1149 }
1150 inside
1151}
1152
1153#[must_use]
1155pub fn ring_signed_area(ring: &[Point]) -> f64 {
1156 let n = ring.len();
1157 if n < 3 {
1158 return 0.0;
1159 }
1160 let o = ring.first().copied().unwrap_or(Point::ORIGIN);
1161 let mut s = 0.0;
1162 for i in 0..n {
1163 if let (Some(a), Some(b)) = (ring.get(i), ring.get((i + 1) % n)) {
1164 s += (*a - o).cross(*b - o);
1165 }
1166 }
1167 s * 0.5
1168}
1169
1170fn ring_centroid(ring: &[Point]) -> Option<Point> {
1171 let n = ring.len();
1172 let o = *ring.first()?;
1173 let mut a2 = 0.0;
1174 let (mut cx, mut cy) = (0.0, 0.0);
1175 for i in 0..n {
1176 let p = *ring.get(i)? - o;
1177 let q = *ring.get((i + 1) % n)? - o;
1178 let c = p.cross(q);
1179 a2 += c;
1180 cx += (p.x + q.x) * c;
1181 cy += (p.y + q.y) * c;
1182 }
1183 if a2.abs() < f64::MIN_POSITIVE {
1184 return None;
1185 }
1186 Some(Point::new(o.x + cx / (3.0 * a2), o.y + cy / (3.0 * a2)))
1187}
1188
1189fn polyline_signed_area(p: &Polyline) -> f64 {
1190 let mut a = ring_signed_area(&p.points);
1191 for i in 0..p.segment_count() {
1192 if let Some(Curve::Arc(arc)) = p.segment(i) {
1193 let th = arc.sweep;
1195 a += 0.5 * arc.radius * arc.radius * (th - crate::math::sin(th));
1196 }
1197 }
1198 a
1199}
1200
1201#[must_use]
1203pub fn arc_to_cubics(a: Arc) -> Vec<Curve> {
1204 let n = (a.sweep.abs() / FRAC_PI_2).ceil().max(1.0) as u32;
1205 let step = a.sweep / f64::from(n);
1206 let k = 4.0 / 3.0 * crate::math::tan(step / 4.0);
1207 (0..n)
1208 .map(|i| {
1209 let a0 = a.start + step * f64::from(i);
1210 let a1 = a0 + step;
1211 let p0 = a.center + Vector::from_angle(a0) * a.radius;
1212 let p3 = a.center + Vector::from_angle(a1) * a.radius;
1213 let p1 = p0 + Vector::from_angle(a0).perp() * (a.radius * k);
1214 let p2 = p3 - Vector::from_angle(a1).perp() * (a.radius * k);
1215 Curve::Cubic(CubicBez { p0, p1, p2, p3 })
1216 })
1217 .collect()
1218}
1219
1220fn curves_to_path(curves: &[Curve], closed: bool) -> Path {
1221 let mut elements = Vec::new();
1222 if let Some(first) = curves.first() {
1223 elements.push(PathEl::MoveTo(first.start()));
1224 }
1225 for c in curves {
1226 match *c {
1227 Curve::Line(s) => elements.push(PathEl::LineTo(s.b)),
1228 Curve::Cubic(b) => elements.push(PathEl::CubicTo(b.p1, b.p2, b.p3)),
1229 Curve::Arc(a) => {
1230 for cb in arc_to_cubics(a) {
1231 if let Curve::Cubic(b) = cb {
1232 elements.push(PathEl::CubicTo(b.p1, b.p2, b.p3));
1233 }
1234 }
1235 }
1236 Curve::Circle(ci) => {
1237 if let Ok(a) = Arc::new(ci.center, ci.radius, 0.0, TAU) {
1238 for cb in arc_to_cubics(a) {
1239 if let Curve::Cubic(b) = cb {
1240 elements.push(PathEl::CubicTo(b.p1, b.p2, b.p3));
1241 }
1242 }
1243 }
1244 }
1245 }
1246 }
1247 if closed {
1248 elements.push(PathEl::Close);
1249 }
1250 Path { elements }
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255 use super::*;
1256 use core::f64::consts::PI;
1257
1258 #[test]
1259 fn serde_shape_tagging() {
1260 let s = Shape::Line(Segment::new(Point::new(0.0, 0.0), Point::new(1.0, 2.0)));
1261 let j = serde_json::to_string(&s).unwrap();
1262 assert_eq!(j, r#"{"type":"line","a":[0.0,0.0],"b":[1.0,2.0]}"#);
1263 let back: Shape = serde_json::from_str(&j).unwrap();
1264 assert_eq!(back, s);
1265 let p = Shape::Path(Path {
1266 elements: vec![
1267 PathEl::MoveTo(Point::ORIGIN),
1268 PathEl::CubicTo(Point::new(1.0, 1.0), Point::new(2.0, 1.0), Point::new(3.0, 0.0)),
1269 PathEl::Close,
1270 ],
1271 });
1272 let j = serde_json::to_string(&p).unwrap();
1273 let back: Shape = serde_json::from_str(&j).unwrap();
1274 assert_eq!(back, p);
1275 }
1276
1277 #[test]
1278 fn rect_rotation_is_strict_by_default() {
1279 let r = Shape::Rect(Rect { origin: Point::ORIGIN, width: 2.0, height: 1.0 });
1280 assert!(r.transform(Affine::rotate(0.3), TransformPolicy::Strict).is_err());
1281 let conv = r.transform(Affine::rotate(PI / 2.0), TransformPolicy::Convert).unwrap();
1282 assert_eq!(conv.kind(), ShapeKind::Polygon);
1283 assert!((conv.area() - 2.0).abs() < 1e-12);
1284 let mirrored = r.transform(Affine::scale(-1.0, 1.0), TransformPolicy::Strict).unwrap();
1285 assert_eq!(mirrored.bbox().min, Point::new(-2.0, 0.0));
1286 }
1287
1288 #[test]
1289 fn circle_nonuniform_scale_policy() {
1290 let c = Shape::Circle(Circle::new(Point::ORIGIN, 1.0).unwrap());
1291 let err = c.transform(Affine::scale(2.0, 1.0), TransformPolicy::Strict);
1292 assert!(matches!(err, Err(GeometryError::UnsupportedTransform { .. })));
1293 let e = c.transform(Affine::scale(2.0, 1.0), TransformPolicy::Convert).unwrap();
1294 assert_eq!(e.kind(), ShapeKind::Path);
1295 assert!((e.area() - 2.0 * PI).abs() / (2.0 * PI) < 1e-3, "{}", e.area());
1297 assert!(e.anchor("center").is_none());
1299 }
1300
1301 #[test]
1302 fn contains_with_holes() {
1303 let poly = Shape::Polygon(Polygon {
1304 outer: vec![Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0), Point::new(0.0, 10.0)],
1305 holes: vec![vec![Point::new(4.0, 4.0), Point::new(6.0, 4.0), Point::new(6.0, 6.0), Point::new(4.0, 6.0)]],
1306 });
1307 let tol = FlattenTolerance::default();
1308 assert!(poly.contains_point(Point::new(1.0, 1.0), tol));
1309 assert!(!poly.contains_point(Point::new(5.0, 5.0), tol));
1310 assert!(!poly.contains_point(Point::new(11.0, 5.0), tol));
1311 assert!((poly.area() - 96.0).abs() < 1e-12);
1312 }
1313
1314 #[test]
1315 fn polyline_with_bulge_area_and_length() {
1316 let p = Shape::Polyline(Polyline {
1318 points: vec![Point::new(-1.0, 0.0), Point::new(1.0, 0.0)],
1319 bulges: vec![0.0, 1.0],
1320 closed: true,
1321 });
1322 assert!((p.area() - PI / 2.0).abs() < 1e-12, "{}", p.area());
1323 assert!((p.length() - (2.0 + PI)).abs() < 1e-12);
1324 }
1325
1326 #[test]
1327 fn crossing_vs_window_selection() {
1328 let l = Shape::Line(Segment::new(Point::new(0.0, 0.0), Point::new(10.0, 0.0)));
1329 let r = Aabb::from_corners(Point::new(4.0, -1.0), Point::new(6.0, 1.0));
1330 assert!(l.intersects_rect(r, ModelTolerance::DEFAULT));
1331 assert!(!l.inside_rect(r));
1332 let big = Aabb::from_corners(Point::new(-1.0, -1.0), Point::new(11.0, 1.0));
1333 assert!(l.inside_rect(big));
1334 let circle = Shape::Circle(Circle::new(Point::ORIGIN, 10.0).unwrap());
1335 let inner = Aabb::from_corners(Point::new(-1.0, -1.0), Point::new(1.0, 1.0));
1336 assert!(circle.intersects_rect(inner, ModelTolerance::DEFAULT));
1337 }
1338
1339 #[test]
1340 fn validate_rejects_bad_input() {
1341 assert!(Shape::Line(Segment::new(Point::new(f64::NAN, 0.0), Point::ORIGIN)).validate().is_err());
1342 assert!(
1343 Shape::Polyline(Polyline {
1344 points: vec![Point::ORIGIN, Point::new(1.0, 0.0)],
1345 bulges: vec![0.1, 0.2],
1346 closed: false
1347 })
1348 .validate()
1349 .is_err()
1350 );
1351 assert!(Shape::Path(Path { elements: vec![PathEl::LineTo(Point::ORIGIN)] }).validate().is_err());
1352 assert!(Shape::Circle(Circle { center: Point::ORIGIN, radius: 0.0 }).validate().is_err());
1353 }
1354
1355 #[test]
1356 fn line_layout_matches_harfbuzz_kerning() {
1357 let cases = [
1361 ("AV", 2686.0),
1362 ("To", 2390.0),
1363 ("Yo", 2461.0),
1364 ("Wa", 3064.0),
1365 ("LT", 2283.0),
1366 ("Ölçü planı — İğdır", 17152.0),
1367 ("TAVERN", 7926.0),
1368 ("P.", 1829.0),
1369 ("f)", 1505.0),
1370 ("Tığ", 3074.0),
1371 ("kv", 2275.0),
1372 ("Hello", 4936.0),
1373 ("AV\tA", 4675.0),
1374 ];
1375 for (s, expected) in cases {
1376 let (pens, width) = Text::line_pens(s);
1377 assert_eq!(width, expected, "{s}");
1378 assert_eq!(pens.len(), s.chars().count(), "{s}");
1379 }
1380 let (pens, _) = Text::line_pens("AV");
1382 let a_advance = pens[1].1;
1383 let (_, a_alone) = Text::line_pens("A");
1384 assert!(a_advance < a_alone, "{a_advance} vs {a_alone}");
1385 let h = 10.0;
1387 assert!((Text::line_width("AV", h) - 2686.0 / 2048.0 * Text::em_size(h)).abs() < 1e-12);
1388 assert_eq!(Text::line_pens("A\u{7}V").1, 2686.0);
1389 }
1390
1391 #[test]
1392 fn text_similarity_only() {
1393 let t = Shape::Text(Text {
1394 position: Point::new(1.0, 1.0),
1395 content: "Ölçü ğüşıİç".into(),
1396 height: 2.5,
1397 rotation: 0.0,
1398 halign: HAlign::Center,
1399 valign: VAlign::Middle,
1400 });
1401 let r = t.transform(Affine::rotate(PI / 2.0).then(Affine::scale(2.0, 2.0)), TransformPolicy::Strict).unwrap();
1402 if let Shape::Text(tx) = r {
1403 assert!((tx.height - 5.0).abs() < 1e-12);
1404 assert!((tx.rotation - PI / 2.0).abs() < 1e-12);
1405 } else {
1406 unreachable!();
1407 }
1408 assert!(t.transform(Affine::scale(2.0, 1.0), TransformPolicy::Convert).is_err());
1409 let b = t.bbox();
1412 let w = Text::line_width("Ölçü ğüşıİç", 2.5);
1413 assert!((b.width() - w).abs() < 1e-9);
1414 assert!(w > 11.0 * 2.5 * 0.4 && w < 11.0 * 2.5 * 0.8, "{w}");
1415 }
1416}