Skip to main content

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}