1use std::collections::HashMap;
18
19use glam::{Vec2, Vec3};
20use symbios_ground::HeightMap;
21
22use crate::graph::{EdgeId, NodeId, RoadGraph, RoadType};
23use crate::topology;
24
25#[derive(Debug, Clone, Default)]
30pub struct ProceduralMesh {
31 pub vertices: Vec<[f32; 3]>,
33 pub normals: Vec<[f32; 3]>,
35 pub uvs: Vec<[f32; 2]>,
37 pub indices: Vec<u32>,
39}
40
41impl ProceduralMesh {
42 pub fn append(&mut self, other: &ProceduralMesh) {
44 let base = self.vertices.len() as u32;
45 self.vertices.extend_from_slice(&other.vertices);
46 self.normals.extend_from_slice(&other.normals);
47 self.uvs.extend_from_slice(&other.uvs);
48 self.indices.extend(other.indices.iter().map(|i| i + base));
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct RoadMeshConfig {
55 pub major_half_width: f32,
57 pub minor_half_width: f32,
59 pub hub_sides: u32,
61 pub depth_bias: f32,
64 pub texture_scale: f32,
66 pub spline_subdivisions: u32,
69 pub curb_radius: f32,
72 pub skirt: SkirtConfig,
74}
75
76impl Default for RoadMeshConfig {
77 fn default() -> Self {
78 Self {
79 major_half_width: 3.0,
80 minor_half_width: 2.0,
81 hub_sides: 8,
82 depth_bias: 0.05,
83 texture_scale: 0.1,
84 spline_subdivisions: 8,
85 curb_radius: 2.0,
86 skirt: SkirtConfig::default(),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Default)]
93pub struct RoadMeshes {
94 pub hubs: ProceduralMesh,
96 pub ribbons: ProceduralMesh,
98 pub skirts: ProceduralMesh,
100}
101
102#[derive(Debug, Clone)]
104pub struct SkirtConfig {
105 pub width: f32,
107 pub bury_depth: f32,
109}
110
111impl Default for SkirtConfig {
112 fn default() -> Self {
113 Self {
114 width: 3.0,
115 bury_depth: 0.5,
116 }
117 }
118}
119
120pub fn generate_road_meshes(
126 graph: &RoadGraph,
127 heightmap: &HeightMap,
128 config: &RoadMeshConfig,
129) -> RoadMeshes {
130 let mut meshes = RoadMeshes::default();
131
132 let degrees = compute_active_degrees(graph);
133 let truncations = compute_truncations(graph, °rees, config);
134
135 for (node_id, °) in degrees.iter().enumerate() {
137 if deg == 0 || deg == 2 {
138 continue;
139 }
140 let nid = node_id as NodeId;
141 if deg == 1 {
142 let (hub, hub_skirt) = generate_hub_cap(graph, nid, heightmap, config);
143 meshes.hubs.append(&hub);
144 meshes.skirts.append(&hub_skirt);
145 } else {
146 let (hub, hub_skirt) =
147 generate_hub_procedural(graph, nid, &truncations, heightmap, config);
148 meshes.hubs.append(&hub);
149 meshes.skirts.append(&hub_skirt);
150 }
151 }
152
153 let chains = extract_chains(graph, °rees);
155 for chain in &chains {
156 let (ribbon, skirt) = generate_ribbon(graph, chain, &truncations, heightmap, config);
157 meshes.ribbons.append(&ribbon);
158 meshes.skirts.append(&skirt);
159 }
160
161 meshes
162}
163
164fn compute_active_degrees(graph: &RoadGraph) -> Vec<u32> {
169 topology::compute_active_degrees(graph)
170}
171
172fn compute_truncations(
185 graph: &RoadGraph,
186 degrees: &[u32],
187 config: &RoadMeshConfig,
188) -> HashMap<(NodeId, EdgeId), f32> {
189 let mut truncations = HashMap::new();
190 let skirt_w = config.skirt.width;
191
192 for (node_idx, °) in degrees.iter().enumerate() {
193 if deg == 0 || deg == 2 {
194 continue;
195 }
196
197 let nid = node_idx as NodeId;
198 let center = graph.node_pos(nid);
199 let node = &graph.nodes[node_idx];
200
201 let mut arms: Vec<(EdgeId, Vec2, Vec2, f32)> = Vec::new(); for &eid in &node.edges {
204 let edge = &graph.edges[eid as usize];
205 if !edge.active {
206 continue;
207 }
208 let neighbor = graph.opposite(eid, nid);
209 let dir = (graph.node_pos(neighbor) - center).normalize_or_zero();
210 if dir.length_squared() < 1e-12 {
211 continue;
212 }
213 let right = Vec2::new(-dir.y, dir.x);
214 let hw = match edge.road_type {
215 RoadType::Major => config.major_half_width,
216 RoadType::Minor => config.minor_half_width,
217 };
218 arms.push((eid, dir, right, hw));
219 }
220
221 if arms.is_empty() {
222 continue;
223 }
224
225 if deg == 1 {
227 let (eid, _, _, hw) = arms[0];
228 truncations.insert((nid, eid), hw + config.curb_radius);
229 continue;
230 }
231
232 arms.sort_by(|a, b| {
235 let angle_a = (-a.1.y).atan2(a.1.x);
236 let angle_b = (-b.1.y).atan2(b.1.x);
237 angle_a
238 .partial_cmp(&angle_b)
239 .unwrap_or(std::cmp::Ordering::Equal)
240 });
241
242 let n = arms.len();
243 let mut trunc: Vec<f32> = arms.iter().map(|a| a.3).collect();
245
246 for i in 0..n {
248 let j = (i + 1) % n;
249
250 let (_eid_a, dir_a, right_a, hw_a) = arms[i];
251 let (_eid_b, dir_b, right_b, hw_b) = arms[j];
252
253 let w_a = hw_a + skirt_w;
255 let w_b = hw_b + skirt_w;
256
257 let rhs = right_a * w_a + right_b * w_b;
267 let det = dir_a.x * (-dir_b.y) - (-dir_b.x) * dir_a.y;
268
269 if det.abs() < 1e-6 {
270 let fallback = (w_a + w_b) * 0.5;
272 trunc[i] = trunc[i].max(fallback);
273 trunc[j] = trunc[j].max(fallback);
274 continue;
275 }
276
277 let t_a = (rhs.x * (-dir_b.y) - (-dir_b.x) * rhs.y) / det;
278 let t_b = (dir_a.x * rhs.y - dir_a.y * rhs.x) / det;
279
280 if t_a > 0.0 {
282 trunc[i] = trunc[i].max(t_a);
283 }
284 if t_b > 0.0 {
285 trunc[j] = trunc[j].max(t_b);
286 }
287 }
288
289 for (idx, &(eid, _, _, _)) in arms.iter().enumerate() {
291 truncations.insert((nid, eid), trunc[idx]);
292 }
293 }
294
295 truncations
296}
297
298fn generate_hub_cap(
303 graph: &RoadGraph,
304 node_id: NodeId,
305 heightmap: &HeightMap,
306 config: &RoadMeshConfig,
307) -> (ProceduralMesh, ProceduralMesh) {
308 let center = graph.node_pos(node_id);
309 let node = &graph.nodes[node_id as usize];
310
311 let mut radius = config.minor_half_width;
313 for &eid in &node.edges {
314 let edge = &graph.edges[eid as usize];
315 if !edge.active {
316 continue;
317 }
318 let hw = match edge.road_type {
319 RoadType::Major => config.major_half_width,
320 RoadType::Minor => config.minor_half_width,
321 };
322 if hw > radius {
323 radius = hw;
324 }
325 }
326 radius += config.curb_radius;
327
328 let sides = config.hub_sides.max(3);
329 let center_y = node.elevation + config.depth_bias;
330
331 let mut mesh = ProceduralMesh::default();
332
333 mesh.vertices.push([center.x, center_y, center.y]);
335 mesh.normals.push([0.0, 1.0, 0.0]);
336 mesh.uvs.push([
337 center.x * config.texture_scale,
338 center.y * config.texture_scale,
339 ]);
340
341 let angle_step = std::f32::consts::TAU / sides as f32;
343 let mut perimeter_pts = Vec::with_capacity(sides as usize);
344 for i in 0..sides {
345 let angle = angle_step * i as f32;
346 let (sin, cos) = angle.sin_cos();
347 let px = center.x + cos * radius;
348 let pz = center.y + sin * radius;
349
350 mesh.vertices.push([px, center_y, pz]);
351 mesh.normals.push([0.0, 1.0, 0.0]);
352 mesh.uvs
353 .push([px * config.texture_scale, pz * config.texture_scale]);
354 perimeter_pts.push((px, pz));
355 }
356
357 for i in 0..sides {
359 let a = 1 + i;
360 let b = 1 + (i + 1) % sides;
361 mesh.indices.push(0);
362 mesh.indices.push(b);
363 mesh.indices.push(a);
364 }
365
366 let skirt_w = config.skirt.width;
368 let bury = config.skirt.bury_depth;
369 let mut skirt = ProceduralMesh::default();
370
371 for &(px, pz) in &perimeter_pts {
372 let dx = px - center.x;
373 let dz = pz - center.y;
374 let len = (dx * dx + dz * dz).sqrt().max(1e-6);
375 let nx = dx / len;
376 let nz = dz / len;
377
378 let outer_x = px + nx * skirt_w;
379 let outer_z = pz + nz * skirt_w;
380 let outer_y = heightmap.get_height_at(outer_x, outer_z) - bury;
381
382 skirt.vertices.push([px, center_y, pz]);
383 skirt.normals.push([0.0, 1.0, 0.0]);
384 skirt
385 .uvs
386 .push([px * config.texture_scale, pz * config.texture_scale]);
387
388 skirt.vertices.push([outer_x, outer_y, outer_z]);
389 skirt.normals.push([0.0, 1.0, 0.0]);
390 skirt.uvs.push([
391 outer_x * config.texture_scale,
392 outer_z * config.texture_scale,
393 ]);
394 }
395
396 for i in 0..sides {
397 let next = (i + 1) % sides;
398 let i0 = i * 2;
399 let o0 = i * 2 + 1;
400 let i1 = next * 2;
401 let o1 = next * 2 + 1;
402
403 skirt.indices.push(i0);
404 skirt.indices.push(i1);
405 skirt.indices.push(o0);
406
407 skirt.indices.push(o0);
408 skirt.indices.push(i1);
409 skirt.indices.push(o1);
410 }
411
412 (mesh, skirt)
413}
414
415fn generate_hub_procedural(
422 graph: &RoadGraph,
423 node_id: NodeId,
424 truncations: &HashMap<(NodeId, EdgeId), f32>,
425 heightmap: &HeightMap,
426 config: &RoadMeshConfig,
427) -> (ProceduralMesh, ProceduralMesh) {
428 let center = graph.node_pos(node_id);
429 let node = &graph.nodes[node_id as usize];
430 let center_y = node.elevation + config.depth_bias;
431 let skirt_w = config.skirt.width;
432 let bury = config.skirt.bury_depth;
433
434 struct Arm {
436 dir: Vec2,
437 right: Vec2,
438 half_width: f32,
439 truncation: f32,
440 angle: f32,
441 }
442
443 let mut arms: Vec<Arm> = Vec::new();
444 for &eid in &node.edges {
445 let edge = &graph.edges[eid as usize];
446 if !edge.active {
447 continue;
448 }
449 let neighbor = graph.opposite(eid, node_id);
450 let dir = (graph.node_pos(neighbor) - center).normalize_or_zero();
451 if dir.length_squared() < 1e-12 {
452 continue;
453 }
454 let right = Vec2::new(-dir.y, dir.x);
455 let hw = match edge.road_type {
456 RoadType::Major => config.major_half_width,
457 RoadType::Minor => config.minor_half_width,
458 };
459 let trunc = truncations
460 .get(&(node_id, eid))
461 .copied()
462 .unwrap_or(hw + config.curb_radius);
463
464 arms.push(Arm {
465 dir,
466 right,
467 half_width: hw,
468 truncation: trunc,
469 angle: (-dir.y).atan2(dir.x),
470 });
471 }
472
473 if arms.is_empty() {
474 return (ProceduralMesh::default(), ProceduralMesh::default());
475 }
476
477 arms.sort_by(|a, b| {
479 a.angle
480 .partial_cmp(&b.angle)
481 .unwrap_or(std::cmp::Ordering::Equal)
482 });
483
484 let mut perimeter: Vec<Vec2> = Vec::with_capacity(arms.len() * 2);
488 for arm in &arms {
489 let right_corner = center + arm.dir * arm.truncation + arm.right * arm.half_width;
490 let left_corner = center + arm.dir * arm.truncation - arm.right * arm.half_width;
491 perimeter.push(right_corner);
492 perimeter.push(left_corner);
493 }
494
495 let mut mesh = ProceduralMesh::default();
497
498 mesh.vertices.push([center.x, center_y, center.y]);
500 mesh.normals.push([0.0, 1.0, 0.0]);
501 mesh.uvs.push([
502 center.x * config.texture_scale,
503 center.y * config.texture_scale,
504 ]);
505
506 for pt in &perimeter {
508 mesh.vertices.push([pt.x, center_y, pt.y]);
509 mesh.normals.push([0.0, 1.0, 0.0]);
510 mesh.uvs
511 .push([pt.x * config.texture_scale, pt.y * config.texture_scale]);
512 }
513
514 let peri_count = perimeter.len() as u32;
516 for i in 0..peri_count {
517 let a = 1 + i;
518 let b = 1 + (i + 1) % peri_count;
519 mesh.indices.push(0);
520 mesh.indices.push(a);
521 mesh.indices.push(b);
522 }
523
524 let mut skirt = ProceduralMesh::default();
526 let n_arms = arms.len();
527
528 for i in 0..n_arms {
529 let j = (i + 1) % n_arms;
530
531 let left_corner =
533 center + arms[i].dir * arms[i].truncation - arms[i].right * arms[i].half_width;
534 let right_corner =
535 center + arms[j].dir * arms[j].truncation + arms[j].right * arms[j].half_width;
536
537 let left_angle = arms[i].angle + std::f32::consts::FRAC_PI_2; let right_angle = arms[j].angle - std::f32::consts::FRAC_PI_2; let mut gap_angle = right_angle - left_angle;
543 if gap_angle < 0.0 {
544 gap_angle += std::f32::consts::TAU;
545 }
546 if gap_angle > std::f32::consts::TAU {
547 gap_angle -= std::f32::consts::TAU;
548 }
549
550 let subdivs = ((gap_angle / (std::f32::consts::FRAC_PI_4)).ceil() as u32).max(1);
552
553 let base_vert = skirt.vertices.len() as u32;
555
556 for s in 0..=subdivs {
557 let t = s as f32 / subdivs as f32;
558
559 let inner = left_corner.lerp(right_corner, t);
561
562 let out_dir = (inner - center).normalize_or_zero();
564 let outer = inner + out_dir * skirt_w;
565 let outer_y = heightmap.get_height_at(outer.x, outer.y) - bury;
566
567 skirt.vertices.push([inner.x, center_y, inner.y]);
568 skirt.normals.push([0.0, 1.0, 0.0]);
569 skirt.uvs.push([
570 inner.x * config.texture_scale,
571 inner.y * config.texture_scale,
572 ]);
573
574 skirt.vertices.push([outer.x, outer_y, outer.y]);
575 skirt.normals.push([0.0, 1.0, 0.0]);
576 skirt.uvs.push([
577 outer.x * config.texture_scale,
578 outer.y * config.texture_scale,
579 ]);
580 }
581
582 for s in 0..subdivs {
584 let i0 = base_vert + s * 2; let o0 = base_vert + s * 2 + 1; let i1 = base_vert + (s + 1) * 2;
587 let o1 = base_vert + (s + 1) * 2 + 1;
588
589 skirt.indices.push(i0);
590 skirt.indices.push(i1);
591 skirt.indices.push(o0);
592
593 skirt.indices.push(o0);
594 skirt.indices.push(i1);
595 skirt.indices.push(o1);
596 }
597 }
598
599 (mesh, skirt)
600}
601
602struct Chain {
608 nodes: Vec<NodeId>,
609 edges: Vec<EdgeId>,
610 road_type: RoadType,
611}
612
613fn extract_chains(graph: &RoadGraph, degrees: &[u32]) -> Vec<Chain> {
614 topology::extract_chains(graph, degrees)
615 .into_iter()
616 .map(|c| Chain {
617 nodes: c.nodes,
618 edges: c.edges,
619 road_type: c.road_type,
620 })
621 .collect()
622}
623
624fn generate_ribbon(
629 graph: &RoadGraph,
630 chain: &Chain,
631 truncations: &HashMap<(NodeId, EdgeId), f32>,
632 heightmap: &HeightMap,
633 config: &RoadMeshConfig,
634) -> (ProceduralMesh, ProceduralMesh) {
635 let half_width = match chain.road_type {
636 RoadType::Major => config.major_half_width,
637 RoadType::Minor => config.minor_half_width,
638 };
639
640 let smooth_pts: Vec<Vec2> = chain.nodes.iter().map(|&nid| graph.node_pos(nid)).collect();
641 let node_elevs: Vec<f32> = chain
642 .nodes
643 .iter()
644 .map(|&nid| graph.nodes[nid as usize].elevation)
645 .collect();
646 if smooth_pts.len() < 2 {
647 return (ProceduralMesh::default(), ProceduralMesh::default());
648 }
649
650 let first_node = chain.nodes[0];
654 let last_node = chain.nodes[chain.nodes.len() - 1];
655 let first_edge = chain.edges[0];
656 let last_edge = chain.edges[chain.edges.len() - 1];
657
658 let start_trim = truncations
659 .get(&(first_node, first_edge))
660 .copied()
661 .unwrap_or(0.0);
662 let end_trim = truncations
663 .get(&(last_node, last_edge))
664 .copied()
665 .unwrap_or(0.0);
666
667 let (truncated, truncated_elevs) =
668 truncate_polyline_with_elevations(&smooth_pts, &node_elevs, start_trim, end_trim);
669 if truncated.len() < 2 {
670 return (ProceduralMesh::default(), ProceduralMesh::default());
671 }
672
673 extrude_ribbon(&truncated, &truncated_elevs, half_width, heightmap, config)
674}
675
676fn truncate_polyline_with_elevations(
679 points: &[Vec2],
680 elevations: &[f32],
681 start_trim: f32,
682 end_trim: f32,
683) -> (Vec<Vec2>, Vec<f32>) {
684 if points.len() < 2 {
685 return (points.to_vec(), elevations.to_vec());
686 }
687
688 let mut arc_lengths = Vec::with_capacity(points.len());
689 arc_lengths.push(0.0f32);
690 for i in 1..points.len() {
691 let seg_len = (points[i] - points[i - 1]).length();
692 arc_lengths.push(arc_lengths[i - 1] + seg_len);
693 }
694 let total = arc_lengths[arc_lengths.len() - 1];
696
697 let max_trim = total * 0.98;
699 let (adj_start, adj_end) = if start_trim + end_trim > max_trim {
700 let scale = max_trim / (start_trim + end_trim);
701 (start_trim * scale, end_trim * scale)
702 } else {
703 (start_trim, end_trim)
704 };
705
706 let t_start = adj_start;
707 let t_end = total - adj_end;
708 if t_start >= t_end {
709 return (Vec::new(), Vec::new());
710 }
711
712 let mut result_pts = Vec::new();
713 let mut result_elevs = Vec::new();
714
715 result_pts.push(point_at_arc_length(points, &arc_lengths, t_start));
716 result_elevs.push(elevation_at_arc_length(elevations, &arc_lengths, t_start));
717
718 for i in 1..points.len() - 1 {
719 if arc_lengths[i] > t_start && arc_lengths[i] < t_end {
720 result_pts.push(points[i]);
721 result_elevs.push(elevations[i]);
722 }
723 }
724
725 result_pts.push(point_at_arc_length(points, &arc_lengths, t_end));
726 result_elevs.push(elevation_at_arc_length(elevations, &arc_lengths, t_end));
727
728 (result_pts, result_elevs)
729}
730
731fn elevation_at_arc_length(elevations: &[f32], arc_lengths: &[f32], target: f32) -> f32 {
736 for i in 1..elevations.len() {
737 if arc_lengths[i] >= target {
738 let seg_len = arc_lengths[i] - arc_lengths[i - 1];
739 if seg_len < 1e-6 {
740 return elevations[i];
741 }
742 let t = (target - arc_lengths[i - 1]) / seg_len;
743 return elevations[i - 1] + t * (elevations[i] - elevations[i - 1]);
744 }
745 }
746 elevations.last().copied().unwrap_or(0.0)
747}
748
749fn point_at_arc_length(points: &[Vec2], arc_lengths: &[f32], target: f32) -> Vec2 {
753 for i in 1..points.len() {
754 if arc_lengths[i] >= target {
755 let seg_len = arc_lengths[i] - arc_lengths[i - 1];
756 if seg_len < 1e-6 {
757 return points[i];
758 }
759 let t = (target - arc_lengths[i - 1]) / seg_len;
760 return points[i - 1].lerp(points[i], t);
761 }
762 }
763 points.last().copied().unwrap_or(Vec2::ZERO)
764}
765
766fn extrude_ribbon(
769 points: &[Vec2],
770 elevations: &[f32],
771 half_width: f32,
772 heightmap: &HeightMap,
773 config: &RoadMeshConfig,
774) -> (ProceduralMesh, ProceduralMesh) {
775 let n = points.len();
776 let skirt_w = config.skirt.width;
777 let bury = config.skirt.bury_depth;
778
779 let mut asphalt = ProceduralMesh {
780 vertices: Vec::with_capacity(n * 2),
781 normals: Vec::with_capacity(n * 2),
782 uvs: Vec::with_capacity(n * 2),
783 indices: Vec::with_capacity((n - 1) * 6),
784 };
785
786 let mut skirts = ProceduralMesh {
787 vertices: Vec::with_capacity(n * 4),
788 normals: Vec::with_capacity(n * 4),
789 uvs: Vec::with_capacity(n * 4),
790 indices: Vec::with_capacity((n - 1) * 12),
791 };
792
793 let mut accum_dist = 0.0f32;
794
795 for i in 0..n {
796 let tangent = if i == 0 {
797 (points[1] - points[0]).normalize_or_zero()
798 } else if i == n - 1 {
799 (points[n - 1] - points[n - 2]).normalize_or_zero()
800 } else {
801 (points[i + 1] - points[i - 1]).normalize_or_zero()
802 };
803
804 let right = Vec2::new(-tangent.y, tangent.x);
805
806 let left_pt = points[i] - right * half_width;
807 let right_pt = points[i] + right * half_width;
808
809 let center_y = elevations[i] + config.depth_bias;
810
811 let elev_delta = if i == 0 {
812 elevations[1] - elevations[0]
813 } else if i == n - 1 {
814 elevations[n - 1] - elevations[n - 2]
815 } else {
816 elevations[i + 1] - elevations[i - 1]
817 };
818 let forward_3d = Vec3::new(tangent.x, elev_delta, tangent.y).normalize_or_zero();
819 let right_3d = Vec3::new(right.x, 0.0, right.y);
820 let normal = right_3d.cross(forward_3d).normalize_or_zero();
821 let normal = if normal.y < 0.0 { -normal } else { normal };
822 let norm_arr = [normal.x, normal.y, normal.z];
823
824 asphalt.vertices.push([right_pt.x, center_y, right_pt.y]);
825 asphalt.vertices.push([left_pt.x, center_y, left_pt.y]);
826 asphalt.normals.push(norm_arr);
827 asphalt.normals.push(norm_arr);
828
829 if i > 0 {
830 accum_dist += (points[i] - points[i - 1]).length();
831 }
832 let u = accum_dist * config.texture_scale;
833 asphalt.uvs.push([u, 0.0]);
834 asphalt.uvs.push([u, 1.0]);
835
836 let right_outer_pt = points[i] + right * (half_width + skirt_w);
837 let left_outer_pt = points[i] - right * (half_width + skirt_w);
838
839 let right_outer_y = heightmap.get_height_at(right_outer_pt.x, right_outer_pt.y) - bury;
840 let left_outer_y = heightmap.get_height_at(left_outer_pt.x, left_outer_pt.y) - bury;
841
842 skirts.vertices.push([right_pt.x, center_y, right_pt.y]);
843 skirts
844 .vertices
845 .push([right_outer_pt.x, right_outer_y, right_outer_pt.y]);
846 skirts.vertices.push([left_pt.x, center_y, left_pt.y]);
847 skirts
848 .vertices
849 .push([left_outer_pt.x, left_outer_y, left_outer_pt.y]);
850
851 skirts.normals.push([0.0, 1.0, 0.0]);
852 skirts.normals.push([0.0, 1.0, 0.0]);
853 skirts.normals.push([0.0, 1.0, 0.0]);
854 skirts.normals.push([0.0, 1.0, 0.0]);
855
856 let skirt_u = u;
857 skirts.uvs.push([skirt_u, 0.0]);
858 skirts.uvs.push([skirt_u, 1.0]);
859 skirts.uvs.push([skirt_u, 0.0]);
860 skirts.uvs.push([skirt_u, 1.0]);
861 }
862
863 for i in 0..n as u32 - 1 {
865 let bl = i * 2;
866 let br = i * 2 + 1;
867 let tl = (i + 1) * 2;
868 let tr = (i + 1) * 2 + 1;
869
870 asphalt.indices.push(bl);
871 asphalt.indices.push(tl);
872 asphalt.indices.push(br);
873
874 asphalt.indices.push(br);
875 asphalt.indices.push(tl);
876 asphalt.indices.push(tr);
877 }
878
879 for i in 0..n as u32 - 1 {
881 let base = i * 4;
882 let next = (i + 1) * 4;
883
884 let ri0 = base;
885 let ro0 = base + 1;
886 let ri1 = next;
887 let ro1 = next + 1;
888
889 skirts.indices.push(ri0);
890 skirts.indices.push(ro0);
891 skirts.indices.push(ri1);
892 skirts.indices.push(ro0);
893 skirts.indices.push(ro1);
894 skirts.indices.push(ri1);
895
896 let li0 = base + 2;
897 let lo0 = base + 3;
898 let li1 = next + 2;
899 let lo1 = next + 3;
900
901 skirts.indices.push(li0);
902 skirts.indices.push(li1);
903 skirts.indices.push(lo0);
904 skirts.indices.push(lo0);
905 skirts.indices.push(li1);
906 skirts.indices.push(lo1);
907 }
908
909 (asphalt, skirts)
910}
911
912#[cfg(test)]
917mod tests {
918 use super::*;
919 use crate::graph::RoadGraph;
920
921 fn cross_graph() -> RoadGraph {
923 let mut g = RoadGraph::default();
924 let c = g.add_node(Vec2::new(50.0, 50.0));
926 let n = g.add_node(Vec2::new(50.0, 20.0));
928 let s = g.add_node(Vec2::new(50.0, 80.0));
929 let e = g.add_node(Vec2::new(80.0, 50.0));
930 let w = g.add_node(Vec2::new(20.0, 50.0));
931
932 g.add_edge(c, n, RoadType::Major);
933 g.add_edge(c, s, RoadType::Major);
934 g.add_edge(c, e, RoadType::Minor);
935 g.add_edge(c, w, RoadType::Minor);
936
937 g
938 }
939
940 fn flat_heightmap() -> HeightMap {
941 HeightMap::new(64, 64, 2.0)
942 }
943
944 #[test]
945 fn hub_cap_mesh_has_correct_vertex_count() {
946 let mut g = RoadGraph::default();
948 let a = g.add_node(Vec2::new(50.0, 50.0));
949 let b = g.add_node(Vec2::new(80.0, 50.0));
950 g.add_edge(a, b, RoadType::Major);
951
952 let hm = flat_heightmap();
953 let config = RoadMeshConfig::default();
954
955 let (hub, hub_skirt) = generate_hub_cap(&g, a, &hm, &config);
957 let sides = config.hub_sides;
958 assert_eq!(hub.vertices.len(), (1 + sides) as usize);
959 assert_eq!(hub.indices.len(), (sides * 3) as usize);
960 assert_eq!(hub_skirt.vertices.len(), (sides * 2) as usize);
961 assert_eq!(hub_skirt.indices.len(), (sides * 6) as usize);
962 }
963
964 #[test]
965 fn procedural_hub_has_correct_vertex_count() {
966 let g = cross_graph();
967 let hm = flat_heightmap();
968 let config = RoadMeshConfig::default();
969 let degrees = compute_active_degrees(&g);
970 let truncations = compute_truncations(&g, °rees, &config);
971
972 let (hub, _hub_skirt) = generate_hub_procedural(&g, 0, &truncations, &hm, &config);
974 assert_eq!(hub.vertices.len(), 1 + 4 * 2);
976 assert_eq!(hub.indices.len(), 8 * 3);
978 }
979
980 #[test]
981 fn truncations_computed_for_all_edges() {
982 let g = cross_graph();
983 let degrees = compute_active_degrees(&g);
984 let config = RoadMeshConfig::default();
985 let truncations = compute_truncations(&g, °rees, &config);
986
987 for eid in 0..4u32 {
989 assert!(
990 truncations.contains_key(&(0, eid)),
991 "truncation missing for (0, {eid})"
992 );
993 let t = truncations[&(0, eid)];
994 assert!(t > 0.0, "truncation should be positive, got {t}");
995 }
996
997 for nid in 1..5u32 {
999 let node = &g.nodes[nid as usize];
1000 for &eid in &node.edges {
1001 assert!(
1002 truncations.contains_key(&(nid, eid)),
1003 "truncation missing for ({nid}, {eid})"
1004 );
1005 }
1006 }
1007 }
1008
1009 #[test]
1010 fn ribbon_mesh_nonempty() {
1011 let mut g = RoadGraph::default();
1012 let a = g.add_node(Vec2::new(10.0, 10.0));
1013 let b = g.add_node(Vec2::new(90.0, 10.0));
1014 g.add_edge(a, b, RoadType::Major);
1015
1016 let hm = flat_heightmap();
1017 let config = RoadMeshConfig::default();
1018 let meshes = generate_road_meshes(&g, &hm, &config);
1019
1020 assert!(!meshes.hubs.vertices.is_empty());
1021 assert!(!meshes.ribbons.vertices.is_empty());
1022 assert_eq!(meshes.ribbons.indices.len() % 3, 0);
1023 }
1024
1025 #[test]
1026 fn truncate_polyline_shortens() {
1027 let pts = vec![
1028 Vec2::new(0.0, 0.0),
1029 Vec2::new(10.0, 0.0),
1030 Vec2::new(20.0, 0.0),
1031 ];
1032 let elevs = vec![0.0, 5.0, 10.0];
1033 let (truncated, trunc_elevs) = truncate_polyline_with_elevations(&pts, &elevs, 3.0, 3.0);
1034 assert!(!truncated.is_empty());
1035 assert!((truncated[0].x - 3.0).abs() < 1e-4);
1036 assert!((truncated.last().unwrap().x - 17.0).abs() < 1e-4);
1037 assert!((trunc_elevs[0] - 1.5).abs() < 1e-4);
1038 assert!((*trunc_elevs.last().unwrap() - 8.5).abs() < 1e-4);
1039 }
1040
1041 #[test]
1042 fn full_pipeline_cross_graph() {
1043 let g = cross_graph();
1044 let hm = flat_heightmap();
1045 let config = RoadMeshConfig::default();
1046
1047 let meshes = generate_road_meshes(&g, &hm, &config);
1048
1049 let degrees = compute_active_degrees(&g);
1050 let hub_count = degrees.iter().filter(|&&d| d > 0 && d != 2).count();
1051 assert_eq!(hub_count, 5);
1052
1053 assert!(!meshes.hubs.vertices.is_empty());
1054 assert!(!meshes.ribbons.vertices.is_empty());
1055 }
1056}