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
97fn point_in_triangle(
102 px: f64, py: f64,
103 ax: f64, ay: f64,
104 bx: f64, by: f64,
105 cx: f64, cy: f64,
106) -> bool {
107 let d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by);
108 let d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy);
109 let d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay);
110 let has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
111 let has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
112 !(has_neg && has_pos)
113}
114
115fn cross_z(ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64) -> f64 {
117 (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
118}
119
120fn earcut_flat(flat: &[f64], hole_starts: &[usize]) -> Vec<u32> {
129 let total_verts = flat.len() / 2;
130 if total_verts < 3 {
131 return Vec::new();
132 }
133
134 let mut prev = vec![0usize; total_verts];
136 let mut next = vec![0usize; total_verts];
137
138 let outer_end = if hole_starts.is_empty() {
140 total_verts
141 } else {
142 hole_starts[0]
143 };
144
145 if outer_end < 3 {
146 return Vec::new();
147 }
148
149 let outer_ccw = ring_area_flat(flat, 0, outer_end) > 0.0;
151 link_ring(&mut prev, &mut next, 0, outer_end, outer_ccw);
152
153 let mut outer_start = 0usize;
156 for (hi, &h_start) in hole_starts.iter().enumerate() {
157 let h_end = if hi + 1 < hole_starts.len() {
158 hole_starts[hi + 1]
159 } else {
160 total_verts
161 };
162 if h_end - h_start < 3 {
163 continue;
164 }
165 let hole_ccw = ring_area_flat(flat, h_start, h_end) > 0.0;
166 link_ring(&mut prev, &mut next, h_start, h_end, !hole_ccw);
168
169 let bridge_hole = rightmost_vertex(flat, h_start, h_end);
172 outer_start = bridge_hole_to_outer(
173 flat, &mut prev, &mut next, outer_start, outer_end, bridge_hole,
174 );
175 }
176
177 let mut indices = Vec::with_capacity((total_verts - 2) * 3);
179 ear_clip(flat, &prev, &mut next.clone(), outer_start, &mut indices, total_verts);
180 indices
181}
182
183fn ring_area_flat(flat: &[f64], start: usize, end: usize) -> f64 {
185 let mut area = 0.0;
186 let mut j = end - 1;
187 for i in start..end {
188 let jx = flat[j * 2];
189 let jy = flat[j * 2 + 1];
190 let ix = flat[i * 2];
191 let iy = flat[i * 2 + 1];
192 area += (jx - ix) * (jy + iy);
193 j = i;
194 }
195 area * 0.5
196}
197
198fn link_ring(prev: &mut [usize], next: &mut [usize], start: usize, end: usize, forward: bool) {
201 if forward {
202 for i in start..end {
203 let p = if i == start { end - 1 } else { i - 1 };
204 let n = if i == end - 1 { start } else { i + 1 };
205 prev[i] = p;
206 next[i] = n;
207 }
208 } else {
209 for i in start..end {
210 let p = if i == end - 1 { start } else { i + 1 };
211 let n = if i == start { end - 1 } else { i - 1 };
212 prev[i] = p;
213 next[i] = n;
214 }
215 }
216}
217
218fn rightmost_vertex(flat: &[f64], start: usize, end: usize) -> usize {
220 let mut best = start;
221 for i in (start + 1)..end {
222 if flat[i * 2] > flat[best * 2]
223 || (flat[i * 2] == flat[best * 2] && flat[i * 2 + 1] < flat[best * 2 + 1])
224 {
225 best = i;
226 }
227 }
228 best
229}
230
231fn bridge_hole_to_outer(
233 flat: &[f64],
234 prev: &mut [usize],
235 next: &mut [usize],
236 outer_start: usize,
237 _outer_end: usize,
238 hole_vertex: usize,
239) -> usize {
240 let hx = flat[hole_vertex * 2];
242 let hy = flat[hole_vertex * 2 + 1];
243 let mut best = outer_start;
244 let mut best_dist = f64::INFINITY;
245 let mut i = outer_start;
246 loop {
247 let ix = flat[i * 2];
248 let iy = flat[i * 2 + 1];
249 let d = (ix - hx) * (ix - hx) + (iy - hy) * (iy - hy);
250 if d < best_dist {
251 best_dist = d;
252 best = i;
253 }
254 i = next[i];
255 if i == outer_start {
256 break;
257 }
258 }
259
260 let best_next = next[best];
266 let hole_prev = prev[hole_vertex];
267
268 next[best] = hole_vertex;
269 prev[hole_vertex] = best;
270 next[hole_prev] = best_next;
271 prev[best_next] = hole_prev;
272
273 best
274}
275
276fn ear_clip(
278 flat: &[f64],
279 orig_prev: &[usize],
280 next: &mut [usize],
281 start: usize,
282 indices: &mut Vec<u32>,
283 _total_verts: usize,
284) {
285 let mut prev = orig_prev.to_vec();
287 let mut remaining = {
288 let mut count = 1usize;
290 let mut i = next[start];
291 while i != start {
292 count += 1;
293 i = next[i];
294 }
295 count
296 };
297
298 let mut ear = start;
299 let mut _stop = ear;
300 let mut pass = 0;
301
302 while remaining > 2 {
303 let a = prev[ear];
304 let b = ear;
305 let c = next[ear];
306
307 if is_ear(flat, &prev, next, a, b, c) {
308 indices.push(a as u32);
309 indices.push(b as u32);
310 indices.push(c as u32);
311
312 next[a] = c;
314 prev[c] = a;
315
316 remaining -= 1;
317 _stop = c;
318 ear = c;
319 pass = 0;
320 } else {
321 ear = next[ear];
322 pass += 1;
323 if pass >= remaining {
324 fan_remaining(next, start, ear, remaining, indices);
327 break;
328 }
329 }
330 }
331}
332
333fn is_ear(
335 flat: &[f64],
336 prev: &[usize],
337 next: &[usize],
338 a: usize,
339 b: usize,
340 c: usize,
341) -> bool {
342 let ax = flat[a * 2];
343 let ay = flat[a * 2 + 1];
344 let bx = flat[b * 2];
345 let by = flat[b * 2 + 1];
346 let cx = flat[c * 2];
347 let cy = flat[c * 2 + 1];
348
349 if cross_z(ax, ay, bx, by, cx, cy) <= 0.0 {
351 return false;
352 }
353
354 let mut p = next[c];
356 while p != a {
357 let px = flat[p * 2];
358 let py = flat[p * 2 + 1];
359 if point_in_triangle(px, py, ax, ay, bx, by, cx, cy)
360 && cross_z(
361 flat[prev[p] * 2], flat[prev[p] * 2 + 1],
362 px, py,
363 flat[next[p] * 2], flat[next[p] * 2 + 1],
364 ) >= 0.0
365 {
366 return false;
367 }
368 p = next[p];
369 }
370 true
371}
372
373fn fan_remaining(
375 next: &[usize],
376 _start: usize,
377 first: usize,
378 remaining: usize,
379 indices: &mut Vec<u32>,
380) {
381 if remaining < 3 {
382 return;
383 }
384 let anchor = first;
385 let mut b = next[anchor];
386 for _ in 0..remaining - 2 {
387 let c = next[b];
388 indices.push(anchor as u32);
389 indices.push(b as u32);
390 indices.push(c as u32);
391 b = c;
392 }
393}
394
395pub fn triangulate_polygon(coords: &[GeoCoord]) -> Vec<u32> {
436 let n = coords.len();
437 if n < 3 {
438 return Vec::new();
439 }
440
441 let effective_len = effective_ring_len(coords);
442 if effective_len < 3 {
443 return Vec::new();
444 }
445
446 let mut flat = Vec::with_capacity(effective_len * 2);
448 for c in &coords[..effective_len] {
449 flat.push(c.lon);
450 flat.push(c.lat);
451 }
452
453 earcut_flat(&flat, &[])
454}
455
456pub fn triangulate_polygon_with_holes(
488 exterior: &[GeoCoord],
489 holes: &[&[GeoCoord]],
490) -> Vec<u32> {
491 let ext_len = effective_ring_len(exterior);
492 if ext_len < 3 {
493 return Vec::new();
494 }
495
496 let hole_lens: Vec<usize> = holes.iter().map(|h| effective_ring_len(h)).collect();
497 let total_verts: usize = ext_len + hole_lens.iter().sum::<usize>();
498 let mut flat = Vec::with_capacity(total_verts * 2);
499
500 for c in &exterior[..ext_len] {
502 flat.push(c.lon);
503 flat.push(c.lat);
504 }
505
506 let mut hole_starts = Vec::with_capacity(holes.len());
508 let mut offset = ext_len;
509 for (i, hole) in holes.iter().enumerate() {
510 let hl = hole_lens[i];
511 if hl < 3 {
512 continue;
513 }
514 hole_starts.push(offset);
515 for c in &hole[..hl] {
516 flat.push(c.lon);
517 flat.push(c.lat);
518 }
519 offset += hl;
520 }
521
522 earcut_flat(&flat, &hole_starts)
523}
524
525pub fn stroke_line(coords: &[GeoCoord], half_width: f64) -> (Vec<[f64; 2]>, Vec<u32>) {
578 if coords.len() < 2 {
579 return (Vec::new(), Vec::new());
580 }
581
582 let vertex_count = coords.len() * 2;
583 let segment_count = coords.len() - 1;
584 let mut positions = Vec::with_capacity(vertex_count);
585 let mut indices = Vec::with_capacity(segment_count * 6);
586
587 for i in 0..coords.len() {
588 let curr = Vec2 {
589 x: coords[i].lon,
590 y: coords[i].lat,
591 };
592
593 let tangent = if i == 0 {
595 let next = Vec2 {
597 x: coords[1].lon,
598 y: coords[1].lat,
599 };
600 normalize(Vec2 {
601 x: next.x - curr.x,
602 y: next.y - curr.y,
603 })
604 } else if i == coords.len() - 1 {
605 let prev = Vec2 {
607 x: coords[i - 1].lon,
608 y: coords[i - 1].lat,
609 };
610 normalize(Vec2 {
611 x: curr.x - prev.x,
612 y: curr.y - prev.y,
613 })
614 } else {
615 let prev = Vec2 {
617 x: coords[i - 1].lon,
618 y: coords[i - 1].lat,
619 };
620 let next = Vec2 {
621 x: coords[i + 1].lon,
622 y: coords[i + 1].lat,
623 };
624 normalize(Vec2 {
625 x: next.x - prev.x,
626 y: next.y - prev.y,
627 })
628 };
629
630 let normal = Vec2 {
632 x: -tangent.y,
633 y: tangent.x,
634 };
635
636 positions.push([
638 curr.x + normal.x * half_width,
639 curr.y + normal.y * half_width,
640 ]);
641 positions.push([
642 curr.x - normal.x * half_width,
643 curr.y - normal.y * half_width,
644 ]);
645 }
646
647 for i in 0..segment_count as u32 {
655 let base = i * 2;
656 indices.push(base);
657 indices.push(base + 1);
658 indices.push(base + 2);
659 indices.push(base + 1);
660 indices.push(base + 3);
661 indices.push(base + 2);
662 }
663
664 (positions, indices)
665}
666
667#[derive(Debug, Clone)]
674pub struct StrokeLineResult {
675 pub positions: Vec<[f64; 2]>,
677 pub indices: Vec<u32>,
679 pub normals: Vec<[f64; 2]>,
682 pub distances: Vec<f64>,
685 pub cap_join: Vec<f32>,
692}
693
694const ROUND_SEGMENTS: u32 = 8;
696
697const CIRCUMSCRIBE: f64 = {
702 1.01959
706};
707
708pub fn stroke_line_styled(
724 coords: &[GeoCoord],
725 half_width: f64,
726 cap: LineCap,
727 join: LineJoin,
728 miter_limit: f32,
729) -> StrokeLineResult {
730 let empty = StrokeLineResult {
731 positions: Vec::new(),
732 indices: Vec::new(),
733 normals: Vec::new(),
734 distances: Vec::new(),
735 cap_join: Vec::new(),
736 };
737 if coords.len() < 2 {
738 return empty;
739 }
740
741 let seg_count = coords.len() - 1;
743 let mut seg_tangents = Vec::with_capacity(seg_count);
744 let mut seg_normals = Vec::with_capacity(seg_count);
745 let mut seg_lengths = Vec::with_capacity(seg_count);
746
747 for i in 0..seg_count {
748 let dx = coords[i + 1].lon - coords[i].lon;
749 let dy = coords[i + 1].lat - coords[i].lat;
750 let len = (dx * dx + dy * dy).sqrt();
751 let t = if len < 1e-15 {
752 Vec2 { x: 1.0, y: 0.0 }
753 } else {
754 Vec2 { x: dx / len, y: dy / len }
755 };
756 let n = Vec2 { x: -t.y, y: t.x };
757 seg_tangents.push(t);
758 seg_normals.push(n);
759 seg_lengths.push(len);
760 }
761
762 let mut cum_dist = Vec::with_capacity(coords.len());
764 cum_dist.push(0.0);
765 for i in 0..seg_count {
766 cum_dist.push(cum_dist[i] + seg_lengths[i]);
767 }
768
769 let cap_extra = match cap { LineCap::Round => ROUND_SEGMENTS as usize * 2, _ => 2 };
772 let join_extra = match join { LineJoin::Round => ROUND_SEGMENTS as usize, _ => 2 };
773 let est_verts = coords.len() * 2 + cap_extra * 2 + join_extra * seg_count;
774 let est_indices = seg_count * 6 + cap_extra * 6 + join_extra * 3 * seg_count;
775 let mut positions = Vec::with_capacity(est_verts);
776 let mut normals = Vec::with_capacity(est_verts);
777 let mut distances = Vec::with_capacity(est_verts);
778 let mut cap_join_flags = Vec::with_capacity(est_verts);
779 let mut indices = Vec::with_capacity(est_indices);
780
781 #[inline]
783 fn push_vert(
784 positions: &mut Vec<[f64; 2]>,
785 normals: &mut Vec<[f64; 2]>,
786 distances: &mut Vec<f64>,
787 cap_join_flags: &mut Vec<f32>,
788 pos: [f64; 2],
789 nrm: [f64; 2],
790 dist: f64,
791 cj: f32,
792 ) -> u32 {
793 let idx = positions.len() as u32;
794 positions.push(pos);
795 normals.push(nrm);
796 distances.push(dist);
797 cap_join_flags.push(cj);
798 idx
799 }
800
801 let first_n = seg_normals[0];
803 let first_t = seg_tangents[0];
804 let cx = coords[0].lon;
805 let cy = coords[0].lat;
806
807 match cap {
808 LineCap::Butt => {
809 let l = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
811 [cx + first_n.x * half_width, cy + first_n.y * half_width],
812 [first_n.x, first_n.y], 0.0, 0.0);
813 let r = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
814 [cx - first_n.x * half_width, cy - first_n.y * half_width],
815 [-first_n.x, -first_n.y], 0.0, 0.0);
816 let _ = (l, r); }
818 LineCap::Square => {
819 let bx = cx - first_t.x * half_width;
821 let by = cy - first_t.y * half_width;
822 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
823 [bx + first_n.x * half_width, by + first_n.y * half_width],
824 [first_n.x, first_n.y], 0.0, 0.0);
825 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
826 [bx - first_n.x * half_width, by - first_n.y * half_width],
827 [-first_n.x, -first_n.y], 0.0, 0.0);
828 }
829 LineCap::Round => {
830 let center_idx = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
833 [cx, cy], [0.0, 0.0], 0.0, 1.0);
834 let start_angle = first_n.y.atan2(first_n.x);
836 let hw_circ = half_width * CIRCUMSCRIBE;
842 let mut fan_verts = Vec::with_capacity(ROUND_SEGMENTS as usize + 1);
843 for k in 0..=ROUND_SEGMENTS {
844 let a = start_angle + std::f64::consts::PI * k as f64 / ROUND_SEGMENTS as f64;
845 let nx = a.cos();
846 let ny = a.sin();
847 let v = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
848 [cx + nx * hw_circ, cy + ny * hw_circ],
849 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE], 0.0, 1.0);
850 fan_verts.push(v);
851 }
852 for k in 0..ROUND_SEGMENTS {
853 indices.push(center_idx);
854 indices.push(fan_verts[k as usize + 1]);
855 indices.push(fan_verts[k as usize]);
856 }
857 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
862 [cx + first_n.x * half_width, cy + first_n.y * half_width],
863 [first_n.x, first_n.y], 0.0, 0.0);
864 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
865 [cx - first_n.x * half_width, cy - first_n.y * half_width],
866 [-first_n.x, -first_n.y], 0.0, 0.0);
867 }
868 }
869
870 for i in 0..seg_count {
876 let n = seg_normals[i];
877 let dist = cum_dist[i + 1];
878 let vx = coords[i + 1].lon;
879 let vy = coords[i + 1].lat;
880
881 let prev_left = positions.len() as u32 - 2;
883 let prev_right = positions.len() as u32 - 1;
884
885 if i < seg_count - 1 {
886 let n_next = seg_normals[i + 1];
888
889 let bx = n.x + n_next.x;
891 let by = n.y + n_next.y;
892 let blen = (bx * bx + by * by).sqrt();
893
894 let cross = seg_tangents[i].x * seg_tangents[i + 1].y
896 - seg_tangents[i].y * seg_tangents[i + 1].x;
897
898 if blen < 1e-12 {
899 let l = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
901 [vx + n.x * half_width, vy + n.y * half_width],
902 [n.x, n.y], dist, 0.0);
903 let r = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
904 [vx - n.x * half_width, vy - n.y * half_width],
905 [-n.x, -n.y], dist, 0.0);
906 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
908 let l2 = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
910 [vx + n_next.x * half_width, vy + n_next.y * half_width],
911 [n_next.x, n_next.y], dist, 0.0);
912 let r2 = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
913 [vx - n_next.x * half_width, vy - n_next.y * half_width],
914 [-n_next.x, -n_next.y], dist, 0.0);
915 indices.extend_from_slice(&[l, r, l2, r, r2, l2]);
916 } else {
917 let miter_nx = bx / blen;
918 let miter_ny = by / blen;
919 let dot = n.x * miter_nx + n.y * miter_ny;
921 let miter_len = if dot.abs() > 1e-12 { 1.0 / dot } else { miter_limit as f64 + 1.0 };
922
923 let use_miter = matches!(join, LineJoin::Miter) && miter_len <= miter_limit as f64;
924
925 if use_miter {
926 let hw_m = half_width * miter_len;
928 let l = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
929 [vx + miter_nx * hw_m, vy + miter_ny * hw_m],
930 [miter_nx, miter_ny], dist, 0.0);
931 let r = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
932 [vx - miter_nx * hw_m, vy - miter_ny * hw_m],
933 [-miter_nx, -miter_ny], dist, 0.0);
934 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
935 } else if matches!(join, LineJoin::Round) {
936 let l_in = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
938 [vx + n.x * half_width, vy + n.y * half_width],
939 [n.x, n.y], dist, 0.0);
940 let r_in = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
941 [vx - n.x * half_width, vy - n.y * half_width],
942 [-n.x, -n.y], dist, 0.0);
943 indices.extend_from_slice(&[prev_left, prev_right, l_in, prev_right, r_in, l_in]);
944
945 let hw_circ = half_width * CIRCUMSCRIBE;
948 let center_idx = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
949 [vx, vy], [0.0, 0.0], dist, 1.0);
950
951 if cross > 0.0 {
952 let a0 = n.y.atan2(n.x);
954 let a1 = n_next.y.atan2(n_next.x);
955 let mut da = a1 - a0;
956 if da < 0.0 { da += 2.0 * std::f64::consts::PI; }
957 let steps = ROUND_SEGMENTS;
958 let mut prev_v = l_in;
959 for k in 1..=steps {
960 let a = a0 + da * k as f64 / steps as f64;
961 let nx = a.cos();
962 let ny = a.sin();
963 let v = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
964 [vx + nx * hw_circ, vy + ny * hw_circ],
965 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE], dist, 1.0);
966 indices.extend_from_slice(&[center_idx, prev_v, v]);
967 prev_v = v;
968 }
969 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
971 [vx + n_next.x * half_width, vy + n_next.y * half_width],
972 [n_next.x, n_next.y], dist, 0.0);
973 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
974 [vx - n_next.x * half_width, vy - n_next.y * half_width],
975 [-n_next.x, -n_next.y], dist, 0.0);
976 } else {
977 let a0 = (-n.y).atan2(-n.x);
979 let a1 = (-n_next.y).atan2(-n_next.x);
980 let mut da = a1 - a0;
981 if da > 0.0 { da -= 2.0 * std::f64::consts::PI; }
982 let steps = ROUND_SEGMENTS;
983 let mut prev_v = r_in;
984 for k in 1..=steps {
985 let a = a0 + da * k as f64 / steps as f64;
986 let nx = a.cos();
987 let ny = a.sin();
988 let v = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
989 [vx + nx * hw_circ, vy + ny * hw_circ],
990 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE], dist, 1.0);
991 indices.extend_from_slice(&[center_idx, v, prev_v]);
992 prev_v = v;
993 }
994 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
995 [vx + n_next.x * half_width, vy + n_next.y * half_width],
996 [n_next.x, n_next.y], dist, 0.0);
997 push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
998 [vx - n_next.x * half_width, vy - n_next.y * half_width],
999 [-n_next.x, -n_next.y], dist, 0.0);
1000 }
1001 } else {
1002 let l_in = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1004 [vx + n.x * half_width, vy + n.y * half_width],
1005 [n.x, n.y], dist, 0.0);
1006 let r_in = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1007 [vx - n.x * half_width, vy - n.y * half_width],
1008 [-n.x, -n.y], dist, 0.0);
1009 indices.extend_from_slice(&[prev_left, prev_right, l_in, prev_right, r_in, l_in]);
1010
1011 let l2 = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1013 [vx + n_next.x * half_width, vy + n_next.y * half_width],
1014 [n_next.x, n_next.y], dist, 0.0);
1015 let r2 = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1016 [vx - n_next.x * half_width, vy - n_next.y * half_width],
1017 [-n_next.x, -n_next.y], dist, 0.0);
1018 indices.extend_from_slice(&[l_in, r_in, l2, r_in, r2, l2]);
1019 }
1020 }
1021 } else {
1022 let l = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1024 [vx + n.x * half_width, vy + n.y * half_width],
1025 [n.x, n.y], dist, 0.0);
1026 let r = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1027 [vx - n.x * half_width, vy - n.y * half_width],
1028 [-n.x, -n.y], dist, 0.0);
1029 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
1030 }
1031 }
1032
1033 let last_n = seg_normals[seg_count - 1];
1035 let last_t = seg_tangents[seg_count - 1];
1036 let ex = coords[coords.len() - 1].lon;
1037 let ey = coords[coords.len() - 1].lat;
1038 let end_dist = cum_dist[coords.len() - 1];
1039
1040 match cap {
1041 LineCap::Butt => { }
1042 LineCap::Square => {
1043 let prev_left = positions.len() as u32 - 2;
1044 let prev_right = positions.len() as u32 - 1;
1045 let fx = ex + last_t.x * half_width;
1046 let fy = ey + last_t.y * half_width;
1047 let l = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1048 [fx + last_n.x * half_width, fy + last_n.y * half_width],
1049 [last_n.x, last_n.y], end_dist, 0.0);
1050 let r = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1051 [fx - last_n.x * half_width, fy - last_n.y * half_width],
1052 [-last_n.x, -last_n.y], end_dist, 0.0);
1053 indices.extend_from_slice(&[prev_left, prev_right, l, prev_right, r, l]);
1054 }
1055 LineCap::Round => {
1056 let center_idx = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1057 [ex, ey], [0.0, 0.0], end_dist, 1.0);
1058 let hw_circ = half_width * CIRCUMSCRIBE;
1060 let start_angle = last_n.y.atan2(last_n.x);
1061 let mut fan_verts = Vec::with_capacity(ROUND_SEGMENTS as usize + 1);
1062 for k in 0..=ROUND_SEGMENTS {
1063 let a = start_angle - std::f64::consts::PI * k as f64 / ROUND_SEGMENTS as f64;
1064 let nx = a.cos();
1065 let ny = a.sin();
1066 let v = push_vert(&mut positions, &mut normals, &mut distances, &mut cap_join_flags,
1067 [ex + nx * hw_circ, ey + ny * hw_circ],
1068 [nx * CIRCUMSCRIBE, ny * CIRCUMSCRIBE], end_dist, 1.0);
1069 fan_verts.push(v);
1070 }
1071 let prev_left = center_idx - 2;
1073 let prev_right = center_idx - 1;
1074 indices.extend_from_slice(&[prev_left, fan_verts[0], center_idx]);
1075 indices.extend_from_slice(&[prev_right, center_idx, fan_verts[ROUND_SEGMENTS as usize]]);
1076 for k in 0..ROUND_SEGMENTS {
1077 indices.push(center_idx);
1078 indices.push(fan_verts[k as usize]);
1079 indices.push(fan_verts[k as usize + 1]);
1080 }
1081 }
1082 }
1083
1084 StrokeLineResult { positions, indices, normals, distances, cap_join: cap_join_flags }
1085}
1086
1087#[cfg(test)]
1092mod tests {
1093 use super::*;
1094
1095 #[test]
1100 fn triangulate_empty() {
1101 assert!(triangulate_polygon(&[]).is_empty());
1102 }
1103
1104 #[test]
1105 fn triangulate_single_point() {
1106 assert!(triangulate_polygon(&[GeoCoord::from_lat_lon(0.0, 0.0)]).is_empty());
1107 }
1108
1109 #[test]
1110 fn triangulate_two_points() {
1111 let coords = vec![
1112 GeoCoord::from_lat_lon(0.0, 0.0),
1113 GeoCoord::from_lat_lon(1.0, 1.0),
1114 ];
1115 assert!(triangulate_polygon(&coords).is_empty());
1116 }
1117
1118 #[test]
1119 fn triangulate_triangle() {
1120 let coords = vec![
1121 GeoCoord::from_lat_lon(0.0, 0.0),
1122 GeoCoord::from_lat_lon(0.0, 1.0),
1123 GeoCoord::from_lat_lon(1.0, 0.0),
1124 ];
1125 let indices = triangulate_polygon(&coords);
1126 assert_eq!(indices.len(), 3);
1127 let mut sorted = indices.clone();
1129 sorted.sort();
1130 assert_eq!(sorted, vec![0, 1, 2]);
1131 }
1132
1133 #[test]
1134 fn triangulate_square() {
1135 let coords = vec![
1136 GeoCoord::from_lat_lon(0.0, 0.0),
1137 GeoCoord::from_lat_lon(0.0, 1.0),
1138 GeoCoord::from_lat_lon(1.0, 1.0),
1139 GeoCoord::from_lat_lon(1.0, 0.0),
1140 ];
1141 let indices = triangulate_polygon(&coords);
1142 assert_eq!(indices.len(), 6); assert!(indices.iter().all(|&i| i < 4));
1144 }
1145
1146 #[test]
1147 fn triangulate_closed_polygon() {
1148 let coords = vec![
1150 GeoCoord::from_lat_lon(0.0, 0.0),
1151 GeoCoord::from_lat_lon(0.0, 1.0),
1152 GeoCoord::from_lat_lon(1.0, 1.0),
1153 GeoCoord::from_lat_lon(1.0, 0.0),
1154 GeoCoord::from_lat_lon(0.0, 0.0),
1155 ];
1156 let indices = triangulate_polygon(&coords);
1157 assert_eq!(indices.len(), 6); assert!(indices.iter().all(|&i| i < 4));
1160 }
1161
1162 #[test]
1163 fn triangulate_closing_vertex_reduces_below_three() {
1164 let coords = vec![
1166 GeoCoord::from_lat_lon(0.0, 0.0),
1167 GeoCoord::from_lat_lon(1.0, 1.0),
1168 GeoCoord::from_lat_lon(0.0, 0.0),
1169 ];
1170 let indices = triangulate_polygon(&coords);
1175 assert_eq!(indices.len(), 3);
1176 }
1177
1178 #[test]
1179 fn triangulate_pentagon() {
1180 let coords = vec![
1181 GeoCoord::from_lat_lon(0.0, 0.0),
1182 GeoCoord::from_lat_lon(0.0, 2.0),
1183 GeoCoord::from_lat_lon(1.0, 3.0),
1184 GeoCoord::from_lat_lon(2.0, 2.0),
1185 GeoCoord::from_lat_lon(2.0, 0.0),
1186 ];
1187 let indices = triangulate_polygon(&coords);
1188 assert_eq!(indices.len(), 9);
1190 assert!(indices.iter().all(|&i| i < 5));
1191 }
1192
1193 #[test]
1194 fn triangulate_indices_are_valid() {
1195 let coords: Vec<GeoCoord> = (0..20)
1197 .map(|i| {
1198 let angle = 2.0 * std::f64::consts::PI * i as f64 / 20.0;
1199 GeoCoord::from_lat_lon(angle.sin() * 10.0, angle.cos() * 10.0)
1200 })
1201 .collect();
1202 let indices = triangulate_polygon(&coords);
1203 assert_eq!(indices.len(), 18 * 3); assert!(indices.iter().all(|&i| (i as usize) < coords.len()));
1205 }
1206
1207 #[test]
1212 fn stroke_empty() {
1213 let (p, i) = stroke_line(&[], 1.0);
1214 assert!(p.is_empty());
1215 assert!(i.is_empty());
1216 }
1217
1218 #[test]
1219 fn stroke_single_point() {
1220 let (p, i) = stroke_line(&[GeoCoord::from_lat_lon(0.0, 0.0)], 1.0);
1221 assert!(p.is_empty());
1222 assert!(i.is_empty());
1223 }
1224
1225 #[test]
1226 fn stroke_line_two_points() {
1227 let coords = vec![
1228 GeoCoord::from_lat_lon(0.0, 0.0),
1229 GeoCoord::from_lat_lon(0.0, 1.0),
1230 ];
1231 let (positions, indices) = stroke_line(&coords, 0.01);
1232 assert_eq!(positions.len(), 4); assert_eq!(indices.len(), 6); }
1235
1236 #[test]
1237 fn stroke_line_three_points() {
1238 let coords = vec![
1239 GeoCoord::from_lat_lon(0.0, 0.0),
1240 GeoCoord::from_lat_lon(0.0, 1.0),
1241 GeoCoord::from_lat_lon(0.0, 2.0),
1242 ];
1243 let (positions, indices) = stroke_line(&coords, 0.01);
1244 assert_eq!(positions.len(), 6); assert_eq!(indices.len(), 12); }
1247
1248 #[test]
1249 fn stroke_ribbon_has_nonzero_width() {
1250 let coords = vec![
1252 GeoCoord::from_lat_lon(0.0, 0.0),
1253 GeoCoord::from_lat_lon(0.0, 1.0),
1254 ];
1255 let hw = 0.5;
1256 let (positions, _) = stroke_line(&coords, hw);
1257
1258 let left = positions[0];
1260 let right = positions[1];
1261
1262 let dy = (left[1] - right[1]).abs();
1266 assert!(
1267 (dy - 2.0 * hw).abs() < 1e-12,
1268 "expected ribbon width {}, got {dy}",
1269 2.0 * hw
1270 );
1271 }
1272
1273 #[test]
1274 fn stroke_indices_are_valid() {
1275 let coords: Vec<GeoCoord> = (0..10)
1276 .map(|i| GeoCoord::from_lat_lon(0.0, i as f64))
1277 .collect();
1278 let (positions, indices) = stroke_line(&coords, 0.01);
1279 let max_idx = positions.len() as u32;
1280 assert!(
1281 indices.iter().all(|&i| i < max_idx),
1282 "all indices must be within the position buffer"
1283 );
1284 }
1285
1286 #[test]
1287 fn stroke_zero_width() {
1288 let coords = vec![
1291 GeoCoord::from_lat_lon(0.0, 0.0),
1292 GeoCoord::from_lat_lon(0.0, 1.0),
1293 ];
1294 let (positions, indices) = stroke_line(&coords, 0.0);
1295 assert_eq!(positions.len(), 4);
1296 assert_eq!(indices.len(), 6);
1297 assert_eq!(positions[0], positions[1]);
1299 }
1300
1301 #[test]
1302 fn stroke_coincident_points() {
1303 let coords = vec![
1306 GeoCoord::from_lat_lon(5.0, 10.0),
1307 GeoCoord::from_lat_lon(5.0, 10.0),
1308 ];
1309 let (positions, indices) = stroke_line(&coords, 0.01);
1310 assert_eq!(positions.len(), 4);
1311 assert_eq!(indices.len(), 6);
1312 }
1313
1314 #[test]
1319 fn normalize_unit_vector() {
1320 let v = normalize(Vec2 { x: 3.0, y: 4.0 });
1321 let len = (v.x * v.x + v.y * v.y).sqrt();
1322 assert!((len - 1.0).abs() < 1e-12);
1323 }
1324
1325 #[test]
1326 fn normalize_zero_vector() {
1327 let v = normalize(Vec2 { x: 0.0, y: 0.0 });
1328 assert_eq!(v.x, 0.0);
1329 assert_eq!(v.y, 0.0);
1330 }
1331
1332 #[test]
1333 fn normalize_tiny_vector() {
1334 let v = normalize(Vec2 { x: 1e-16, y: 0.0 });
1336 assert_eq!(v.x, 0.0);
1337 assert_eq!(v.y, 0.0);
1338 }
1339
1340 #[test]
1345 fn triangulate_concave_l_shape() {
1346 let coords = vec![
1354 GeoCoord::from_lat_lon(0.0, 0.0),
1355 GeoCoord::from_lat_lon(0.0, 2.0),
1356 GeoCoord::from_lat_lon(1.0, 2.0),
1357 GeoCoord::from_lat_lon(1.0, 1.0),
1358 GeoCoord::from_lat_lon(2.0, 1.0),
1359 GeoCoord::from_lat_lon(2.0, 0.0),
1360 ];
1361 let indices = triangulate_polygon(&coords);
1362 assert_eq!(indices.len(), 12);
1364 assert!(indices.iter().all(|&i| i < 6));
1365
1366 let area: f64 = indices.chunks(3).map(|tri| {
1369 let a = &coords[tri[0] as usize];
1370 let b = &coords[tri[1] as usize];
1371 let c = &coords[tri[2] as usize];
1372 ((b.lon - a.lon) * (c.lat - a.lat) - (c.lon - a.lon) * (b.lat - a.lat)) * 0.5
1374 }).sum::<f64>().abs();
1375 assert!((area - 3.0).abs() < 1e-6, "L-shape area should be 3.0, got {area}");
1376 }
1377
1378 #[test]
1383 fn triangulate_with_hole() {
1384 let exterior = vec![
1385 GeoCoord::from_lat_lon(0.0, 0.0),
1386 GeoCoord::from_lat_lon(0.0, 4.0),
1387 GeoCoord::from_lat_lon(4.0, 4.0),
1388 GeoCoord::from_lat_lon(4.0, 0.0),
1389 ];
1390 let hole = vec![
1391 GeoCoord::from_lat_lon(1.0, 1.0),
1392 GeoCoord::from_lat_lon(1.0, 3.0),
1393 GeoCoord::from_lat_lon(3.0, 3.0),
1394 GeoCoord::from_lat_lon(3.0, 1.0),
1395 ];
1396 let indices = triangulate_polygon_with_holes(&exterior, &[&hole]);
1397 assert_eq!(indices.len() % 3, 0);
1399 assert!(!indices.is_empty());
1400 assert!(indices.iter().all(|&i| (i as usize) < 8));
1402 }
1403
1404 #[test]
1405 fn triangulate_with_hole_empty_exterior() {
1406 let exterior: Vec<GeoCoord> = Vec::new();
1407 let hole = vec![
1408 GeoCoord::from_lat_lon(1.0, 1.0),
1409 GeoCoord::from_lat_lon(1.0, 3.0),
1410 GeoCoord::from_lat_lon(3.0, 3.0),
1411 ];
1412 assert!(triangulate_polygon_with_holes(&exterior, &[&hole]).is_empty());
1413 }
1414
1415 #[test]
1420 fn styled_stroke_empty() {
1421 let r = stroke_line_styled(&[], 1.0, LineCap::Butt, LineJoin::Miter, 2.0);
1422 assert!(r.positions.is_empty());
1423 assert!(r.indices.is_empty());
1424 assert!(r.normals.is_empty());
1425 assert!(r.distances.is_empty());
1426 }
1427
1428 #[test]
1429 fn styled_stroke_single_point() {
1430 let r = stroke_line_styled(
1431 &[GeoCoord::from_lat_lon(0.0, 0.0)],
1432 1.0, LineCap::Butt, LineJoin::Miter, 2.0,
1433 );
1434 assert!(r.positions.is_empty());
1435 }
1436
1437 #[test]
1438 fn styled_stroke_butt_cap_two_points() {
1439 let coords = vec![
1440 GeoCoord::from_lat_lon(0.0, 0.0),
1441 GeoCoord::from_lat_lon(0.0, 1.0),
1442 ];
1443 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1444 assert_eq!(r.positions.len(), 4);
1446 assert_eq!(r.normals.len(), 4);
1447 assert_eq!(r.distances.len(), 4);
1448 assert!(!r.indices.is_empty());
1449 let max_idx = r.positions.len() as u32;
1451 assert!(r.indices.iter().all(|&i| i < max_idx));
1452 }
1453
1454 #[test]
1455 fn styled_stroke_square_cap_extends_beyond_endpoints() {
1456 let coords = vec![
1457 GeoCoord::from_lat_lon(0.0, 0.0),
1458 GeoCoord::from_lat_lon(0.0, 1.0),
1459 ];
1460 let hw = 0.1;
1461 let r = stroke_line_styled(&coords, hw, LineCap::Square, LineJoin::Miter, 2.0);
1462 let min_lon: f64 = r.positions.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
1464 assert!(min_lon < -0.05, "square cap should extend before the first vertex, got {min_lon}");
1465 }
1466
1467 #[test]
1468 fn styled_stroke_round_cap_produces_more_vertices() {
1469 let coords = vec![
1470 GeoCoord::from_lat_lon(0.0, 0.0),
1471 GeoCoord::from_lat_lon(0.0, 1.0),
1472 ];
1473 let butt = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1474 let round = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Miter, 2.0);
1475 assert!(
1476 round.positions.len() > butt.positions.len(),
1477 "round cap should produce more vertices than butt: {} vs {}",
1478 round.positions.len(), butt.positions.len(),
1479 );
1480 }
1481
1482 #[test]
1483 fn styled_stroke_distances_are_monotonic() {
1484 let coords = vec![
1485 GeoCoord::from_lat_lon(0.0, 0.0),
1486 GeoCoord::from_lat_lon(0.0, 1.0),
1487 GeoCoord::from_lat_lon(0.0, 2.0),
1488 ];
1489 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1490 assert!(r.distances.iter().all(|&d| d >= 0.0));
1492 let max_dist = r.distances.iter().cloned().fold(0.0_f64, f64::max);
1493 assert!(max_dist > 0.0, "max distance should be positive");
1494 }
1495
1496 #[test]
1497 fn styled_stroke_normals_are_unit_length_or_zero() {
1498 let coords = vec![
1499 GeoCoord::from_lat_lon(0.0, 0.0),
1500 GeoCoord::from_lat_lon(0.0, 1.0),
1501 GeoCoord::from_lat_lon(1.0, 1.0),
1502 ];
1503 let r = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Round, 2.0);
1504 for (i, nrm) in r.normals.iter().enumerate() {
1505 let len = (nrm[0] * nrm[0] + nrm[1] * nrm[1]).sqrt();
1506 if r.cap_join[i] > 0.5 {
1507 assert!(
1509 len < CIRCUMSCRIBE + 1e-10 || len < 1e-10,
1510 "cap/join normal length should be ≤{CIRCUMSCRIBE} or ~0, got {len}"
1511 );
1512 } else {
1513 assert!(
1515 len < 1.0 + 1e-10 || len < 1e-10,
1516 "body normal length should be ≤1 or ~0, got {len}"
1517 );
1518 }
1519 }
1520 }
1521
1522 #[test]
1523 fn styled_stroke_cap_join_flags_correct_for_round_caps() {
1524 let coords = vec![
1525 GeoCoord::from_lat_lon(0.0, 0.0),
1526 GeoCoord::from_lat_lon(0.0, 1.0),
1527 ];
1528 let r = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Round, 2.0);
1529 assert_eq!(r.cap_join.len(), r.positions.len());
1530 let cap_join_count = r.cap_join.iter().filter(|&&f| f > 0.5).count();
1532 assert!(cap_join_count > 0, "round cap should flag some vertices");
1533 let body_count = r.cap_join.iter().filter(|&&f| f < 0.5).count();
1535 assert!(body_count > 0, "ribbon body vertices should exist");
1536 }
1537
1538 #[test]
1539 fn styled_stroke_cap_join_flags_zero_for_butt_caps() {
1540 let coords = vec![
1541 GeoCoord::from_lat_lon(0.0, 0.0),
1542 GeoCoord::from_lat_lon(0.0, 1.0),
1543 ];
1544 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Bevel, 2.0);
1545 assert_eq!(r.cap_join.len(), r.positions.len());
1546 for &flag in &r.cap_join {
1548 assert!(flag < 0.5, "butt cap + bevel join should have no cap_join flags");
1549 }
1550 }
1551
1552 #[test]
1553 fn styled_stroke_circumscribed_fan_extends_beyond_unit() {
1554 let coords = vec![
1557 GeoCoord::from_lat_lon(0.0, 0.0),
1558 GeoCoord::from_lat_lon(0.0, 1.0),
1559 ];
1560 let r = stroke_line_styled(&coords, 0.01, LineCap::Round, LineJoin::Round, 2.0);
1561 let mut found_circumscribed = false;
1562 for (i, nrm) in r.normals.iter().enumerate() {
1563 if r.cap_join[i] > 0.5 {
1564 let len = (nrm[0] * nrm[0] + nrm[1] * nrm[1]).sqrt();
1565 if len > 1.0 + 1e-10 {
1566 found_circumscribed = true;
1567 assert!(
1568 (len - CIRCUMSCRIBE).abs() < 1e-6,
1569 "circumscribed normal should be ~{CIRCUMSCRIBE}, got {len}"
1570 );
1571 }
1572 }
1573 }
1574 assert!(found_circumscribed, "should find circumscribed fan normals with length > 1");
1575 }
1576
1577 #[test]
1578 fn styled_stroke_bevel_join_at_sharp_angle() {
1579 let coords = vec![
1581 GeoCoord::from_lat_lon(0.0, 0.0),
1582 GeoCoord::from_lat_lon(0.0, 1.0),
1583 GeoCoord::from_lat_lon(1.0, 1.0),
1584 ];
1585 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Bevel, 2.0);
1586 let straight = stroke_line_styled(
1588 &[GeoCoord::from_lat_lon(0.0, 0.0), GeoCoord::from_lat_lon(0.0, 2.0)],
1589 0.01, LineCap::Butt, LineJoin::Bevel, 2.0,
1590 );
1591 assert!(
1592 r.positions.len() > straight.positions.len(),
1593 "bevel join should add extra vertices: {} vs {}",
1594 r.positions.len(), straight.positions.len(),
1595 );
1596 }
1597
1598 #[test]
1599 fn styled_stroke_round_join_produces_fan_vertices() {
1600 let coords = vec![
1601 GeoCoord::from_lat_lon(0.0, 0.0),
1602 GeoCoord::from_lat_lon(0.0, 1.0),
1603 GeoCoord::from_lat_lon(1.0, 1.0),
1604 ];
1605 let bevel = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Bevel, 2.0);
1606 let round = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Round, 2.0);
1607 assert!(
1608 round.positions.len() > bevel.positions.len(),
1609 "round join should produce more vertices than bevel: {} vs {}",
1610 round.positions.len(), bevel.positions.len(),
1611 );
1612 }
1613
1614 #[test]
1615 fn styled_stroke_miter_join_collinear() {
1616 let coords = vec![
1618 GeoCoord::from_lat_lon(0.0, 0.0),
1619 GeoCoord::from_lat_lon(0.0, 1.0),
1620 GeoCoord::from_lat_lon(0.0, 2.0),
1621 ];
1622 let r = stroke_line_styled(&coords, 0.01, LineCap::Butt, LineJoin::Miter, 2.0);
1623 let max_idx = r.positions.len() as u32;
1625 assert!(r.indices.iter().all(|&i| i < max_idx));
1626 assert!(!r.indices.is_empty());
1627 }
1628
1629 #[test]
1630 fn styled_stroke_indices_valid_for_complex_line() {
1631 let coords: Vec<GeoCoord> = (0..10)
1633 .map(|i| {
1634 let lat = if i % 2 == 0 { 0.0 } else { 1.0 };
1635 GeoCoord::from_lat_lon(lat, i as f64)
1636 })
1637 .collect();
1638 for cap in [LineCap::Butt, LineCap::Round, LineCap::Square] {
1639 for join in [LineJoin::Miter, LineJoin::Bevel, LineJoin::Round] {
1640 let r = stroke_line_styled(&coords, 0.01, cap, join, 2.0);
1641 let max_idx = r.positions.len() as u32;
1642 assert!(
1643 r.indices.iter().all(|&i| i < max_idx),
1644 "invalid index for cap={cap:?} join={join:?}: max valid={max_idx}",
1645 );
1646 assert_eq!(r.positions.len(), r.normals.len());
1647 assert_eq!(r.positions.len(), r.distances.len());
1648 }
1649 }
1650 }
1651}