symbios_tensor/graph.rs
1//! Arena-based road graph with soft-deletion support for edge splitting.
2//!
3//! The graph stores nodes, edges, and extracted city blocks in flat `Vec`s
4//! indexed by [`NodeId`], [`EdgeId`], and [`BlockId`] (all `u32`). Edges
5//! carry an `active` flag so that [`RoadGraph::split_edge`] can deactivate
6//! the original while inserting two replacement segments without invalidating
7//! existing indices.
8
9use glam::Vec2;
10use serde::{Deserialize, Serialize};
11
12/// Index into [`RoadGraph::nodes`].
13pub type NodeId = u32;
14/// Index into [`RoadGraph::edges`].
15pub type EdgeId = u32;
16/// Index into [`RoadGraph::blocks`].
17pub type BlockId = u32;
18
19/// Classification of a road segment.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum RoadType {
22 /// Contour-following avenue.
23 Major,
24 /// Gradient-following street.
25 Minor,
26}
27
28/// A road intersection or endpoint.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct RoadNode {
31 /// World-space position (X, Z in a Y-up coordinate system).
32 pub position: Vec2,
33 /// World-space Y elevation (height above sea level).
34 /// Set from the heightmap during tracing, then smoothed by rationalization.
35 pub elevation: f32,
36 /// Indices of all edges incident to this node (both active and inactive).
37 pub edges: Vec<EdgeId>,
38}
39
40/// An undirected road segment connecting two nodes.
41///
42/// Although stored with [`start`](Self::start) and [`end`](Self::end) fields,
43/// edges are traversed bidirectionally — both endpoints list the edge in their
44/// adjacency lists.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct RoadEdge {
47 /// Source node index.
48 pub start: NodeId,
49 /// Destination node index.
50 pub end: NodeId,
51 /// Whether this is a major (contour) or minor (gradient) road.
52 pub road_type: RoadType,
53 /// `false` after the edge has been split — superseded by two child edges.
54 pub active: bool,
55}
56
57/// An enclosed city block bounded by road edges.
58///
59/// The perimeter is a closed polygon of node indices extracted by
60/// [`crate::polygons::extract_blocks`] using the left-most-turn algorithm.
61/// Blocks always wind clockwise (negative signed area).
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CityBlock {
64 /// Ordered list of node indices forming a closed polygon perimeter.
65 pub perimeter: Vec<NodeId>,
66}
67
68/// The complete road network: nodes, edges, and extracted city blocks.
69///
70/// All public fields are arena `Vec`s — indices ([`NodeId`], [`EdgeId`],
71/// [`BlockId`]) are stable for the lifetime of the graph.
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73pub struct RoadGraph {
74 /// Road intersections and endpoints.
75 pub nodes: Vec<RoadNode>,
76 /// Road segments (check [`RoadEdge::active`] before traversal).
77 pub edges: Vec<RoadEdge>,
78 /// Enclosed city blocks extracted by [`crate::polygons::extract_blocks`].
79 pub blocks: Vec<CityBlock>,
80}
81
82impl RoadGraph {
83 /// Inserts a new node and returns its [`NodeId`].
84 pub fn add_node(&mut self, position: Vec2) -> NodeId {
85 let id = self.nodes.len() as NodeId;
86 self.nodes.push(RoadNode {
87 position,
88 elevation: 0.0,
89 edges: Vec::new(),
90 });
91 id
92 }
93
94 /// Inserts a new node with an explicit elevation and returns its [`NodeId`].
95 pub fn add_node_with_elevation(&mut self, position: Vec2, elevation: f32) -> NodeId {
96 let id = self.nodes.len() as NodeId;
97 self.nodes.push(RoadNode {
98 position,
99 elevation,
100 edges: Vec::new(),
101 });
102 id
103 }
104
105 /// Inserts a new active edge between two nodes and returns its [`EdgeId`].
106 pub fn add_edge(&mut self, start: NodeId, end: NodeId, road_type: RoadType) -> EdgeId {
107 let id = self.edges.len() as EdgeId;
108 self.edges.push(RoadEdge {
109 start,
110 end,
111 road_type,
112 active: true,
113 });
114 self.nodes[start as usize].edges.push(id);
115 self.nodes[end as usize].edges.push(id);
116 id
117 }
118
119 /// Deactivates an edge and splits it at `split_pos`, returning `(new_node, edge_a, edge_b)`.
120 ///
121 /// The original edge is marked inactive. Two new edges are created connecting
122 /// the original endpoints through the new split node.
123 pub fn split_edge(&mut self, edge_id: EdgeId, split_pos: Vec2) -> (NodeId, EdgeId, EdgeId) {
124 let edge = &self.edges[edge_id as usize];
125 let start = edge.start;
126 let end = edge.end;
127 let road_type = edge.road_type;
128
129 self.edges[edge_id as usize].active = false;
130
131 // Remove the now-inactive edge from its endpoint adjacency lists
132 // to prevent unbounded accumulation of stale IDs.
133 self.nodes[start as usize].edges.retain(|&e| e != edge_id);
134 self.nodes[end as usize].edges.retain(|&e| e != edge_id);
135
136 // Interpolate elevation from the edge endpoints based on position.
137 let start_pos = self.nodes[start as usize].position;
138 let end_pos = self.nodes[end as usize].position;
139 let seg_len = (end_pos - start_pos).length();
140 let t = if seg_len > 1e-6 {
141 ((split_pos - start_pos).length() / seg_len).clamp(0.0, 1.0)
142 } else {
143 0.5
144 };
145 let start_elev = self.nodes[start as usize].elevation;
146 let end_elev = self.nodes[end as usize].elevation;
147 let mid_elev = start_elev + t * (end_elev - start_elev);
148
149 let mid = self.add_node_with_elevation(split_pos, mid_elev);
150 let ea = self.add_edge(start, mid, road_type);
151 let eb = self.add_edge(mid, end, road_type);
152
153 (mid, ea, eb)
154 }
155
156 /// Returns the other endpoint of an edge relative to `node_id`.
157 pub fn opposite(&self, edge_id: EdgeId, node_id: NodeId) -> NodeId {
158 let edge = &self.edges[edge_id as usize];
159 if edge.start == node_id {
160 edge.end
161 } else {
162 edge.start
163 }
164 }
165
166 /// Returns position of a node by id.
167 pub fn node_pos(&self, id: NodeId) -> Vec2 {
168 self.nodes[id as usize].position
169 }
170}