1use crate::layers::{LineCap, LineJoin};
29use rustial_math::GeoCoord;
30
31#[derive(Debug, Clone, Copy)]
40struct Vec2 {
41 x: f64,
42 y: f64,
43}
44
45#[inline]
51fn normalize(v: Vec2) -> Vec2 {
52 let len = (v.x * v.x + v.y * v.y).sqrt();
53 if len < 1e-15 {
54 Vec2 { x: 0.0, y: 0.0 }
55 } else {
56 Vec2 {
57 x: v.x / len,
58 y: v.y / len,
59 }
60 }
61}
62
63fn effective_ring_len(coords: &[GeoCoord]) -> usize {
74 let n = coords.len();
75 if n > 3
76 && (coords[0].lat - coords[n - 1].lat).abs() < 1e-12
77 && (coords[0].lon - coords[n - 1].lon).abs() < 1e-12
78 {
79 n - 1
80 } else {
81 n
82 }
83}
84
85fn _signed_ring_area(coords: &[GeoCoord], len: usize) -> f64 {
88 let mut area = 0.0;
89 let mut j = len - 1;
90 for i in 0..len {
91 area += (coords[j].lon - coords[i].lon) * (coords[j].lat + coords[i].lat);
92 j = i;
93 }
94 area * 0.5
95}
96
97#[allow(clippy::too_many_arguments)]
102fn point_in_triangle(
103 px: f64,
104 py: f64,
105 ax: f64,
106 ay: f64,
107 bx: f64,
108 by: f64,
109 cx: f64,
110 cy: f64,
111) -> bool {
112 let d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by);
113 let d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy);
114 let d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay);
115 let has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
116 let has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
117 !(has_neg && has_pos)
118}
119
120fn cross_z(ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64) -> f64 {
122 (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
123}
124
125fn earcut_flat(flat: &[f64], hole_starts: &[usize]) -> Vec<u32> {
134 let total_verts = flat.len() / 2;
135 if total_verts < 3 {
136 return Vec::new();
137 }
138
139 let mut prev = vec![0usize; total_verts];
141 let mut next = vec![0usize; total_verts];
142
143 let outer_end = if hole_starts.is_empty() {
145 total_verts
146 } else {
147 hole_starts[0]
148 };
149
150 if outer_end < 3 {
151 return Vec::new();
152 }
153
154 let outer_ccw = ring_area_flat(flat, 0, outer_end) > 0.0;
156 link_ring(&mut prev, &mut next, 0, outer_end, outer_ccw);
157
158 let mut outer_start = 0usize;
161 for (hi, &h_start) in hole_starts.iter().enumerate() {
162 let h_end = if hi + 1 < hole_starts.len() {
163 hole_starts[hi + 1]
164 } else {
165 total_verts
166 };
167 if h_end - h_start < 3 {
168 continue;
169 }
170 let hole_ccw = ring_area_flat(flat, h_start, h_end) > 0.0;
171 link_ring(&mut prev, &mut next, h_start, h_end, !hole_ccw);
173
174 let bridge_hole = rightmost_vertex(flat, h_start, h_end);
177 outer_start = bridge_hole_to_outer(
178 flat,
179 &mut prev,
180 &mut next,
181 outer_start,
182 outer_end,
183 bridge_hole,
184 );
185 }
186
187 let mut indices = Vec::with_capacity((total_verts - 2) * 3);
189 ear_clip(
190 flat,
191 &prev,
192 &mut next.clone(),
193 outer_start,
194 &mut indices,
195 total_verts,
196 );
197 indices
198}
199
200fn ring_area_flat(flat: &[f64], start: usize, end: usize) -> f64 {
202 let mut area = 0.0;
203 let mut j = end - 1;
204 for i in start..end {
205 let jx = flat[j * 2];
206 let jy = flat[j * 2 + 1];
207 let ix = flat[i * 2];
208 let iy = flat[i * 2 + 1];
209 area += (jx - ix) * (jy + iy);
210 j = i;
211 }
212 area * 0.5
213}
214
215fn link_ring(prev: &mut [usize], next: &mut [usize], start: usize, end: usize, forward: bool) {
218 if forward {
219 for i in start..end {
220 let p = if i == start { end - 1 } else { i - 1 };
221 let n = if i == end - 1 { start } else { i + 1 };
222 prev[i] = p;
223 next[i] = n;
224 }
225 } else {
226 for i in start..end {
227 let p = if i == end - 1 { start } else { i + 1 };
228 let n = if i == start { end - 1 } else { i - 1 };
229 prev[i] = p;
230 next[i] = n;
231 }
232 }
233}
234
235fn rightmost_vertex(flat: &[f64], start: usize, end: usize) -> usize {
237 let mut best = start;
238 for i in (start + 1)..end {
239 if flat[i * 2] > flat[best * 2]
240 || (flat[i * 2] == flat[best * 2] && flat[i * 2 + 1] < flat[best * 2 + 1])
241 {
242 best = i;
243 }
244 }
245 best
246}
247
248fn bridge_hole_to_outer(
250 flat: &[f64],
251 prev: &mut [usize],
252 next: &mut [usize],
253 outer_start: usize,
254 _outer_end: usize,
255 hole_vertex: usize,
256) -> usize {
257 let hx = flat[hole_vertex * 2];
259 let hy = flat[hole_vertex * 2 + 1];
260 let mut best = outer_start;
261 let mut best_dist = f64::INFINITY;
262 let mut i = outer_start;
263 loop {
264 let ix = flat[i * 2];
265 let iy = flat[i * 2 + 1];
266 let d = (ix - hx) * (ix - hx) + (iy - hy) * (iy - hy);
267 if d < best_dist {
268 best_dist = d;
269 best = i;
270 }
271 i = next[i];
272 if i == outer_start {
273 break;
274 }
275 }
276
277 let best_next = next[best];
283 let hole_prev = prev[hole_vertex];
284
285 next[best] = hole_vertex;
286 prev[hole_vertex] = best;
287 next[hole_prev] = best_next;
288 prev[best_next] = hole_prev;
289
290 best
291}
292
293fn ear_clip(
295 flat: &[f64],
296 orig_prev: &[usize],
297 next: &mut [usize],
298 start: usize,
299 indices: &mut Vec<u32>,
300 _total_verts: usize,
301) {
302 let mut prev = orig_prev.to_vec();
304 let mut remaining = {
305 let mut count = 1usize;
307 let mut i = next[start];
308 while i != start {
309 count += 1;
310 i = next[i];
311 }
312 count
313 };
314
315 let mut ear = start;
316 let mut _stop = ear;
317 let mut pass = 0;
318
319 while remaining > 2 {
320 let a = prev[ear];
321 let b = ear;
322 let c = next[ear];
323
324 if is_ear(flat, &prev, next, a, b, c) {
325 indices.push(a as u32);
326 indices.push(b as u32);
327 indices.push(c as u32);
328
329 next[a] = c;
331 prev[c] = a;
332
333 remaining -= 1;
334 _stop = c;
335 ear = c;
336 pass = 0;
337 } else {
338 ear = next[ear];
339 pass += 1;
340 if pass >= remaining {
341 fan_remaining(next, start, ear, remaining, indices);
344 break;
345 }
346 }
347 }
348}
349
350fn is_ear(flat: &[f64], prev: &[usize], next: &[usize], a: usize, b: usize, c: usize) -> bool {
352 let ax = flat[a * 2];
353 let ay = flat[a * 2 + 1];
354 let bx = flat[b * 2];
355 let by = flat[b * 2 + 1];
356 let cx = flat[c * 2];
357 let cy = flat[c * 2 + 1];
358
359 if cross_z(ax, ay, bx, by, cx, cy) <= 0.0 {
361 return false;
362 }
363
364 let mut p = next[c];
368 while p != a {
369 let px = flat[p * 2];
370 let py = flat[p * 2 + 1];
371 if point_in_triangle(px, py, ax, ay, bx, by, cx, cy)
372 && cross_z(
373 flat[prev[p] * 2],
374 flat[prev[p] * 2 + 1],
375 px,
376 py,
377 flat[next[p] * 2],
378 flat[next[p] * 2 + 1],
379 ) <= 0.0
380 {
381 return false;
382 }
383 p = next[p];
384 }
385 true
386}
387
388fn fan_remaining(
390 next: &[usize],
391 _start: usize,
392 first: usize,
393 remaining: usize,
394 indices: &mut Vec<u32>,
395) {
396 if remaining < 3 {
397 return;
398 }
399 let anchor = first;
400 let mut b = next[anchor];
401 for _ in 0..remaining - 2 {
402 let c = next[b];
403 indices.push(anchor as u32);
404 indices.push(b as u32);
405 indices.push(c as u32);
406 b = c;
407 }
408}
409
410pub fn triangulate_polygon(coords: &[GeoCoord]) -> Vec<u32> {
451 let n = coords.len();
452 if n < 3 {
453 return Vec::new();
454 }
455
456 let effective_len = effective_ring_len(coords);
457 if effective_len < 3 {
458 return Vec::new();
459 }
460
461 let mut flat = Vec::with_capacity(effective_len * 2);
463 for c in &coords[..effective_len] {
464 flat.push(c.lon);
465 flat.push(c.lat);
466 }
467
468 earcut_flat(&flat, &[])
469}
470
471pub fn triangulate_polygon_with_holes(exterior: &[GeoCoord], holes: &[&[GeoCoord]]) -> Vec<u32> {
503 let ext_len = effective_ring_len(exterior);
504 if ext_len < 3 {
505 return Vec::new();
506 }
507
508 let hole_lens: Vec<usize> = holes.iter().map(|h| effective_ring_len(h)).collect();
509 let total_verts: usize = ext_len + hole_lens.iter().sum::<usize>();
510 let mut flat = Vec::with_capacity(total_verts * 2);
511
512 for c in &exterior[..ext_len] {
514 flat.push(c.lon);
515 flat.push(c.lat);
516 }
517
518 let mut hole_starts = Vec::with_capacity(holes.len());
520 let mut offset = ext_len;
521 for (i, hole) in holes.iter().enumerate() {
522 let hl = hole_lens[i];
523 if hl < 3 {
524 continue;
525 }
526 hole_starts.push(offset);
527 for c in &hole[..hl] {
528 flat.push(c.lon);
529 flat.push(c.lat);
530 }
531 offset += hl;
532 }
533
534 earcut_flat(&flat, &hole_starts)
535}
536
537pub fn stroke_line(coords: &[GeoCoord], half_width: f64) -> (Vec<[f64; 2]>, Vec<u32>) {
590 if coords.len() < 2 {
591 return (Vec::new(), Vec::new());
592 }
593
594 let vertex_count = coords.len() * 2;
595 let segment_count = coords.len() - 1;
596 let mut positions = Vec::with_capacity(vertex_count);
597 let mut indices = Vec::with_capacity(segment_count * 6);
598
599 for i in 0..coords.len() {
600 let curr = Vec2 {
601 x: coords[i].lon,
602 y: coords[i].lat,
603 };
604
605 let tangent = if i == 0 {
607 let next = Vec2 {
609 x: coords[1].lon,
610 y: coords[1].lat,
611 };
612 normalize(Vec2 {
613 x: next.x - curr.x,
614 y: next.y - curr.y,
615 })
616 } else if i == coords.len() - 1 {
617 let prev = Vec2 {
619 x: coords[i - 1].lon,
620 y: coords[i - 1].lat,
621 };
622 normalize(Vec2 {
623 x: curr.x - prev.x,
624 y: curr.y - prev.y,
625 })
626 } else {
627 let prev = Vec2 {
629 x: coords[i - 1].lon,
630 y: coords[i - 1].lat,
631 };
632 let next = Vec2 {
633 x: coords[i + 1].lon,
634 y: coords[i + 1].lat,
635 };
636 normalize(Vec2 {
637 x: next.x - prev.x,
638 y: next.y - prev.y,
639 })
640 };
641
642 let normal = Vec2 {
644 x: -tangent.y,
645 y: tangent.x,
646 };
647
648 positions.push([
650 curr.x + normal.x * half_width,
651 curr.y + normal.y * half_width,
652 ]);
653 positions.push([
654 curr.x - normal.x * half_width,
655 curr.y - normal.y * half_width,
656 ]);
657 }
658
659 for i in 0..segment_count as u32 {
667 let base = i * 2;
668 indices.push(base);
669 indices.push(base + 1);
670 indices.push(base + 2);
671 indices.push(base + 1);
672 indices.push(base + 3);
673 indices.push(base + 2);
674 }
675
676 (positions, indices)
677}
678
679#[derive(Debug, Clone)]
686pub struct StrokeLineResult {
687 pub positions: Vec<[f64; 2]>,
689 pub indices: Vec<u32>,
691 pub normals: Vec<[f64; 2]>,
694 pub distances: Vec<f64>,
697 pub cap_join: Vec<f32>,
704}
705
706const ROUND_SEGMENTS: u32 = 8;
708
709const CIRCUMSCRIBE: f64 = {
714 1.01959
718};
719
720pub fn stroke_line_styled(
736 coords: &[GeoCoord],
737 half_width: f64,
738 cap: LineCap,
739 join: LineJoin,
740 miter_limit: f32,
741) -> StrokeLineResult {
742 let empty = StrokeLineResult {
743 positions: Vec::new(),
744 indices: Vec::new(),
745 normals: Vec::new(),
746 distances: Vec::new(),
747 cap_join: Vec::new(),
748 };
749 if coords.len() < 2 {
750 return empty;
751 }
752
753 let seg_count = coords.len() - 1;
755 let mut seg_tangents = Vec::with_capacity(seg_count);
756 let mut seg_normals = Vec::with_capacity(seg_count);
757 let mut seg_lengths = Vec::with_capacity(seg_count);
758
759 for i in 0..seg_count {
760 let dx = coords[i + 1].lon - coords[i].lon;
761 let dy = coords[i + 1].lat - coords[i].lat;
762 let len = (dx * dx + dy * dy).sqrt();
763 let t = if len < 1e-15 {
764 Vec2 { x: 1.0, y: 0.0 }
765 } else {
766 Vec2 {
767 x: dx / len,
768 y: dy / len,
769 }
770 };
771 let n = Vec2 { x: -t.y, y: t.x };
772 seg_tangents.push(t);
773 seg_normals.push(n);
774 seg_lengths.push(len);
775 }
776
777 let mut cum_dist = Vec::with_capacity(coords.len());
779 cum_dist.push(0.0);
780 for i in 0..seg_count {
781 cum_dist.push(cum_dist[i] + seg_lengths[i]);
782 }
783
784 let cap_extra = match cap {
787 LineCap::Round => ROUND_SEGMENTS as usize * 2,
788 _ => 2,
789 };
790 let join_extra = match join {
791 LineJoin::Round => ROUND_SEGMENTS as usize,
792 _ => 2,
793 };
794 let est_verts = coords.len() * 2 + cap_extra * 2 + join_extra * seg_count;
795 let est_indices = seg_count * 6 + cap_extra * 6 + join_extra * 3 * seg_count;
796 let mut positions = Vec::with_capacity(est_verts);
797 let mut normals = Vec::with_capacity(est_verts);
798 let mut distances = Vec::with_capacity(est_verts);
799 let mut cap_join_flags = Vec::with_capacity(est_verts);
800 let mut indices = Vec::with_capacity(est_indices);
801
802 #[inline]
804 #[allow(clippy::too_many_arguments)]
805 fn push_vert(
806 positions: &mut Vec<[f64; 2]>,
807 normals: &mut Vec<[f64; 2]>,
808 distances: &mut Vec<f64>,
809 cap_join_flags: &mut Vec<f32>,
810 pos: [f64; 2],
811 nrm: [f64; 2],
812 dist: f64,
813 cj: f32,
814 ) -> u32 {
815 let idx = positions.len() as u32;
816 positions.push(pos);
817 normals.push(nrm);
818 distances.push(dist);
819 cap_join_flags.push(cj);
820 idx
821 }
822
823 let first_n = seg_normals[0];
825 let first_t = seg_tangents[0];
826 let cx = coords[0].lon;
827 let cy = coords[0].lat;
828
829 match cap {
830 LineCap::Butt => {
831 let l = push_vert(
833 &mut positions,
834 &mut normals,
835 &mut distances,
836 &mut cap_join_flags,
837 [cx + first_n.x * half_width, cy + first_n.y * half_width],
838 [first_n.x, first_n.y],
839 0.0,
840 0.0,
841 );
842 let r = push_vert(
843 &mut positions,
844 &mut normals,
845 &mut distances,
846 &mut cap_join_flags,
847 [cx - first_n.x * half_width, cy - first_n.y * half_width],
848 [-first_n.x, -first_n.y],
849 0.0,
850 0.0,
851 );
852 let _ = (l, r); }
854 LineCap::Square => {
855 let bx = cx - first_t.x * half_width;
857 let by = cy - first_t.y * half_width;
858 push_vert(
859 &mut positions,
860 &mut normals,
861 &mut distances,
862 &mut cap_join_flags,
863 [bx + first_n.x * half_width, by + first_n.y * half_width],
864 [first_n.x, first_n.y],
865 0.0,
866 0.0,
867 );
868 push_vert(
869 &mut positions,
870 &mut normals,
871 &mut distances,
872 &mut cap_join_flags,
873 [bx - first_n.x * half_width, by - first_n.y * half_width],
874 [-first_n.x, -first_n.y],
875 0.0,
876 0.0,
877 );
878 }
879 LineCap::Round => {
880 let center_idx = push_vert(
883 &mut positions,
884 &mut normals,
885 &mut distances,
886 &mut cap_join_flags,
887 [cx, cy],
888 [0.0, 0.0],
889 0.0,
890 1.0,
891 );
892 let start_angle = first_n.y.atan2(first_n.x);
894 let hw_circ = half_width * CIRCUMSCRIBE;
900 let mut fan_verts = Vec::with_capacity(ROUND_SEGMENTS as usize + 1);
901 for k in 0..=ROUND_SEGMENTS {
902 let a = start_angle + std::f64::consts::PI * k as f64 / ROUND_SEGMENTS as f64;
903 let nx = a.cos();
904 let ny = a.sin();
905 let v = push_vert(
906 &mut positions,
907 &mut normals,
908 &mut distances,
909 &mut cap_join_flags,
910 [cx + nx * hw_circ, cy + ny * hw_circ],
911 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE],
912 0.0,
913 1.0,
914 );
915 fan_verts.push(v);
916 }
917 for k in 0..ROUND_SEGMENTS {
918 indices.push(center_idx);
919 indices.push(fan_verts[k as usize + 1]);
920 indices.push(fan_verts[k as usize]);
921 }
922 push_vert(
927 &mut positions,
928 &mut normals,
929 &mut distances,
930 &mut cap_join_flags,
931 [cx + first_n.x * half_width, cy + first_n.y * half_width],
932 [first_n.x, first_n.y],
933 0.0,
934 0.0,
935 );
936 push_vert(
937 &mut positions,
938 &mut normals,
939 &mut distances,
940 &mut cap_join_flags,
941 [cx - first_n.x * half_width, cy - first_n.y * half_width],
942 [-first_n.x, -first_n.y],
943 0.0,
944 0.0,
945 );
946 }
947 }
948
949 for i in 0..seg_count {
955 let n = seg_normals[i];
956 let dist = cum_dist[i + 1];
957 let vx = coords[i + 1].lon;
958 let vy = coords[i + 1].lat;
959
960 let prev_left = positions.len() as u32 - 2;
962 let prev_right = positions.len() as u32 - 1;
963
964 if i < seg_count - 1 {
965 let n_next = seg_normals[i + 1];
967
968 let bx = n.x + n_next.x;
970 let by = n.y + n_next.y;
971 let blen = (bx * bx + by * by).sqrt();
972
973 let cross = seg_tangents[i].x * seg_tangents[i + 1].y
975 - seg_tangents[i].y * seg_tangents[i + 1].x;
976
977 if blen < 1e-12 {
978 let l = push_vert(
980 &mut positions,
981 &mut normals,
982 &mut distances,
983 &mut cap_join_flags,
984 [vx + n.x * half_width, vy + n.y * half_width],
985 [n.x, n.y],
986 dist,
987 0.0,
988 );
989 let r = push_vert(
990 &mut positions,
991 &mut normals,
992 &mut distances,
993 &mut cap_join_flags,
994 [vx - n.x * half_width, vy - n.y * half_width],
995 [-n.x, -n.y],
996 dist,
997 0.0,
998 );
999 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
1001 let l2 = push_vert(
1003 &mut positions,
1004 &mut normals,
1005 &mut distances,
1006 &mut cap_join_flags,
1007 [vx + n_next.x * half_width, vy + n_next.y * half_width],
1008 [n_next.x, n_next.y],
1009 dist,
1010 0.0,
1011 );
1012 let r2 = push_vert(
1013 &mut positions,
1014 &mut normals,
1015 &mut distances,
1016 &mut cap_join_flags,
1017 [vx - n_next.x * half_width, vy - n_next.y * half_width],
1018 [-n_next.x, -n_next.y],
1019 dist,
1020 0.0,
1021 );
1022 indices.extend_from_slice(&[l, r, l2, r, r2, l2]);
1023 } else {
1024 let miter_nx = bx / blen;
1025 let miter_ny = by / blen;
1026 let dot = n.x * miter_nx + n.y * miter_ny;
1028 let miter_len = if dot.abs() > 1e-12 {
1029 1.0 / dot
1030 } else {
1031 miter_limit as f64 + 1.0
1032 };
1033
1034 let use_miter = matches!(join, LineJoin::Miter) && miter_len <= miter_limit as f64;
1035
1036 if use_miter {
1037 let hw_m = half_width * miter_len;
1039 let l = push_vert(
1040 &mut positions,
1041 &mut normals,
1042 &mut distances,
1043 &mut cap_join_flags,
1044 [vx + miter_nx * hw_m, vy + miter_ny * hw_m],
1045 [miter_nx, miter_ny],
1046 dist,
1047 0.0,
1048 );
1049 let r = push_vert(
1050 &mut positions,
1051 &mut normals,
1052 &mut distances,
1053 &mut cap_join_flags,
1054 [vx - miter_nx * hw_m, vy - miter_ny * hw_m],
1055 [-miter_nx, -miter_ny],
1056 dist,
1057 0.0,
1058 );
1059 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
1060 } else if matches!(join, LineJoin::Round) {
1061 let l_in = push_vert(
1063 &mut positions,
1064 &mut normals,
1065 &mut distances,
1066 &mut cap_join_flags,
1067 [vx + n.x * half_width, vy + n.y * half_width],
1068 [n.x, n.y],
1069 dist,
1070 0.0,
1071 );
1072 let r_in = push_vert(
1073 &mut positions,
1074 &mut normals,
1075 &mut distances,
1076 &mut cap_join_flags,
1077 [vx - n.x * half_width, vy - n.y * half_width],
1078 [-n.x, -n.y],
1079 dist,
1080 0.0,
1081 );
1082 indices
1083 .extend_from_slice(&[prev_left, prev_right, l_in, prev_right, r_in, l_in]);
1084
1085 let hw_circ = half_width * CIRCUMSCRIBE;
1088 let center_idx = push_vert(
1089 &mut positions,
1090 &mut normals,
1091 &mut distances,
1092 &mut cap_join_flags,
1093 [vx, vy],
1094 [0.0, 0.0],
1095 dist,
1096 1.0,
1097 );
1098
1099 if cross > 0.0 {
1100 let a0 = n.y.atan2(n.x);
1102 let a1 = n_next.y.atan2(n_next.x);
1103 let mut da = a1 - a0;
1104 if da < 0.0 {
1105 da += 2.0 * std::f64::consts::PI;
1106 }
1107 let steps = ROUND_SEGMENTS;
1108 let mut prev_v = l_in;
1109 for k in 1..=steps {
1110 let a = a0 + da * k as f64 / steps as f64;
1111 let nx = a.cos();
1112 let ny = a.sin();
1113 let v = push_vert(
1114 &mut positions,
1115 &mut normals,
1116 &mut distances,
1117 &mut cap_join_flags,
1118 [vx + nx * hw_circ, vy + ny * hw_circ],
1119 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE],
1120 dist,
1121 1.0,
1122 );
1123 indices.extend_from_slice(&[center_idx, prev_v, v]);
1124 prev_v = v;
1125 }
1126 push_vert(
1128 &mut positions,
1129 &mut normals,
1130 &mut distances,
1131 &mut cap_join_flags,
1132 [vx + n_next.x * half_width, vy + n_next.y * half_width],
1133 [n_next.x, n_next.y],
1134 dist,
1135 0.0,
1136 );
1137 push_vert(
1138 &mut positions,
1139 &mut normals,
1140 &mut distances,
1141 &mut cap_join_flags,
1142 [vx - n_next.x * half_width, vy - n_next.y * half_width],
1143 [-n_next.x, -n_next.y],
1144 dist,
1145 0.0,
1146 );
1147 } else {
1148 let a0 = (-n.y).atan2(-n.x);
1150 let a1 = (-n_next.y).atan2(-n_next.x);
1151 let mut da = a1 - a0;
1152 if da > 0.0 {
1153 da -= 2.0 * std::f64::consts::PI;
1154 }
1155 let steps = ROUND_SEGMENTS;
1156 let mut prev_v = r_in;
1157 for k in 1..=steps {
1158 let a = a0 + da * k as f64 / steps as f64;
1159 let nx = a.cos();
1160 let ny = a.sin();
1161 let v = push_vert(
1162 &mut positions,
1163 &mut normals,
1164 &mut distances,
1165 &mut cap_join_flags,
1166 [vx + nx * hw_circ, vy + ny * hw_circ],
1167 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE],
1168 dist,
1169 1.0,
1170 );
1171 indices.extend_from_slice(&[center_idx, v, prev_v]);
1172 prev_v = v;
1173 }
1174 push_vert(
1175 &mut positions,
1176 &mut normals,
1177 &mut distances,
1178 &mut cap_join_flags,
1179 [vx + n_next.x * half_width, vy + n_next.y * half_width],
1180 [n_next.x, n_next.y],
1181 dist,
1182 0.0,
1183 );
1184 push_vert(
1185 &mut positions,
1186 &mut normals,
1187 &mut distances,
1188 &mut cap_join_flags,
1189 [vx - n_next.x * half_width, vy - n_next.y * half_width],
1190 [-n_next.x, -n_next.y],
1191 dist,
1192 0.0,
1193 );
1194 }
1195 } else {
1196 let l_in = push_vert(
1198 &mut positions,
1199 &mut normals,
1200 &mut distances,
1201 &mut cap_join_flags,
1202 [vx + n.x * half_width, vy + n.y * half_width],
1203 [n.x, n.y],
1204 dist,
1205 0.0,
1206 );
1207 let r_in = push_vert(
1208 &mut positions,
1209 &mut normals,
1210 &mut distances,
1211 &mut cap_join_flags,
1212 [vx - n.x * half_width, vy - n.y * half_width],
1213 [-n.x, -n.y],
1214 dist,
1215 0.0,
1216 );
1217 indices
1218 .extend_from_slice(&[prev_left, prev_right, l_in, prev_right, r_in, l_in]);
1219
1220 let l2 = push_vert(
1222 &mut positions,
1223 &mut normals,
1224 &mut distances,
1225 &mut cap_join_flags,
1226 [vx + n_next.x * half_width, vy + n_next.y * half_width],
1227 [n_next.x, n_next.y],
1228 dist,
1229 0.0,
1230 );
1231 let r2 = push_vert(
1232 &mut positions,
1233 &mut normals,
1234 &mut distances,
1235 &mut cap_join_flags,
1236 [vx - n_next.x * half_width, vy - n_next.y * half_width],
1237 [-n_next.x, -n_next.y],
1238 dist,
1239 0.0,
1240 );
1241 indices.extend_from_slice(&[l_in, r_in, l2, r_in, r2, l2]);
1242 }
1243 }
1244 } else {
1245 let l = push_vert(
1247 &mut positions,
1248 &mut normals,
1249 &mut distances,
1250 &mut cap_join_flags,
1251 [vx + n.x * half_width, vy + n.y * half_width],
1252 [n.x, n.y],
1253 dist,
1254 0.0,
1255 );
1256 let r = push_vert(
1257 &mut positions,
1258 &mut normals,
1259 &mut distances,
1260 &mut cap_join_flags,
1261 [vx - n.x * half_width, vy - n.y * half_width],
1262 [-n.x, -n.y],
1263 dist,
1264 0.0,
1265 );
1266 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
1267 }
1268 }
1269
1270 let last_n = seg_normals[seg_count - 1];
1272 let last_t = seg_tangents[seg_count - 1];
1273 let ex = coords[coords.len() - 1].lon;
1274 let ey = coords[coords.len() - 1].lat;
1275 let end_dist = cum_dist[coords.len() - 1];
1276
1277 match cap {
1278 LineCap::Butt => { }
1279 LineCap::Square => {
1280 let prev_left = positions.len() as u32 - 2;
1281 let prev_right = positions.len() as u32 - 1;
1282 let fx = ex + last_t.x * half_width;
1283 let fy = ey + last_t.y * half_width;
1284 let l = push_vert(
1285 &mut positions,
1286 &mut normals,
1287 &mut distances,
1288 &mut cap_join_flags,
1289 [fx + last_n.x * half_width, fy + last_n.y * half_width],
1290 [last_n.x, last_n.y],
1291 end_dist,
1292 0.0,
1293 );
1294 let r = push_vert(
1295 &mut positions,
1296 &mut normals,
1297 &mut distances,
1298 &mut cap_join_flags,
1299 [fx - last_n.x * half_width, fy - last_n.y * half_width],
1300 [-last_n.x, -last_n.y],
1301 end_dist,
1302 0.0,
1303 );
1304 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
1305 }
1306 LineCap::Round => {
1307 let center_idx = push_vert(
1308 &mut positions,
1309 &mut normals,
1310 &mut distances,
1311 &mut cap_join_flags,
1312 [ex, ey],
1313 [0.0, 0.0],
1314 end_dist,
1315 1.0,
1316 );
1317 let hw_circ = half_width * CIRCUMSCRIBE;
1319 let start_angle = last_n.y.atan2(last_n.x);
1320 let mut fan_verts = Vec::with_capacity(ROUND_SEGMENTS as usize + 1);
1321 for k in 0..=ROUND_SEGMENTS {
1322 let a = start_angle - std::f64::consts::PI * k as f64 / ROUND_SEGMENTS as f64;
1323 let nx = a.cos();
1324 let ny = a.sin();
1325 let v = push_vert(
1326 &mut positions,
1327 &mut normals,
1328 &mut distances,
1329 &mut cap_join_flags,
1330 [ex + nx * hw_circ, ey + ny * hw_circ],
1331 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE],
1332 end_dist,
1333 1.0,
1334 );
1335 fan_verts.push(v);
1336 }
1337 let prev_left = center_idx - 2;
1339 let prev_right = center_idx - 1;
1340 indices.extend_from_slice(&[prev_left, fan_verts[0], center_idx]);
1341 indices.extend_from_slice(&[
1342 prev_right,
1343 center_idx,
1344 fan_verts[ROUND_SEGMENTS as usize],
1345 ]);
1346 for k in 0..ROUND_SEGMENTS {
1347 indices.push(center_idx);
1348 indices.push(fan_verts[k as usize]);
1349 indices.push(fan_verts[k as usize + 1]);
1350 }
1351 }
1352 }
1353
1354 StrokeLineResult {
1355 positions,
1356 indices,
1357 normals,
1358 distances,
1359 cap_join: cap_join_flags,
1360 }
1361}
1362
1363#[cfg(test)]
1368mod tests {
1369 use super::*;
1370
1371 #[test]
1376 fn triangulate_empty() {
1377 assert!(triangulate_polygon(&[]).is_empty());
1378 }
1379
1380 #[test]
1381 fn triangulate_single_point() {
1382 assert!(triangulate_polygon(&[GeoCoord::from_lat_lon(0.0, 0.0)]).is_empty());
1383 }
1384
1385 #[test]
1386 fn triangulate_two_points() {
1387 let coords = vec![
1388 GeoCoord::from_lat_lon(0.0, 0.0),
1389 GeoCoord::from_lat_lon(1.0, 1.0),
1390 ];
1391 assert!(triangulate_polygon(&coords).is_empty());
1392 }
1393
1394 #[test]
1395 fn triangulate_triangle() {
1396 let coords = vec![
1397 GeoCoord::from_lat_lon(0.0, 0.0),
1398 GeoCoord::from_lat_lon(0.0, 1.0),
1399 GeoCoord::from_lat_lon(1.0, 0.0),
1400 ];
1401 let indices = triangulate_polygon(&coords);
1402 assert_eq!(indices.len(), 3);
1403 let mut sorted = indices.clone();
1405 sorted.sort();
1406 assert_eq!(sorted, vec![0, 1, 2]);
1407 }
1408
1409 #[test]
1410 fn triangulate_square() {
1411 let coords = vec![
1412 GeoCoord::from_lat_lon(0.0, 0.0),
1413 GeoCoord::from_lat_lon(0.0, 1.0),
1414 GeoCoord::from_lat_lon(1.0, 1.0),
1415 GeoCoord::from_lat_lon(1.0, 0.0),
1416 ];
1417 let indices = triangulate_polygon(&coords);
1418 assert_eq!(indices.len(), 6); assert!(indices.iter().all(|&i| i < 4));
1420 }
1421
1422 #[test]
1423 fn triangulate_closed_polygon() {
1424 let coords = vec![
1426 GeoCoord::from_lat_lon(0.0, 0.0),
1427 GeoCoord::from_lat_lon(0.0, 1.0),
1428 GeoCoord::from_lat_lon(1.0, 1.0),
1429 GeoCoord::from_lat_lon(1.0, 0.0),
1430 GeoCoord::from_lat_lon(0.0, 0.0),
1431 ];
1432 let indices = triangulate_polygon(&coords);
1433 assert_eq!(indices.len(), 6); assert!(indices.iter().all(|&i| i < 4));
1436 }
1437
1438 #[test]
1439 fn triangulate_closing_vertex_reduces_below_three() {
1440 let coords = vec![
1442 GeoCoord::from_lat_lon(0.0, 0.0),
1443 GeoCoord::from_lat_lon(1.0, 1.0),
1444 GeoCoord::from_lat_lon(0.0, 0.0),
1445 ];
1446 let indices = triangulate_polygon(&coords);
1451 assert_eq!(indices.len(), 3);
1452 }
1453
1454 #[test]
1455 fn triangulate_pentagon() {
1456 let coords = vec![
1457 GeoCoord::from_lat_lon(0.0, 0.0),
1458 GeoCoord::from_lat_lon(0.0, 2.0),
1459 GeoCoord::from_lat_lon(1.0, 3.0),
1460 GeoCoord::from_lat_lon(2.0, 2.0),
1461 GeoCoord::from_lat_lon(2.0, 0.0),
1462 ];
1463 let indices = triangulate_polygon(&coords);
1464 assert_eq!(indices.len(), 9);
1466 assert!(indices.iter().all(|&i| i < 5));
1467 }
1468
1469 #[test]
1470 fn triangulate_indices_are_valid() {
1471 let coords: Vec<GeoCoord> = (0..20)
1473 .map(|i| {
1474 let angle = 2.0 * std::f64::consts::PI * i as f64 / 20.0;
1475 GeoCoord::from_lat_lon(angle.sin() * 10.0, angle.cos() * 10.0)
1476 })
1477 .collect();
1478 let indices = triangulate_polygon(&coords);
1479 assert_eq!(indices.len(), 18 * 3); assert!(indices.iter().all(|&i| (i as usize) < coords.len()));
1481 }
1482
1483 #[test]
1488 fn stroke_empty() {
1489 let (p, i) = stroke_line(&[], 1.0);
1490 assert!(p.is_empty());
1491 assert!(i.is_empty());
1492 }
1493
1494 #[test]
1495 fn stroke_single_point() {
1496 let (p, i) = stroke_line(&[GeoCoord::from_lat_lon(0.0, 0.0)], 1.0);
1497 assert!(p.is_empty());
1498 assert!(i.is_empty());
1499 }
1500
1501 #[test]
1502 fn stroke_line_two_points() {
1503 let coords = vec![
1504 GeoCoord::from_lat_lon(0.0, 0.0),
1505 GeoCoord::from_lat_lon(0.0, 1.0),
1506 ];
1507 let (positions, indices) = stroke_line(&coords, 0.01);
1508 assert_eq!(positions.len(), 4); assert_eq!(indices.len(), 6); }
1511
1512 #[test]
1513 fn stroke_line_three_points() {
1514 let coords = vec![
1515 GeoCoord::from_lat_lon(0.0, 0.0),
1516 GeoCoord::from_lat_lon(0.0, 1.0),
1517 GeoCoord::from_lat_lon(0.0, 2.0),
1518 ];
1519 let (positions, indices) = stroke_line(&coords, 0.01);
1520 assert_eq!(positions.len(), 6); assert_eq!(indices.len(), 12); }
1523
1524 #[test]
1525 fn stroke_ribbon_has_nonzero_width() {
1526 let coords = vec![
1528 GeoCoord::from_lat_lon(0.0, 0.0),
1529 GeoCoord::from_lat_lon(0.0, 1.0),
1530 ];
1531 let hw = 0.5;
1532 let (positions, _) = stroke_line(&coords, hw);
1533
1534 let left = positions[0];
1536 let right = positions[1];
1537
1538 let dy = (left[1] - right[1]).abs();
1542 assert!(
1543 (dy - 2.0 * hw).abs() < 1e-12,
1544 "expected ribbon width {}, got {dy}",
1545 2.0 * hw
1546 );
1547 }
1548
1549 #[test]
1550 fn stroke_indices_are_valid() {
1551 let coords: Vec<GeoCoord> = (0..10)
1552 .map(|i| GeoCoord::from_lat_lon(0.0, i as f64))
1553 .collect();
1554 let (positions, indices) = stroke_line(&coords, 0.01);
1555 let max_idx = positions.len() as u32;
1556 assert!(
1557 indices.iter().all(|&i| i < max_idx),
1558 "all indices must be within the position buffer"
1559 );
1560 }
1561
1562 #[test]
1563 fn stroke_zero_width() {
1564 let coords = vec![
1567 GeoCoord::from_lat_lon(0.0, 0.0),
1568 GeoCoord::from_lat_lon(0.0, 1.0),
1569 ];
1570 let (positions, indices) = stroke_line(&coords, 0.0);
1571 assert_eq!(positions.len(), 4);
1572 assert_eq!(indices.len(), 6);
1573 assert_eq!(positions[0], positions[1]);
1575 }
1576
1577 #[test]
1578 fn stroke_coincident_points() {
1579 let coords = vec![
1582 GeoCoord::from_lat_lon(5.0, 10.0),
1583 GeoCoord::from_lat_lon(5.0, 10.0),
1584 ];
1585 let (positions, indices) = stroke_line(&coords, 0.01);
1586 assert_eq!(positions.len(), 4);
1587 assert_eq!(indices.len(), 6);
1588 }
1589
1590 #[test]
1595 fn normalize_unit_vector() {
1596 let v = normalize(Vec2 { x: 3.0, y: 4.0 });
1597 let len = (v.x * v.x + v.y * v.y).sqrt();
1598 assert!((len - 1.0).abs() < 1e-12);
1599 }
1600
1601 #[test]
1602 fn normalize_zero_vector() {
1603 let v = normalize(Vec2 { x: 0.0, y: 0.0 });
1604 assert_eq!(v.x, 0.0);
1605 assert_eq!(v.y, 0.0);
1606 }
1607
1608 #[test]
1609 fn normalize_tiny_vector() {
1610 let v = normalize(Vec2 { x: 1e-16, y: 0.0 });
1612 assert_eq!(v.x, 0.0);
1613 assert_eq!(v.y, 0.0);
1614 }
1615
1616 #[test]
1621 fn triangulate_concave_l_shape() {
1622 let coords = vec![
1630 GeoCoord::from_lat_lon(0.0, 0.0),
1631 GeoCoord::from_lat_lon(0.0, 2.0),
1632 GeoCoord::from_lat_lon(1.0, 2.0),
1633 GeoCoord::from_lat_lon(1.0, 1.0),
1634 GeoCoord::from_lat_lon(2.0, 1.0),
1635 GeoCoord::from_lat_lon(2.0, 0.0),
1636 ];
1637 let indices = triangulate_polygon(&coords);
1638 assert_eq!(indices.len(), 12);
1640 assert!(indices.iter().all(|&i| i < 6));
1641
1642 let area: f64 = indices
1645 .chunks(3)
1646 .map(|tri| {
1647 let a = &coords[tri[0] as usize];
1648 let b = &coords[tri[1] as usize];
1649 let c = &coords[tri[2] as usize];
1650 ((b.lon - a.lon) * (c.lat - a.lat) - (c.lon - a.lon) * (b.lat - a.lat)) * 0.5
1652 })
1653 .sum::<f64>()
1654 .abs();
1655 assert!(
1656 (area - 3.0).abs() < 1e-6,
1657 "L-shape area should be 3.0, got {area}"
1658 );
1659 }
1660
1661 #[test]
1666 fn triangulate_with_hole() {
1667 let exterior = vec![
1668 GeoCoord::from_lat_lon(0.0, 0.0),
1669 GeoCoord::from_lat_lon(0.0, 4.0),
1670 GeoCoord::from_lat_lon(4.0, 4.0),
1671 GeoCoord::from_lat_lon(4.0, 0.0),
1672 ];
1673 let hole = vec![
1674 GeoCoord::from_lat_lon(1.0, 1.0),
1675 GeoCoord::from_lat_lon(1.0, 3.0),
1676 GeoCoord::from_lat_lon(3.0, 3.0),
1677 GeoCoord::from_lat_lon(3.0, 1.0),
1678 ];
1679 let indices = triangulate_polygon_with_holes(&exterior, &[&hole]);
1680 assert_eq!(indices.len() % 3, 0);
1682 assert!(!indices.is_empty());
1683 assert!(indices.iter().all(|&i| (i as usize) < 8));
1685 }
1686
1687 #[test]
1688 fn triangulate_with_hole_empty_exterior() {
1689 let exterior: Vec<GeoCoord> = Vec::new();
1690 let hole = vec![
1691 GeoCoord::from_lat_lon(1.0, 1.0),
1692 GeoCoord::from_lat_lon(1.0, 3.0),
1693 GeoCoord::from_lat_lon(3.0, 3.0),
1694 ];
1695 assert!(triangulate_polygon_with_holes(&exterior, &[&hole]).is_empty());
1696 }
1697
1698 #[test]
1703 fn styled_stroke_empty() {
1704 let r = stroke_line_styled(&[], 1.0, LineCap::Butt, LineJoin::Miter, 2.0);
1705 assert!(r.positions.is_empty());
1706 assert!(r.indices.is_empty());
1707 assert!(r.normals.is_empty());
1708 assert!(r.distances.is_empty());
1709 }
1710
1711 #[test]
1712 fn styled_stroke_single_point() {
1713 let r = stroke_line_styled(
1714 &[GeoCoord::from_lat_lon(0.0, 0.0)],
1715 1.0,
1716 LineCap::Butt,
1717 LineJoin::Miter,
1718 2.0,
1719 );
1720 assert!(r.positions.is_empty());
1721 }
1722
1723 #[test]
1724 fn styled_stroke_butt_cap_two_points() {
1725 let coords = vec![
1726 GeoCoord::from_lat_lon(0.0, 0.0),
1727 GeoCoord::from_lat_lon(0.0, 1.0),
1728 ];
1729 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1730 assert_eq!(r.positions.len(), 4);
1732 assert_eq!(r.normals.len(), 4);
1733 assert_eq!(r.distances.len(), 4);
1734 assert!(!r.indices.is_empty());
1735 let max_idx = r.positions.len() as u32;
1737 assert!(r.indices.iter().all(|&i| i < max_idx));
1738 }
1739
1740 #[test]
1741 fn styled_stroke_square_cap_extends_beyond_endpoints() {
1742 let coords = vec![
1743 GeoCoord::from_lat_lon(0.0, 0.0),
1744 GeoCoord::from_lat_lon(0.0, 1.0),
1745 ];
1746 let hw = 0.1;
1747 let r = stroke_line_styled(&coords, hw, LineCap::Square, LineJoin::Miter, 2.0);
1748 let min_lon: f64 = r
1750 .positions
1751 .iter()
1752 .map(|p| p[0])
1753 .fold(f64::INFINITY, f64::min);
1754 assert!(
1755 min_lon < -0.05,
1756 "square cap should extend before the first vertex, got {min_lon}"
1757 );
1758 }
1759
1760 #[test]
1761 fn styled_stroke_round_cap_produces_more_vertices() {
1762 let coords = vec![
1763 GeoCoord::from_lat_lon(0.0, 0.0),
1764 GeoCoord::from_lat_lon(0.0, 1.0),
1765 ];
1766 let butt = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1767 let round = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Miter, 2.0);
1768 assert!(
1769 round.positions.len() > butt.positions.len(),
1770 "round cap should produce more vertices than butt: {} vs {}",
1771 round.positions.len(),
1772 butt.positions.len(),
1773 );
1774 }
1775
1776 #[test]
1777 fn styled_stroke_distances_are_monotonic() {
1778 let coords = vec![
1779 GeoCoord::from_lat_lon(0.0, 0.0),
1780 GeoCoord::from_lat_lon(0.0, 1.0),
1781 GeoCoord::from_lat_lon(0.0, 2.0),
1782 ];
1783 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1784 assert!(r.distances.iter().all(|&d| d >= 0.0));
1786 let max_dist = r.distances.iter().cloned().fold(0.0_f64, f64::max);
1787 assert!(max_dist > 0.0, "max distance should be positive");
1788 }
1789
1790 #[test]
1791 fn styled_stroke_normals_are_unit_length_or_zero() {
1792 let coords = vec![
1793 GeoCoord::from_lat_lon(0.0, 0.0),
1794 GeoCoord::from_lat_lon(0.0, 1.0),
1795 GeoCoord::from_lat_lon(1.0, 1.0),
1796 ];
1797 let r = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Round, 2.0);
1798 for (i, nrm) in r.normals.iter().enumerate() {
1799 let len = (nrm[0] * nrm[0] + nrm[1] * nrm[1]).sqrt();
1800 if r.cap_join[i] > 0.5 {
1801 assert!(
1803 len < CIRCUMSCRIBE + 1e-10 || len < 1e-10,
1804 "cap/join normal length should be ≤{CIRCUMSCRIBE} or ~0, got {len}"
1805 );
1806 } else {
1807 assert!(
1809 len < 1.0 + 1e-10 || len < 1e-10,
1810 "body normal length should be ≤1 or ~0, got {len}"
1811 );
1812 }
1813 }
1814 }
1815
1816 #[test]
1817 fn styled_stroke_cap_join_flags_correct_for_round_caps() {
1818 let coords = vec![
1819 GeoCoord::from_lat_lon(0.0, 0.0),
1820 GeoCoord::from_lat_lon(0.0, 1.0),
1821 ];
1822 let r = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Round, 2.0);
1823 assert_eq!(r.cap_join.len(), r.positions.len());
1824 let cap_join_count = r.cap_join.iter().filter(|&&f| f > 0.5).count();
1826 assert!(cap_join_count > 0, "round cap should flag some vertices");
1827 let body_count = r.cap_join.iter().filter(|&&f| f < 0.5).count();
1829 assert!(body_count > 0, "ribbon body vertices should exist");
1830 }
1831
1832 #[test]
1833 fn styled_stroke_cap_join_flags_zero_for_butt_caps() {
1834 let coords = vec![
1835 GeoCoord::from_lat_lon(0.0, 0.0),
1836 GeoCoord::from_lat_lon(0.0, 1.0),
1837 ];
1838 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Bevel, 2.0);
1839 assert_eq!(r.cap_join.len(), r.positions.len());
1840 for &flag in &r.cap_join {
1842 assert!(
1843 flag < 0.5,
1844 "butt cap + bevel join should have no cap_join flags"
1845 );
1846 }
1847 }
1848
1849 #[test]
1850 fn styled_stroke_circumscribed_fan_extends_beyond_unit() {
1851 let coords = vec![
1854 GeoCoord::from_lat_lon(0.0, 0.0),
1855 GeoCoord::from_lat_lon(0.0, 1.0),
1856 ];
1857 let r = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Round, 2.0);
1858 let mut found_circumscribed = false;
1859 for (i, nrm) in r.normals.iter().enumerate() {
1860 if r.cap_join[i] > 0.5 {
1861 let len = (nrm[0] * nrm[0] + nrm[1] * nrm[1]).sqrt();
1862 if len > 1.0 + 1e-10 {
1863 found_circumscribed = true;
1864 assert!(
1865 (len - CIRCUMSCRIBE).abs() < 1e-6,
1866 "circumscribed normal should be ~{CIRCUMSCRIBE}, got {len}"
1867 );
1868 }
1869 }
1870 }
1871 assert!(
1872 found_circumscribed,
1873 "should find circumscribed fan normals with length > 1"
1874 );
1875 }
1876
1877 #[test]
1878 fn styled_stroke_bevel_join_at_sharp_angle() {
1879 let coords = vec![
1881 GeoCoord::from_lat_lon(0.0, 0.0),
1882 GeoCoord::from_lat_lon(0.0, 1.0),
1883 GeoCoord::from_lat_lon(1.0, 1.0),
1884 ];
1885 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Bevel, 2.0);
1886 let straight = stroke_line_styled(
1888 &[
1889 GeoCoord::from_lat_lon(0.0, 0.0),
1890 GeoCoord::from_lat_lon(0.0, 2.0),
1891 ],
1892 0.01,
1893 LineCap::Butt,
1894 LineJoin::Bevel,
1895 2.0,
1896 );
1897 assert!(
1898 r.positions.len() > straight.positions.len(),
1899 "bevel join should add extra vertices: {} vs {}",
1900 r.positions.len(),
1901 straight.positions.len(),
1902 );
1903 }
1904
1905 #[test]
1906 fn styled_stroke_round_join_produces_fan_vertices() {
1907 let coords = vec![
1908 GeoCoord::from_lat_lon(0.0, 0.0),
1909 GeoCoord::from_lat_lon(0.0, 1.0),
1910 GeoCoord::from_lat_lon(1.0, 1.0),
1911 ];
1912 let bevel = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Bevel, 2.0);
1913 let round = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Round, 2.0);
1914 assert!(
1915 round.positions.len() > bevel.positions.len(),
1916 "round join should produce more vertices than bevel: {} vs {}",
1917 round.positions.len(),
1918 bevel.positions.len(),
1919 );
1920 }
1921
1922 #[test]
1923 fn styled_stroke_miter_join_collinear() {
1924 let coords = vec![
1926 GeoCoord::from_lat_lon(0.0, 0.0),
1927 GeoCoord::from_lat_lon(0.0, 1.0),
1928 GeoCoord::from_lat_lon(0.0, 2.0),
1929 ];
1930 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1931 let max_idx = r.positions.len() as u32;
1933 assert!(r.indices.iter().all(|&i| i < max_idx));
1934 assert!(!r.indices.is_empty());
1935 }
1936
1937 #[test]
1938 fn styled_stroke_indices_valid_for_complex_line() {
1939 let coords: Vec<GeoCoord> = (0..10)
1941 .map(|i| {
1942 let lat = if i % 2 == 0 { 0.0 } else { 1.0 };
1943 GeoCoord::from_lat_lon(lat, i as f64)
1944 })
1945 .collect();
1946 for cap in [LineCap::Butt, LineCap::Round, LineCap::Square] {
1947 for join in [LineJoin::Miter, LineJoin::Bevel, LineJoin::Round] {
1948 let r = stroke_line_styled(&coords, 0.01, cap, join, 2.0);
1949 let max_idx = r.positions.len() as u32;
1950 assert!(
1951 r.indices.iter().all(|&i| i < max_idx),
1952 "invalid index for cap={cap:?} join={join:?}: max valid={max_idx}",
1953 );
1954 assert_eq!(r.positions.len(), r.normals.len());
1955 assert_eq!(r.positions.len(), r.distances.len());
1956 }
1957 }
1958 }
1959}