载入中...
搜索中...
未找到
RoadNetwork.cpp
浏览该文件的文档.
2
4
5#include "common/Diagnostic.h"
6
7#include <algorithm>
8#include <cmath>
9#include <limits>
10#include <string>
11#include <utility>
12
13namespace eve::procgen::road {
14namespace {
15
16bool finite3(float x, float y, float z) { return std::isfinite(x) && std::isfinite(y) && std::isfinite(z); }
17
18bool hasUsableGeometry(const std::vector<RoadControlPoint>& points) {
19 constexpr float minimumSegmentLengthSquared = 1e-8f;
20 for (std::size_t i = 1; i < points.size(); ++i) {
21 const float dx = points[i].x - points[i - 1].x;
22 const float dy = points[i].y - points[i - 1].y;
23 const float dz = points[i].z - points[i - 1].z;
24 if (dx * dx + dy * dy + dz * dz > minimumSegmentLengthSquared) return true;
25 }
26 return false;
27}
28
29Result<SplinePath> makeCenterlineSpline(const RoadEdge& edge) {
30 SplinePath path;
31 auto kind = path.setKindResult("catmullRom");
32 if (!kind.ok()) return Result<SplinePath>::failure(kind.status());
33 path.setClosed(false);
34 for (const auto& point : edge.controlPoints) {
35 SplinePoint splinePoint;
36 splinePoint.x = point.x;
37 splinePoint.y = point.y;
38 splinePoint.z = point.z;
39 auto added = path.addPointResult(splinePoint);
40 if (!added.ok()) return Result<SplinePath>::failure(added.status());
41 }
42 return Result<SplinePath>::success(std::move(path));
43}
44
45RoadControlPoint P(float x, float y, float z) { return RoadControlPoint{x, y, z}; }
46
47int laneCount(const RoadEdge& edge, RoadLaneDirection direction) {
49}
50
51std::uint32_t laneEntryNode(const RoadEdge& edge, RoadLaneDirection direction) {
53}
54
55std::uint32_t laneExitNode(const RoadEdge& edge, RoadLaneDirection direction) {
57}
58
59bool sameLaneLink(const RoadLaneConnection& a, const RoadLaneConnection& b) {
60 return a.inEdge == b.inEdge && a.inLane == b.inLane && a.outEdge == b.outEdge && a.outLane == b.outLane &&
61 a.inDirection == b.inDirection && a.outDirection == b.outDirection;
62}
63
64RoadStyle groundStyle() {
65 RoadStyle style;
66 style.deckThickness = 0.35f;
67 style.pierClearance = 100.f;
68 style.curbHeight = 0.50f;
69 style.curbWidth = 0.42f;
70 return style;
71}
72
73RoadStyle bridgeStyle() {
74 RoadStyle style = groundStyle();
75 style.deckThickness = 0.75f;
76 style.pierClearance = 1.25f;
77 style.pierSpacing = 8.f;
78 style.pierWidth = 1.3f;
79 style.pierDepth = 1.3f;
80 style.curbHeight = 0.55f;
81 return style;
82}
83
84Result<RoadNetwork> makeTightTurnScene(int lanes) {
85 if (lanes < 1 || lanes > 4)
87 Diagnostic::error(DiagnosticCode::InvalidArgument, "lanes in [1,4] required", "tight-turn"));
88
89 RoadNetwork network;
90 RoadStyle style = groundStyle();
91 style.laneWidth = 1.2f;
92 style.sidewalkWidth = 0.25f;
93
94 auto incomingStart = network.addNode(-9.f, 0.f, -3.f, 1.f);
95 auto hub = network.addNode(0.f, 0.f, 0.f, 8.f);
96 auto outgoingEnd = network.addNode(-9.f, 0.f, 3.f, 1.f);
97 for (auto* node : {&incomingStart, &hub, &outgoingEnd}) {
98 if (!node->ok()) return Result<RoadNetwork>::failure(node->status());
99 }
100
101 auto incoming = network.addEdge(incomingStart.value(), hub.value(),
102 {P(-9.f, 0.f, -3.f), P(0.f, 0.f, 0.f)}, lanes, 0, style);
103 auto outgoing = network.addEdge(hub.value(), outgoingEnd.value(),
104 {P(0.f, 0.f, 0.f), P(-9.f, 0.f, 3.f)}, lanes, 0, style);
105 if (!incoming.ok()) return Result<RoadNetwork>::failure(incoming.status());
106 if (!outgoing.ok()) return Result<RoadNetwork>::failure(outgoing.status());
107 auto turn = network.addLaneLink({incoming.value(), 0, outgoing.value(), 0, RoadLaneDirection::Forward,
109 if (!turn.ok()) return Result<RoadNetwork>::failure(turn.status());
110 return Result<RoadNetwork>::success(std::move(network));
111}
112
113Result<RoadNetwork> makeThreeArmScene(const std::string& scene, int lanes) {
114 if (lanes < 1 || lanes > 4)
116 Diagnostic::error(DiagnosticCode::InvalidArgument, "lanes in [1,4] required", scene));
117
118 RoadNetwork network;
119 RoadStyle narrow = groundStyle();
120 RoadStyle wide = groundStyle();
121 narrow.laneWidth = 3.f;
122 wide.laneWidth = 4.2f;
123
124 const bool yJunction = scene == "y-junction";
125 auto hub = network.addNode(0.f, 0.f, 0.f, yJunction ? 5.f : 6.f);
126 auto stem = network.addNode(0.f, 0.f, 24.f, 2.f);
127 auto left = network.addNode(-18.f, 0.f, yJunction ? -18.f : 0.f, 2.f);
128 auto right = network.addNode(18.f, 0.f, yJunction ? -18.f : 0.f, 2.f);
129 for (auto* node : {&hub, &stem, &left, &right}) {
130 if (!node->ok()) return Result<RoadNetwork>::failure(node->status());
131 }
132 auto incoming = network.addEdge(stem.value(), hub.value(), {P(0.f, 0.f, 24.f), P(0.f, 0.f, 0.f)}, lanes,
133 yJunction && lanes > 1 ? 1 : 0, yJunction ? wide : narrow);
134 auto west = network.addEdge(hub.value(), left.value(),
135 {P(0.f, 0.f, 0.f), P(-18.f, 0.f, yJunction ? -18.f : 0.f)}, lanes, 0, narrow);
136 auto east = network.addEdge(hub.value(), right.value(),
137 {P(0.f, 0.f, 0.f), P(18.f, 0.f, yJunction ? -18.f : 0.f)},
138 yJunction ? std::min(4, lanes + 1) : lanes, 0, yJunction ? wide : narrow);
139 for (auto* edge : {&incoming, &west, &east}) {
140 if (!edge->ok()) return Result<RoadNetwork>::failure(edge->status());
141 }
142 auto turns = network.connectAllTurns(hub.value());
143 if (!turns.ok()) return Result<RoadNetwork>::failure(turns.status());
144 return Result<RoadNetwork>::success(std::move(network));
145}
146
147Result<RoadNetwork> makeSlopedJunctionScene(int lanes, bool curved) {
148 if (lanes < 1 || lanes > 4)
150 DiagnosticCode::InvalidArgument, "lanes in [1,4] required", curved ? "curve-uphill" : "sloped-t"));
151
152 RoadNetwork network;
153 RoadStyle style = groundStyle();
154 auto hub = network.addNode(0.f, 4.f, 0.f, 6.f);
155 auto low = network.addNode(-22.f, 0.f, 12.f, 2.f);
156 auto high = network.addNode(-20.f, 7.f, -14.f, 2.f);
157 auto exit = network.addNode(24.f, 4.f, 0.f, 2.f);
158 for (auto* node : {&hub, &low, &high, &exit}) {
159 if (!node->ok()) return Result<RoadNetwork>::failure(node->status());
160 }
161 const std::vector<RoadControlPoint> lowPoints =
162 curved ? std::vector<RoadControlPoint>{P(-22.f, 0.f, 12.f), P(-13.f, 1.4f, 14.f), P(-6.f, 3.f, 7.f),
163 P(0.f, 4.f, 0.f)}
164 : std::vector<RoadControlPoint>{P(-22.f, 0.f, 12.f), P(0.f, 4.f, 0.f)};
165 const std::vector<RoadControlPoint> highPoints =
166 curved ? std::vector<RoadControlPoint>{P(-20.f, 7.f, -14.f), P(-12.f, 6.4f, -16.f), P(-5.f, 5.f, -7.f),
167 P(0.f, 4.f, 0.f)}
168 : std::vector<RoadControlPoint>{P(-20.f, 7.f, -14.f), P(0.f, 4.f, 0.f)};
169 auto lowEdge = network.addEdge(low.value(), hub.value(), lowPoints, lanes, 0, style);
170 auto highEdge = network.addEdge(high.value(), hub.value(), highPoints, lanes, 0, style);
171 auto exitEdge = network.addEdge(hub.value(), exit.value(), {P(0.f, 4.f, 0.f), P(24.f, 4.f, 0.f)},
172 std::min(4, lanes + 1), 0, style);
173 for (auto* edge : {&lowEdge, &highEdge, &exitEdge}) {
174 if (!edge->ok()) return Result<RoadNetwork>::failure(edge->status());
175 }
176 auto turns = network.connectAllTurns(hub.value());
177 if (!turns.ok()) return Result<RoadNetwork>::failure(turns.status());
178 return Result<RoadNetwork>::success(std::move(network));
179}
180
181} // namespace
182
183Result<void> RoadNetwork::validateStyle(const RoadStyle& style) const {
184 auto profile = makeRoadProfile(style, 1, 0);
185 if (!profile.ok()) return Result<void>::failure(profile.status());
186 if (!std::isfinite(style.speedLimitMps) || style.speedLimitMps <= 0.f || style.speedLimitMps > 200.f ||
187 style.trafficPriority < 0 || style.trafficPriority > 255)
189 "road traffic settings are outside supported bounds",
190 "style.traffic"));
191 if (!std::isfinite(style.sideObjectStartOffset) || style.sideObjectStartOffset < 0.f ||
192 !std::isfinite(style.sideObjectEndOffset) || style.sideObjectEndOffset < 0.f)
194 "road side-object offsets must be finite and non-negative",
195 "style.sideObjects"));
196 return Result<void>::success();
197}
198
199int mapLaneByLateralRank(int inLane, int inLaneCount, int outLaneCount) {
200 if (inLaneCount <= 1) return (outLaneCount - 1) / 2;
201 const float rank = static_cast<float>(inLane) / static_cast<float>(inLaneCount - 1);
202 return static_cast<int>(std::lround(rank * static_cast<float>(outLaneCount - 1)));
203}
204
208
209int RoadNetwork::findNodeIndex(std::uint32_t id) const {
210 const auto it = nodeIndex_.find(id);
211 return it == nodeIndex_.end() ? -1 : it->second;
212}
213
214int RoadNetwork::findEdgeIndex(std::uint32_t id) const {
215 const auto it = edgeIndex_.find(id);
216 return it == edgeIndex_.end() ? -1 : it->second;
217}
218
219Result<std::uint32_t> RoadNetwork::addNode(float x, float y, float z, float junctionRadius) {
220 if (!finite3(x, y, z) || !std::isfinite(junctionRadius) || junctionRadius <= 0.f)
222 DiagnosticCode::InvalidArgument, "node requires finite position and positive junctionRadius", "node"));
223 const std::uint32_t id = nextNodeId_++;
224 nodeIndex_[id] = static_cast<int>(nodes_.size());
225 nodes_.push_back(RoadNode{id, x, y, z, junctionRadius});
226 ++revision_;
228}
229
231 if (node.id == 0 || node.id == std::numeric_limits<std::uint32_t>::max() || findNodeIndex(node.id) >= 0)
233 Diagnostic::error(DiagnosticCode::AlreadyExists, "node id must be non-zero and unused", "node.id"));
234 if (!finite3(node.x, node.y, node.z) || !std::isfinite(node.junctionRadius) || node.junctionRadius <= 0.f)
236 DiagnosticCode::InvalidArgument, "node requires finite position and positive junctionRadius", "node"));
237 if (!validJunctionControl(node.junctionControl))
239 DiagnosticCode::InvalidArgument, "node junction control is outside supported bounds", "node.junctionControl"));
240 nodeIndex_[node.id] = static_cast<int>(nodes_.size());
241 nodes_.push_back(node);
242 nextNodeId_ = std::max(nextNodeId_, node.id + 1);
243 ++revision_;
244 return Result<void>::success();
245}
246
248 std::vector<RoadControlPoint> controlPoints, int lanesForward,
249 int lanesBackward, RoadStyle style) {
250 if (findNodeIndex(from) < 0 || findNodeIndex(to) < 0)
252 Diagnostic::error(DiagnosticCode::NotFound, "edge endpoints must reference live nodes", "edge"));
253 if (from == to)
255 DiagnosticCode::PreconditionViolation, "edge endpoints must be distinct", "edge"));
256 if (controlPoints.size() < 2)
258 Diagnostic::error(DiagnosticCode::InvalidArgument, "edge needs at least two control points", "edge"));
259 for (const auto& p : controlPoints) {
260 if (!finite3(p.x, p.y, p.z))
262 Diagnostic::error(DiagnosticCode::InvalidArgument, "control points must be finite", "edge"));
263 }
264 auto styleOk = validateStyle(style);
265 if (!styleOk.ok()) return Result<std::uint32_t>::failure(styleOk.status());
266 auto profile = makeRoadProfile(style, lanesForward, lanesBackward);
267 if (!profile.ok()) return Result<std::uint32_t>::failure(profile.status());
268
269 // A node is the authoritative connection position. Canonicalizing the two
270 // endpoint samples prevents authored handles from leaving invisible gaps or
271 // sending turn curves toward a stale position.
272 const auto& fromNode = nodes_[static_cast<std::size_t>(findNodeIndex(from))];
273 const auto& toNode = nodes_[static_cast<std::size_t>(findNodeIndex(to))];
274 controlPoints.front() = RoadControlPoint{fromNode.x, fromNode.y, fromNode.z};
275 controlPoints.back() = RoadControlPoint{toNode.x, toNode.y, toNode.z};
276 if (!hasUsableGeometry(controlPoints))
278 DiagnosticCode::PreconditionViolation, "edge centerline must contain a non-degenerate segment", "edge"));
279
280 const std::uint32_t id = nextEdgeId_++;
281 edgeIndex_[id] = static_cast<int>(edges_.size());
283 edge.id = id;
284 edge.from = from;
285 edge.to = to;
286 edge.controlPoints = std::move(controlPoints);
287 edge.lanesForward = lanesForward;
288 edge.lanesBackward = lanesBackward;
289 edge.style = style;
290 edges_.push_back(std::move(edge));
291 ++revision_;
293}
294
296 if (edge.id == 0 || edge.id == std::numeric_limits<std::uint32_t>::max() || findEdgeIndex(edge.id) >= 0)
298 Diagnostic::error(DiagnosticCode::AlreadyExists, "edge id must be non-zero and unused", "edge.id"));
299 if (findNodeIndex(edge.from) < 0 || findNodeIndex(edge.to) < 0)
301 Diagnostic::error(DiagnosticCode::NotFound, "edge endpoints must reference live nodes", "edge"));
302 if (edge.from == edge.to)
304 Diagnostic::error(DiagnosticCode::PreconditionViolation, "edge endpoints must be distinct", "edge"));
305 if (edge.controlPoints.size() < 2)
307 Diagnostic::error(DiagnosticCode::InvalidArgument, "edge needs at least two control points", "edge"));
308 for (const auto& point : edge.controlPoints) {
309 if (!finite3(point.x, point.y, point.z))
311 Diagnostic::error(DiagnosticCode::InvalidArgument, "control points must be finite", "edge"));
312 }
313 auto styleOk = validateStyle(edge.style);
314 if (!styleOk.ok()) return styleOk;
315 auto profile = makeRoadProfile(edge.style, edge.lanesForward, edge.lanesBackward);
316 if (!profile.ok()) return Result<void>::failure(profile.status());
317 const auto& from = nodes_[static_cast<std::size_t>(findNodeIndex(edge.from))];
318 const auto& to = nodes_[static_cast<std::size_t>(findNodeIndex(edge.to))];
319 edge.controlPoints.front() = RoadControlPoint{from.x, from.y, from.z};
320 edge.controlPoints.back() = RoadControlPoint{to.x, to.y, to.z};
321 if (!hasUsableGeometry(edge.controlPoints))
323 DiagnosticCode::PreconditionViolation, "edge centerline must contain a non-degenerate segment", "edge"));
324 edgeIndex_[edge.id] = static_cast<int>(edges_.size());
325 nextEdgeId_ = std::max(nextEdgeId_, edge.id + 1);
326 edges_.push_back(std::move(edge));
327 ++revision_;
328 return Result<void>::success();
329}
330
331Result<void> RoadNetwork::validateLaneConnection(const RoadLaneConnection& link) const {
332 const int inIdx = findEdgeIndex(link.inEdge);
333 const int outIdx = findEdgeIndex(link.outEdge);
334 if (inIdx < 0 || outIdx < 0)
336 Diagnostic::error(DiagnosticCode::NotFound, "lane link references missing edges", "laneLink"));
337 const auto& inEdge = edges_[static_cast<std::size_t>(inIdx)];
338 const auto& outEdge = edges_[static_cast<std::size_t>(outIdx)];
339 const auto sharedNode = laneEntryNode(inEdge, link.inDirection);
340 if (sharedNode != laneExitNode(outEdge, link.outDirection))
342 "lane link directions must meet at the same node", "laneLink"));
343 if (link.inEdge == link.outEdge)
345 "lane link cannot make an immediate U-turn on one edge",
346 "laneLink"));
347 if (link.inLane < 0 || link.inLane >= laneCount(inEdge, link.inDirection))
349 Diagnostic::error(DiagnosticCode::InvalidArgument, "inLane out of range", "laneLink"));
350 if (link.outLane < 0 || link.outLane >= laneCount(outEdge, link.outDirection))
352 Diagnostic::error(DiagnosticCode::InvalidArgument, "outLane out of range", "laneLink"));
353 return Result<void>::success();
354}
355
357 auto valid = validateLaneConnection(link);
358 if (!valid.ok()) return valid;
359 if (std::any_of(laneLinks_.begin(), laneLinks_.end(),
360 [&](const auto& existing) { return sameLaneLink(existing, link); }))
362 Diagnostic::error(DiagnosticCode::AlreadyExists, "lane link already exists", "laneLink"));
363 if (std::any_of(blockedLaneLinks_.begin(), blockedLaneLinks_.end(),
364 [&](const auto& blocked) { return sameLaneLink(blocked, link); }))
366 DiagnosticCode::PreconditionViolation, "lane link is persistently blocked", "laneLink"));
367 laneLinks_.push_back(link);
368 ++revision_;
369 return Result<void>::success();
370}
371
373 auto valid = validateLaneConnection(link);
374 if (!valid.ok()) return Result<bool>::failure(valid.status());
375 if (std::any_of(blockedLaneLinks_.begin(), blockedLaneLinks_.end(),
376 [&](const auto& blocked) { return sameLaneLink(blocked, link); }))
378 Diagnostic::error(DiagnosticCode::AlreadyExists, "lane link is already blocked", "laneLink"));
379 auto candidate = *this;
380 const auto active = std::find_if(candidate.laneLinks_.begin(), candidate.laneLinks_.end(),
381 [&](const auto& current) { return sameLaneLink(current, link); });
382 const bool removed = active != candidate.laneLinks_.end();
383 if (removed) candidate.laneLinks_.erase(active);
384 candidate.blockedLaneLinks_.push_back(link);
385 auto checked = candidate.validate();
386 if (!checked.ok()) return Result<bool>::failure(checked.status());
387 candidate.revision_ = revision_ + 1;
388 *this = std::move(candidate);
390}
391
393 const auto found = std::find_if(blockedLaneLinks_.begin(), blockedLaneLinks_.end(),
394 [&](const auto& blocked) { return sameLaneLink(blocked, link); });
395 if (found == blockedLaneLinks_.end())
397 Diagnostic::error(DiagnosticCode::NotFound, "lane link block does not exist", "laneLink"));
398 blockedLaneLinks_.erase(found);
399 ++revision_;
400 return Result<void>::success();
401}
402
404 const auto found = std::find_if(laneLinks_.begin(), laneLinks_.end(),
405 [&](const RoadLaneConnection& candidate) { return sameLaneLink(candidate, link); });
406 if (found == laneLinks_.end())
408 Diagnostic::error(DiagnosticCode::NotFound, "lane link does not exist", "laneLink"));
409 laneLinks_.erase(found);
410 ++revision_;
411 return Result<void>::success();
412}
413
415 if (findNodeIndex(nodeId) < 0)
416 return Result<int>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown node", "nodeId"));
417 auto candidate = *this;
418 int added = 0;
420 for (const auto& inEdge : candidate.edges_) {
421 for (const auto inDirection : directions) {
422 const int inLanes = laneCount(inEdge, inDirection);
423 if (inLanes == 0 || laneEntryNode(inEdge, inDirection) != nodeId) continue;
424 for (const auto& outEdge : candidate.edges_) {
425 if (outEdge.id == inEdge.id) continue;
426 for (const auto outDirection : directions) {
427 const int outLanes = laneCount(outEdge, outDirection);
428 if (outLanes == 0 || laneExitNode(outEdge, outDirection) != nodeId) continue;
429 for (int il = 0; il < inLanes; ++il) {
430 // Preserve normalized lateral rank when lane counts change. This
431 // distributes merges/expansions across the full destination width
432 // instead of collapsing every overflow lane onto one side.
433 const int ol = mapLaneByLateralRank(il, inLanes, outLanes);
434 RoadLaneConnection link{inEdge.id, il, outEdge.id, ol, inDirection, outDirection};
435 const bool blocked = std::any_of(candidate.blockedLaneLinks_.begin(),
436 candidate.blockedLaneLinks_.end(),
437 [&](const auto& current) {
438 return sameLaneLink(current, link);
439 });
440 if (blocked) continue;
441 // Any authored link for the same incoming lane and outgoing
442 // physical route is an override, even when it selects another
443 // destination lane. Do not re-add an automatic parallel turn.
444 const bool overridden =
445 std::any_of(candidate.laneLinks_.begin(), candidate.laneLinks_.end(), [&](const auto& current) {
446 return current.inEdge == link.inEdge && current.inLane == link.inLane &&
447 current.outEdge == link.outEdge && current.inDirection == link.inDirection &&
448 current.outDirection == link.outDirection;
449 });
450 if (overridden) continue;
451 auto r = candidate.addLaneLink(link);
452 if (!r.ok()) return Result<int>::failure(r.status());
453 ++added;
454 }
455 }
456 }
457 }
458 }
459 if (added == 0) return Result<int>::success(0);
460 auto valid = candidate.validate();
461 if (!valid.ok()) return Result<int>::failure(valid.status());
462 candidate.revision_ = revision_ + 1;
463 *this = std::move(candidate);
464 return Result<int>::success(added);
465}
466
468 for (const auto& node : nodes_) {
469 if (!finite3(node.x, node.y, node.z) || !std::isfinite(node.junctionRadius) || node.junctionRadius <= 0.f ||
470 !validJunctionControl(node.junctionControl))
472 DiagnosticCode::InvariantViolation, "network contains an invalid junction node", "node"));
473 }
474 for (const auto& edge : edges_) {
475 auto styleOk = validateStyle(edge.style);
476 if (!styleOk.ok()) return styleOk;
477 if (findNodeIndex(edge.from) < 0 || findNodeIndex(edge.to) < 0)
479 Diagnostic::error(DiagnosticCode::InvariantViolation, "edge references a missing node", "edge"));
480 if (edge.from == edge.to)
482 Diagnostic::error(DiagnosticCode::InvariantViolation, "edge forms a self-loop", "edge"));
483 if (edge.controlPoints.size() < 2)
485 Diagnostic::error(DiagnosticCode::InvariantViolation, "edge has fewer than two samples", "edge"));
486 if (!hasUsableGeometry(edge.controlPoints))
488 DiagnosticCode::InvariantViolation, "edge centerline has no usable segment", "edge"));
489 const auto& a = nodes_[static_cast<std::size_t>(findNodeIndex(edge.from))];
490 const auto& b = nodes_[static_cast<std::size_t>(findNodeIndex(edge.to))];
491 const auto& p0 = edge.controlPoints.front();
492 const auto& p1 = edge.controlPoints.back();
493 if (p0.x != a.x || p0.y != a.y || p0.z != a.z || p1.x != b.x || p1.y != b.y || p1.z != b.z)
495 "edge samples are not anchored to endpoint nodes", "edge"));
496 }
497 for (std::size_t i = 0; i < laneLinks_.size(); ++i) {
498 const auto& link = laneLinks_[i];
499 const int inIdx = findEdgeIndex(link.inEdge);
500 const int outIdx = findEdgeIndex(link.outEdge);
501 if (inIdx < 0 || outIdx < 0)
503 "lane link references a missing edge", "laneLink"));
504 const auto& inEdge = edges_[static_cast<std::size_t>(inIdx)];
505 const auto& outEdge = edges_[static_cast<std::size_t>(outIdx)];
506 if (laneEntryNode(inEdge, link.inDirection) != laneExitNode(outEdge, link.outDirection) || link.inLane < 0 ||
507 link.inLane >= laneCount(inEdge, link.inDirection) || link.outLane < 0 ||
508 link.outLane >= laneCount(outEdge, link.outDirection))
510 "lane link violates endpoint or lane bounds", "laneLink"));
511 for (std::size_t j = i + 1; j < laneLinks_.size(); ++j) {
512 if (sameLaneLink(link, laneLinks_[j]))
514 "network contains a duplicate lane link", "laneLink"));
515 }
516 }
517 return Result<void>::success();
518}
519
521 const int idx = findEdgeIndex(edgeId);
522 if (idx < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
523 auto styleOk = validateStyle(style);
524 if (!styleOk.ok()) return styleOk;
525 auto profile = makeRoadProfile(style, edges_[static_cast<std::size_t>(idx)].lanesForward,
526 edges_[static_cast<std::size_t>(idx)].lanesBackward);
527 if (!profile.ok()) return Result<void>::failure(profile.status());
528 edges_[static_cast<std::size_t>(idx)].style = style;
529 ++revision_;
530 return Result<void>::success();
531}
532
533Result<void> RoadNetwork::setNodePosition(std::uint32_t nodeId, float x, float y, float z) {
534 const int index = findNodeIndex(nodeId);
535 if (index < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown node", "nodeId"));
536 if (!finite3(x, y, z))
538 Diagnostic::error(DiagnosticCode::InvalidArgument, "node position must be finite", "position"));
539
540 const auto& node = nodes_[static_cast<std::size_t>(index)];
541 if (node.x == x && node.y == y && node.z == z) return Result<void>::success();
542 auto candidate = *this;
543 auto& candidateNode = candidate.nodes_[static_cast<std::size_t>(index)];
544 candidateNode.x = x;
545 candidateNode.y = y;
546 candidateNode.z = z;
547 for (auto& edge : candidate.edges_) {
548 if (edge.from == nodeId) edge.controlPoints.front() = RoadControlPoint{x, y, z};
549 if (edge.to == nodeId) edge.controlPoints.back() = RoadControlPoint{x, y, z};
550 }
551 for (std::size_t i = 0; i < blockedLaneLinks_.size(); ++i) {
552 const auto& blocked = blockedLaneLinks_[i];
553 auto valid = validateLaneConnection(blocked);
554 if (!valid.ok())
556 DiagnosticCode::InvariantViolation, "blocked lane link violates topology", "blockedLaneLink"));
557 if (std::any_of(laneLinks_.begin(), laneLinks_.end(),
558 [&](const auto& active) { return sameLaneLink(active, blocked); }))
560 DiagnosticCode::InvariantViolation, "lane link is both active and blocked", "blockedLaneLink"));
561 for (std::size_t j = i + 1; j < blockedLaneLinks_.size(); ++j) {
562 if (sameLaneLink(blocked, blockedLaneLinks_[j]))
564 DiagnosticCode::InvariantViolation, "network contains a duplicate lane-link block",
565 "blockedLaneLink"));
566 }
567 }
568 auto valid = candidate.validate();
569 if (!valid.ok()) return valid;
570 candidate.revision_ = revision_ + 1;
571 *this = std::move(candidate);
572 return Result<void>::success();
573}
574
575Result<void> RoadNetwork::setNodeJunctionRadius(std::uint32_t nodeId, float junctionRadius) {
576 const int index = findNodeIndex(nodeId);
577 if (index < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown node", "nodeId"));
578 if (!std::isfinite(junctionRadius) || junctionRadius <= 0.f)
580 DiagnosticCode::InvalidArgument, "junction radius must be finite and positive", "junctionRadius"));
581 auto& node = nodes_[static_cast<std::size_t>(index)];
582 if (node.junctionRadius == junctionRadius) return Result<void>::success();
583 node.junctionRadius = junctionRadius;
584 ++revision_;
585 return Result<void>::success();
586}
587
589 const int index = findNodeIndex(nodeId);
590 if (index < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown node", "nodeId"));
591 if (!validJunctionControl(control))
593 DiagnosticCode::InvalidArgument, "junction control is outside supported bounds", "junctionControl"));
594 auto& node = nodes_[static_cast<std::size_t>(index)];
595 if (node.junctionControl == control) return Result<void>::success();
596 node.junctionControl = control;
597 ++revision_;
598 return Result<void>::success();
599}
600
601Result<int> RoadNetwork::mergeNodes(std::uint32_t keepNodeId, std::uint32_t removeNodeId, float maxDistance) {
602 if (keepNodeId == removeNodeId)
604 Diagnostic::error(DiagnosticCode::InvalidArgument, "merge nodes must be distinct", "removeNodeId"));
605 const int keepIndex = findNodeIndex(keepNodeId), removeIndex = findNodeIndex(removeNodeId);
606 if (keepIndex < 0 || removeIndex < 0)
608 Diagnostic::error(DiagnosticCode::NotFound, "merge references an unknown node", "nodeId"));
609 if (!std::isfinite(maxDistance) || maxDistance < 0.f)
611 Diagnostic::error(DiagnosticCode::InvalidArgument, "merge distance must be finite and non-negative",
612 "maxDistance"));
613 const auto& keep = nodes_[static_cast<std::size_t>(keepIndex)];
614 const auto& removed = nodes_[static_cast<std::size_t>(removeIndex)];
615 const float dx = keep.x - removed.x, dy = keep.y - removed.y, dz = keep.z - removed.z;
616 if (dx * dx + dy * dy + dz * dz > maxDistance * maxDistance)
618 DiagnosticCode::PreconditionViolation, "nodes are outside the merge distance", "maxDistance"));
619 if (std::any_of(edges_.begin(), edges_.end(), [&](const auto& edge) {
620 return (edge.from == keepNodeId && edge.to == removeNodeId) ||
621 (edge.from == removeNodeId && edge.to == keepNodeId);
622 }))
624 DiagnosticCode::PreconditionViolation, "merging nodes would create a self-loop edge", "removeNodeId"));
625
626 auto candidate = *this;
627 int rewired = 0;
628 for (auto& edge : candidate.edges_) {
629 if (edge.from == removeNodeId) {
630 edge.from = keepNodeId;
631 edge.controlPoints.front() = {keep.x, keep.y, keep.z};
632 ++rewired;
633 }
634 if (edge.to == removeNodeId) {
635 edge.to = keepNodeId;
636 edge.controlPoints.back() = {keep.x, keep.y, keep.z};
637 ++rewired;
638 }
639 }
640 candidate.nodes_.erase(candidate.nodes_.begin() + removeIndex);
641 candidate.nodeIndex_.clear();
642 for (std::size_t i = 0; i < candidate.nodes_.size(); ++i)
643 candidate.nodeIndex_[candidate.nodes_[i].id] = static_cast<int>(i);
644 auto valid = candidate.validate();
645 if (!valid.ok()) return Result<int>::failure(valid.status());
646 candidate.revision_ = revision_ + 1;
647 *this = std::move(candidate);
648 return Result<int>::success(rewired);
649}
650
651Result<void> RoadNetwork::setEdgeControlPoints(std::uint32_t edgeId, std::vector<RoadControlPoint> controlPoints) {
652 const int index = findEdgeIndex(edgeId);
653 if (index < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
654 if (controlPoints.size() < 2)
656 Diagnostic::error(DiagnosticCode::InvalidArgument, "edge needs at least two control points", "points"));
657 for (const auto& point : controlPoints) {
658 if (!finite3(point.x, point.y, point.z))
660 Diagnostic::error(DiagnosticCode::InvalidArgument, "control points must be finite", "points"));
661 }
662
663 auto& edge = edges_[static_cast<std::size_t>(index)];
664 const auto& from = nodes_[static_cast<std::size_t>(findNodeIndex(edge.from))];
665 const auto& to = nodes_[static_cast<std::size_t>(findNodeIndex(edge.to))];
666 controlPoints.front() = RoadControlPoint{from.x, from.y, from.z};
667 controlPoints.back() = RoadControlPoint{to.x, to.y, to.z};
668 if (!hasUsableGeometry(controlPoints))
670 DiagnosticCode::PreconditionViolation, "edge centerline must contain a non-degenerate segment", "points"));
671 edge.controlPoints = std::move(controlPoints);
672 ++revision_;
673 return Result<void>::success();
674}
675
676Result<int> RoadNetwork::setEdgeLaneCounts(std::uint32_t edgeId, int lanesForward, int lanesBackward) {
677 const int idx = findEdgeIndex(edgeId);
678 if (idx < 0) return Result<int>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
679 auto profile = makeRoadProfile(edges_[static_cast<std::size_t>(idx)].style, lanesForward, lanesBackward);
680 if (!profile.ok()) return Result<int>::failure(profile.status());
681
682 auto candidate = *this;
683 auto& edge = candidate.edges_[static_cast<std::size_t>(idx)];
684 edge.lanesForward = lanesForward;
685 edge.lanesBackward = lanesBackward;
686 const auto invalid = [&](const RoadLaneConnection& link) {
687 if (link.inEdge == edgeId && link.inLane >= laneCount(edge, link.inDirection)) return true;
688 return link.outEdge == edgeId && link.outLane >= laneCount(edge, link.outDirection);
689 };
690 const auto before = candidate.laneLinks_.size();
691 candidate.laneLinks_.erase(std::remove_if(candidate.laneLinks_.begin(), candidate.laneLinks_.end(), invalid),
692 candidate.laneLinks_.end());
693 candidate.blockedLaneLinks_.erase(
694 std::remove_if(candidate.blockedLaneLinks_.begin(), candidate.blockedLaneLinks_.end(), invalid),
695 candidate.blockedLaneLinks_.end());
696 auto valid = candidate.validate();
697 if (!valid.ok()) return Result<int>::failure(valid.status());
698 candidate.revision_ = revision_ + 1;
699 const int removed = static_cast<int>(before - candidate.laneLinks_.size());
700 *this = std::move(candidate);
702}
703
705 const int index = findEdgeIndex(edgeId);
706 if (index < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
707 auto candidate = *this;
708 auto& edge = candidate.edges_[static_cast<std::size_t>(index)];
709 std::swap(edge.from, edge.to);
710 std::reverse(edge.controlPoints.begin(), edge.controlPoints.end());
711 std::swap(edge.lanesForward, edge.lanesBackward);
712 std::swap(edge.style.sideObjectStartOffset, edge.style.sideObjectEndOffset);
713 std::swap(edge.style.sideObjectsLeft, edge.style.sideObjectsRight);
714 const auto flip = [](RoadLaneDirection direction) {
716 };
717 for (auto& link : candidate.laneLinks_) {
718 if (link.inEdge == edgeId) link.inDirection = flip(link.inDirection);
719 if (link.outEdge == edgeId) link.outDirection = flip(link.outDirection);
720 }
721 for (auto& link : candidate.blockedLaneLinks_) {
722 if (link.inEdge == edgeId) link.inDirection = flip(link.inDirection);
723 if (link.outEdge == edgeId) link.outDirection = flip(link.outDirection);
724 }
725 auto valid = candidate.validate();
726 if (!valid.ok()) return valid;
727 candidate.revision_ = revision_ + 1;
728 *this = std::move(candidate);
729 return Result<void>::success();
730}
731
732Result<int> RoadNetwork::reconnectEdgeEndpoint(std::uint32_t edgeId, bool fromEndpoint,
733 std::uint32_t nodeId) {
734 const int edgeIndex = findEdgeIndex(edgeId);
735 const int nodeIndex = findNodeIndex(nodeId);
736 if (edgeIndex < 0)
737 return Result<int>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
738 if (nodeIndex < 0)
739 return Result<int>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown node", "nodeId"));
740 const auto& current = edges_[static_cast<std::size_t>(edgeIndex)];
741 const std::uint32_t oldNodeId = fromEndpoint ? current.from : current.to;
742 if (oldNodeId == nodeId) return Result<int>::success(0);
743 if ((fromEndpoint ? current.to : current.from) == nodeId)
745 DiagnosticCode::PreconditionViolation, "edge endpoint reconnect would create a self-loop", "nodeId"));
746
747 auto candidate = *this;
748 auto& edge = candidate.edges_[static_cast<std::size_t>(edgeIndex)];
749 const auto& node = candidate.nodes_[static_cast<std::size_t>(nodeIndex)];
750 if (fromEndpoint) {
751 edge.from = nodeId;
752 edge.controlPoints.front() = {node.x, node.y, node.z};
753 } else {
754 edge.to = nodeId;
755 edge.controlPoints.back() = {node.x, node.y, node.z};
756 }
757 const auto stale = [&](const RoadLaneConnection& link) {
758 if (link.inEdge != edgeId && link.outEdge != edgeId) return false;
759 return !candidate.validateLaneConnection(link).ok();
760 };
761 const auto activeBefore = candidate.laneLinks_.size();
762 const auto blockedBefore = candidate.blockedLaneLinks_.size();
763 candidate.laneLinks_.erase(std::remove_if(candidate.laneLinks_.begin(), candidate.laneLinks_.end(), stale),
764 candidate.laneLinks_.end());
765 candidate.blockedLaneLinks_.erase(
766 std::remove_if(candidate.blockedLaneLinks_.begin(), candidate.blockedLaneLinks_.end(), stale),
767 candidate.blockedLaneLinks_.end());
768 auto valid = candidate.validate();
769 if (!valid.ok()) return Result<int>::failure(valid.status());
770 candidate.revision_ = revision_ + 1;
771 const int removed = static_cast<int>((activeBefore - candidate.laneLinks_.size()) +
772 (blockedBefore - candidate.blockedLaneLinks_.size()));
773 *this = std::move(candidate);
775}
776
777Result<std::uint32_t> RoadNetwork::detachEdgeEndpoint(std::uint32_t edgeId, bool fromEndpoint) {
778 return detachEdgeEndpoint(edgeId, fromEndpoint, 0);
779}
780
781Result<std::uint32_t> RoadNetwork::detachEdgeEndpoint(std::uint32_t edgeId, bool fromEndpoint,
782 std::uint32_t nodeId) {
783 const int edgeIndex = findEdgeIndex(edgeId);
784 if (edgeIndex < 0)
786 Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
787 const auto& edge = edges_[static_cast<std::size_t>(edgeIndex)];
788 const std::uint32_t oldNodeId = fromEndpoint ? edge.from : edge.to;
789 const auto incidentCount = std::count_if(edges_.begin(), edges_.end(), [&](const RoadEdge& candidate) {
790 return candidate.from == oldNodeId || candidate.to == oldNodeId;
791 });
792 if (incidentCount < 2)
795 "edge endpoint must share a junction with another edge before detaching", "edgeId"));
796 const int oldNodeIndex = findNodeIndex(oldNodeId);
797 if (oldNodeIndex < 0)
799 Diagnostic::error(DiagnosticCode::InvariantViolation, "edge endpoint node is missing", "edgeId"));
800 const auto oldNode = nodes_[static_cast<std::size_t>(oldNodeIndex)];
801
802 auto candidate = *this;
803 std::uint32_t createdNodeId = nodeId;
804 if (createdNodeId == 0) {
805 auto created = candidate.addNode(oldNode.x, oldNode.y, oldNode.z, oldNode.junctionRadius);
806 if (!created.ok()) return Result<std::uint32_t>::failure(created.status());
807 createdNodeId = created.value();
808 } else {
809 auto restored = candidate.restoreNode(
810 {createdNodeId, oldNode.x, oldNode.y, oldNode.z, oldNode.junctionRadius});
811 if (!restored.ok()) return Result<std::uint32_t>::failure(restored.status());
812 }
813 auto reconnected = candidate.reconnectEdgeEndpoint(edgeId, fromEndpoint, createdNodeId);
814 if (!reconnected.ok()) return Result<std::uint32_t>::failure(reconnected.status());
815 candidate.revision_ = revision_ + 1;
816 const auto result = createdNodeId;
817 *this = std::move(candidate);
818 return Result<std::uint32_t>::success(result);
819}
820
821Result<RoadEdgeSplitResult> RoadNetwork::splitEdge(std::uint32_t edgeId, std::size_t controlPointIndex,
822 float junctionRadius) {
823 return splitEdge(edgeId, controlPointIndex, junctionRadius, 0, 0);
824}
825
827 float junctionRadius, std::uint32_t nodeId,
828 std::uint32_t secondEdgeId) {
829 const int idx = findEdgeIndex(edgeId);
830 if (idx < 0)
832 Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
833 if (!std::isfinite(parameter) || parameter <= 0.f || parameter >= 1.f)
835 DiagnosticCode::InvalidArgument, "spline split parameter must be inside (0,1)", "parameter"));
836
837 const auto& original = edges_[static_cast<std::size_t>(idx)];
838 auto path = makeCenterlineSpline(original);
839 if (!path.ok()) return Result<RoadEdgeSplitResult>::failure(path.status());
840 auto evaluated = path.value().evaluateResult(parameter);
841 if (!evaluated.ok()) return Result<RoadEdgeSplitResult>::failure(evaluated.status());
842
843 const int segmentCount = path.value().segmentCount();
844 const float scaled = parameter * static_cast<float>(segmentCount);
845 const int nearest = static_cast<int>(std::round(scaled));
846 auto candidate = *this;
847 std::size_t splitIndex = 0;
848 if (nearest > 0 && nearest < segmentCount && std::fabs(scaled - static_cast<float>(nearest)) <= 1e-5f) {
849 splitIndex = static_cast<std::size_t>(nearest);
850 } else {
851 const int segment = std::clamp(static_cast<int>(std::floor(scaled)), 0, segmentCount - 1);
852 auto points = original.controlPoints;
853 splitIndex = static_cast<std::size_t>(segment + 1);
854 points.insert(points.begin() + static_cast<std::ptrdiff_t>(splitIndex),
855 RoadControlPoint{evaluated.value().x, evaluated.value().y, evaluated.value().z});
856 auto changed = candidate.setEdgeControlPoints(edgeId, std::move(points));
857 if (!changed.ok()) return Result<RoadEdgeSplitResult>::failure(changed.status());
858 }
859 auto split = candidate.splitEdge(edgeId, splitIndex, junctionRadius, nodeId, secondEdgeId);
860 if (!split.ok()) return split;
861 const auto result = split.value();
862 candidate.revision_ = revision_ + 1;
863 *this = std::move(candidate);
865}
866
867Result<RoadEdgeSplitResult> RoadNetwork::splitEdge(std::uint32_t edgeId, std::size_t controlPointIndex,
868 float junctionRadius, std::uint32_t nodeId,
869 std::uint32_t secondEdgeId) {
870 const int idx = findEdgeIndex(edgeId);
871 if (idx < 0)
873 Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
874 const RoadEdge original = edges_[static_cast<std::size_t>(idx)];
875 if (controlPointIndex == 0 || controlPointIndex + 1 >= original.controlPoints.size())
877 DiagnosticCode::InvalidArgument, "edge split requires an interior control point", "controlPointIndex"));
878 if (!std::isfinite(junctionRadius) || junctionRadius <= 0.f)
880 Diagnostic::error(DiagnosticCode::InvalidArgument, "edge split radius must be positive", "junctionRadius"));
881
882 auto candidate = *this;
883 const auto splitPoint = original.controlPoints[controlPointIndex];
884 std::uint32_t insertedNodeId = nodeId;
885 if (insertedNodeId == 0) {
886 auto node = candidate.addNode(splitPoint.x, splitPoint.y, splitPoint.z, junctionRadius);
887 if (!node.ok()) return Result<RoadEdgeSplitResult>::failure(node.status());
888 insertedNodeId = node.value();
889 } else {
890 auto node = candidate.restoreNode({insertedNodeId, splitPoint.x, splitPoint.y, splitPoint.z, junctionRadius});
891 if (!node.ok()) return Result<RoadEdgeSplitResult>::failure(node.status());
892 }
893
894 auto& first = candidate.edges_[static_cast<std::size_t>(idx)];
895 first.to = insertedNodeId;
896 first.controlPoints.resize(controlPointIndex + 1u);
897 first.controlPoints.back() = splitPoint;
898 std::vector<RoadControlPoint> continuation(original.controlPoints.begin() +
899 static_cast<std::ptrdiff_t>(controlPointIndex),
900 original.controlPoints.end());
901 std::uint32_t continuationEdgeId = secondEdgeId;
902 if (continuationEdgeId == 0) {
903 auto second = candidate.addEdge(insertedNodeId, original.to, std::move(continuation), original.lanesForward,
904 original.lanesBackward, original.style);
905 if (!second.ok()) return Result<RoadEdgeSplitResult>::failure(second.status());
906 continuationEdgeId = second.value();
907 } else {
908 auto second = candidate.restoreEdge({continuationEdgeId, insertedNodeId, original.to, std::move(continuation),
909 original.lanesForward, original.lanesBackward, original.style});
910 if (!second.ok()) return Result<RoadEdgeSplitResult>::failure(second.status());
911 }
912
913 // The original id remains attached to the `from` half. References whose
914 // port lived at the old `to` endpoint migrate to the continuation edge.
915 for (auto& link : candidate.laneLinks_) {
916 if (link.inEdge == edgeId && link.inDirection == RoadLaneDirection::Forward)
917 link.inEdge = continuationEdgeId;
918 if (link.outEdge == edgeId && link.outDirection == RoadLaneDirection::Backward)
919 link.outEdge = continuationEdgeId;
920 }
921 for (auto& link : candidate.blockedLaneLinks_) {
922 if (link.inEdge == edgeId && link.inDirection == RoadLaneDirection::Forward)
923 link.inEdge = continuationEdgeId;
924 if (link.outEdge == edgeId && link.outDirection == RoadLaneDirection::Backward)
925 link.outEdge = continuationEdgeId;
926 }
927 for (int lane = 0; lane < original.lanesForward; ++lane) {
928 auto linked = candidate.addLaneLink(
929 {edgeId, lane, continuationEdgeId, lane, RoadLaneDirection::Forward, RoadLaneDirection::Forward});
930 if (!linked.ok()) return Result<RoadEdgeSplitResult>::failure(linked.status());
931 }
932 for (int lane = 0; lane < original.lanesBackward; ++lane) {
933 auto linked = candidate.addLaneLink(
934 {continuationEdgeId, lane, edgeId, lane, RoadLaneDirection::Backward, RoadLaneDirection::Backward});
935 if (!linked.ok()) return Result<RoadEdgeSplitResult>::failure(linked.status());
936 }
937 auto valid = candidate.validate();
938 if (!valid.ok()) return Result<RoadEdgeSplitResult>::failure(valid.status());
939 candidate.revision_ = revision_ + 1;
940 const RoadEdgeSplitResult result{insertedNodeId, edgeId, continuationEdgeId};
941 *this = std::move(candidate);
943}
944
946 float maxDistance, float junctionRadius) {
947 return splitEdgeAtPosition(edgeId, position, maxDistance, junctionRadius, 0, 0);
948}
949
951 float maxDistance, float junctionRadius,
952 std::uint32_t nodeId, std::uint32_t secondEdgeId) {
953 const int idx = findEdgeIndex(edgeId);
954 if (idx < 0)
956 Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
957 if (!finite3(position.x, position.y, position.z) || !std::isfinite(maxDistance) || maxDistance < 0.f)
959 DiagnosticCode::InvalidArgument, "split position and snap distance must be finite", "position"));
960
961 const auto& edge = edges_[static_cast<std::size_t>(idx)];
962 auto path = makeCenterlineSpline(edge);
963 if (!path.ok()) return Result<RoadEdgeSplitResult>::failure(path.status());
964 const int intervals = std::min(4096, std::max(32, path.value().segmentCount() * 32));
965 float bestDistanceSquared = std::numeric_limits<float>::max();
966 float bestParameter = 0.f;
967 auto previous = path.value().evaluateResult(0.f);
968 if (!previous.ok()) return Result<RoadEdgeSplitResult>::failure(previous.status());
969 for (int i = 0; i < intervals; ++i) {
970 const float nextParameter = static_cast<float>(i + 1) / static_cast<float>(intervals);
971 auto next = path.value().evaluateResult(nextParameter);
972 if (!next.ok()) return Result<RoadEdgeSplitResult>::failure(next.status());
973 const RoadControlPoint a{previous.value().x, previous.value().y, previous.value().z};
974 const RoadControlPoint b{next.value().x, next.value().y, next.value().z};
975 const float dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
976 const float lengthSquared = dx * dx + dy * dy + dz * dz;
977 if (lengthSquared > 1e-8f) {
978 const float local = std::clamp(((position.x - a.x) * dx + (position.y - a.y) * dy +
979 (position.z - a.z) * dz) /
980 lengthSquared,
981 0.f, 1.f);
982 const RoadControlPoint projected{a.x + dx * local, a.y + dy * local, a.z + dz * local};
983 const float px = position.x - projected.x, py = position.y - projected.y, pz = position.z - projected.z;
984 const float distanceSquared = px * px + py * py + pz * pz;
985 if (distanceSquared < bestDistanceSquared) {
986 bestDistanceSquared = distanceSquared;
987 const float previousParameter = static_cast<float>(i) / static_cast<float>(intervals);
988 bestParameter = previousParameter + (nextParameter - previousParameter) * local;
989 }
990 }
991 previous = std::move(next);
992 }
993 if (bestDistanceSquared > maxDistance * maxDistance)
995 DiagnosticCode::PreconditionViolation, "position is outside the road snap distance", "maxDistance"));
996
997 constexpr float endpointTolerance = 1e-4f;
998 if (bestParameter <= endpointTolerance || bestParameter >= 1.f - endpointTolerance)
1000 DiagnosticCode::PreconditionViolation, "position resolves to an edge endpoint", "position"));
1001 return splitEdgeAtSplineParameter(edgeId, bestParameter, junctionRadius, nodeId, secondEdgeId);
1002}
1003
1005 const int index = findEdgeIndex(edgeId);
1006 if (index < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "edgeId"));
1007 laneLinks_.erase(
1008 std::remove_if(laneLinks_.begin(), laneLinks_.end(),
1009 [edgeId](const auto& link) { return link.inEdge == edgeId || link.outEdge == edgeId; }),
1010 laneLinks_.end());
1011 blockedLaneLinks_.erase(
1012 std::remove_if(blockedLaneLinks_.begin(), blockedLaneLinks_.end(),
1013 [edgeId](const auto& link) { return link.inEdge == edgeId || link.outEdge == edgeId; }),
1014 blockedLaneLinks_.end());
1015 edges_.erase(edges_.begin() + index);
1016 edgeIndex_.clear();
1017 for (std::size_t i = 0; i < edges_.size(); ++i) edgeIndex_[edges_[i].id] = static_cast<int>(i);
1018 ++revision_;
1019 return Result<void>::success();
1020}
1021
1023 const int index = findNodeIndex(nodeId);
1024 if (index < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown node", "nodeId"));
1025 if (std::any_of(edges_.begin(), edges_.end(),
1026 [nodeId](const auto& edge) { return edge.from == nodeId || edge.to == nodeId; }))
1027 return Result<void>::failure(
1028 Diagnostic::error(DiagnosticCode::PreconditionViolation, "node must be isolated before removal", "nodeId"));
1029 nodes_.erase(nodes_.begin() + index);
1030 nodeIndex_.clear();
1031 for (std::size_t i = 0; i < nodes_.size(); ++i) nodeIndex_[nodes_[i].id] = static_cast<int>(i);
1032 ++revision_;
1033 return Result<void>::success();
1034}
1035
1037 nodes_.clear();
1038 edges_.clear();
1039 laneLinks_.clear();
1040 blockedLaneLinks_.clear();
1041 nodeIndex_.clear();
1042 edgeIndex_.clear();
1043 nextNodeId_ = 1;
1044 nextEdgeId_ = 1;
1045 ++revision_;
1046}
1047
1049 const int idx = findNodeIndex(id);
1050 if (idx < 0) return Result<RoadNode>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown node", "id"));
1051 return Result<RoadNode>::success(nodes_[static_cast<std::size_t>(idx)]);
1052}
1053
1055 const int idx = findEdgeIndex(id);
1056 if (idx < 0) return Result<RoadEdge>::failure(Diagnostic::error(DiagnosticCode::NotFound, "unknown edge", "id"));
1057 return Result<RoadEdge>::success(edges_[static_cast<std::size_t>(idx)]);
1058}
1059
1061 if (!std::isfinite(length) || length < 8.f || lanes < 1 || lanes > 4)
1063 Diagnostic::error(DiagnosticCode::InvalidArgument, "length>=8, lanes in [1,4] required", "straight"));
1064 RoadNetwork network;
1065 const float half = length * 0.5f;
1066 auto a = network.addNode(-half, 0.f, 0.f, 2.f);
1067 auto b = network.addNode(half, 0.f, 0.f, 2.f);
1068 if (!a.ok()) return Result<RoadNetwork>::failure(a.status());
1069 if (!b.ok()) return Result<RoadNetwork>::failure(b.status());
1070 auto edge = network.addEdge(a.value(), b.value(), {P(-half, 0.f, 0.f), P(0.f, 0.f, 0.f), P(half, 0.f, 0.f)}, lanes,
1071 0, groundStyle());
1072 if (!edge.ok()) return Result<RoadNetwork>::failure(edge.status());
1073 return Result<RoadNetwork>::success(std::move(network));
1074}
1075
1077 if (!std::isfinite(radius) || radius < 8.f || lanes < 1 || lanes > 4)
1079 Diagnostic::error(DiagnosticCode::InvalidArgument, "radius>=8, lanes in [1,4] required", "curve"));
1080 RoadNetwork network;
1081 auto a = network.addNode(-radius, 0.f, 0.f, 2.f);
1082 auto b = network.addNode(0.f, 0.f, radius, 2.f);
1083 if (!a.ok()) return Result<RoadNetwork>::failure(a.status());
1084 if (!b.ok()) return Result<RoadNetwork>::failure(b.status());
1085 auto edge = network.addEdge(a.value(), b.value(),
1086 {P(-radius, 0.f, 0.f), P(-radius * 0.55f, 0.f, radius * 0.15f),
1087 P(-radius * 0.15f, 0.f, radius * 0.55f), P(0.f, 0.f, radius)},
1088 lanes, 0, groundStyle());
1089 if (!edge.ok()) return Result<RoadNetwork>::failure(edge.status());
1090 return Result<RoadNetwork>::success(std::move(network));
1091}
1092
1094 if (!std::isfinite(length) || length < 12.f || !std::isfinite(height) || height < 2.f || lanes < 1 || lanes > 4)
1096 DiagnosticCode::InvalidArgument, "length>=12, height>=2, lanes in [1,4] required", "bridge"));
1097 RoadNetwork network;
1098 const float half = length * 0.5f;
1099 // Flat elevated deck at constant height (no ramps yet) — isolates pier baking
1100 // from Frenet-frame twist that shows up on steep climbs.
1101 auto a = network.addNode(-half, height, 0.f, 2.f);
1102 auto b = network.addNode(half, height, 0.f, 2.f);
1103 if (!a.ok()) return Result<RoadNetwork>::failure(a.status());
1104 if (!b.ok()) return Result<RoadNetwork>::failure(b.status());
1105 auto edge =
1106 network.addEdge(a.value(), b.value(), {P(-half, height, 0.f), P(0.f, height, 0.f), P(half, height, 0.f)}, lanes,
1107 0, bridgeStyle());
1108 if (!edge.ok()) return Result<RoadNetwork>::failure(edge.status());
1109 return Result<RoadNetwork>::success(std::move(network));
1110}
1111
1113 if (!std::isfinite(span) || span < 16.f || lanes < 1 || lanes > 4)
1115 Diagnostic::error(DiagnosticCode::InvalidArgument, "span>=16, lanes in [1,4] required", "cross"));
1116 RoadNetwork network;
1117 const float half = span * 0.5f;
1118 RoadStyle style = groundStyle();
1119 style.deckThickness = 0.08f;
1120 style.sidewalkWidth = 1.2f;
1121 style.curbWidth = 0.35f;
1122 style.curbHeight = 0.45f;
1123 // Push arms outward past the asphalt half-width so sidewalks do not overlap;
1124 // the leftover ring is filled by quarter-circle curb returns.
1125 const float asphaltHalf = 0.5f * style.laneWidth * static_cast<float>(lanes);
1126 const float cornerR = 2.8f;
1127 const float jr = asphaltHalf + cornerR;
1128 auto nC = network.addNode(0.f, 0.f, 0.f, jr);
1129 auto nN = network.addNode(0.f, 0.f, -half, 2.f);
1130 auto nS = network.addNode(0.f, 0.f, half, 2.f);
1131 auto nW = network.addNode(-half, 0.f, 0.f, 2.f);
1132 auto nE = network.addNode(half, 0.f, 0.f, 2.f);
1133 for (auto* r : {&nC, &nN, &nS, &nW, &nE}) {
1134 if (!r->ok()) return Result<RoadNetwork>::failure(r->status());
1135 }
1136 // Arms run into the hub; bake trims each end by junctionRadius so strips
1137 // stop outside the corner arcs.
1138 auto e1 = network.addEdge(nN.value(), nC.value(), {P(0.f, 0.f, -half), P(0.f, 0.f, 0.f)}, lanes, 0, style);
1139 auto e2 = network.addEdge(nC.value(), nS.value(), {P(0.f, 0.f, 0.f), P(0.f, 0.f, half)}, lanes, 0, style);
1140 auto e3 = network.addEdge(nW.value(), nC.value(), {P(-half, 0.f, 0.f), P(0.f, 0.f, 0.f)}, lanes, 0, style);
1141 auto e4 = network.addEdge(nC.value(), nE.value(), {P(0.f, 0.f, 0.f), P(half, 0.f, 0.f)}, lanes, 0, style);
1142 for (auto* e : {&e1, &e2, &e3, &e4}) {
1143 if (!e->ok()) return Result<RoadNetwork>::failure(e->status());
1144 }
1145 auto turns = network.connectAllTurns(nC.value());
1146 if (!turns.ok()) return Result<RoadNetwork>::failure(turns.status());
1147 return Result<RoadNetwork>::success(std::move(network));
1148}
1149
1151 if (!std::isfinite(span) || span < 16.f || lanes < 1 || lanes > 4)
1153 Diagnostic::error(DiagnosticCode::InvalidArgument, "span>=16, lanes in [1,4] required", "tee"));
1154 RoadNetwork network;
1155 const float half = span * 0.5f;
1156 RoadStyle style = groundStyle();
1157 style.deckThickness = 0.08f;
1158 style.sidewalkWidth = 1.2f;
1159 style.curbWidth = 0.35f;
1160 style.curbHeight = 0.45f;
1161 const float asphaltHalf = 0.5f * style.laneWidth * static_cast<float>(lanes);
1162 const float jr = asphaltHalf + 2.8f;
1163 auto center = network.addNode(0.f, 0.f, 0.f, jr);
1164 auto south = network.addNode(0.f, 0.f, half, 2.f);
1165 auto west = network.addNode(-half, 0.f, 0.f, 2.f);
1166 auto east = network.addNode(half, 0.f, 0.f, 2.f);
1167 for (auto* node : {&center, &south, &west, &east})
1168 if (!node->ok()) return Result<RoadNetwork>::failure(node->status());
1169 auto westEdge = network.addEdge(west.value(), center.value(),
1170 {P(-half, 0.f, 0.f), P(-half * 0.5f, 0.f, 0.f), P(0.f, 0.f, 0.f)}, lanes, 0,
1171 style);
1172 auto eastEdge = network.addEdge(center.value(), east.value(),
1173 {P(0.f, 0.f, 0.f), P(half * 0.5f, 0.f, 0.f), P(half, 0.f, 0.f)}, lanes, 0,
1174 style);
1175 auto southEdge = network.addEdge(center.value(), south.value(),
1176 {P(0.f, 0.f, 0.f), P(0.f, 0.f, half * 0.5f), P(0.f, 0.f, half)}, lanes, 0,
1177 style);
1178 for (auto* edge : {&westEdge, &eastEdge, &southEdge})
1179 if (!edge->ok()) return Result<RoadNetwork>::failure(edge->status());
1180 auto turns = network.connectAllTurns(center.value());
1181 if (!turns.ok()) return Result<RoadNetwork>::failure(turns.status());
1182 return Result<RoadNetwork>::success(std::move(network));
1183}
1184
1186 if (!std::isfinite(span) || span < 16.f || lanes < 1 || lanes > 4)
1188 Diagnostic::error(DiagnosticCode::InvalidArgument, "span>=16, lanes in [1,4] required", "y"));
1189 return makeFan(span, lanes, {270.f, 30.f, 150.f}, {true, false, false});
1190}
1191
1192Result<RoadNetwork> RoadNetwork::makeFan(float span, int lanes, std::vector<float> armAnglesDeg,
1193 std::vector<bool> intoHub) {
1194 if (!std::isfinite(span) || span < 16.f || lanes < 1 || lanes > 4)
1196 Diagnostic::error(DiagnosticCode::InvalidArgument, "span>=16, lanes in [1,4] required", "fan"));
1197 if (armAnglesDeg.size() < 2u)
1199 Diagnostic::error(DiagnosticCode::InvalidArgument, "fan needs at least two arm angles", "fan"));
1200 if (!intoHub.empty() && intoHub.size() != armAnglesDeg.size())
1202 DiagnosticCode::InvalidArgument, "intoHub size must match armAnglesDeg", "fan"));
1203
1204 struct ArmSpec {
1205 float degrees = 0.f;
1206 bool intoHub = true;
1207 };
1208 std::vector<ArmSpec> specs;
1209 specs.reserve(armAnglesDeg.size());
1210 for (std::size_t index = 0; index < armAnglesDeg.size(); ++index) {
1211 if (!std::isfinite(armAnglesDeg[index]))
1213 Diagnostic::error(DiagnosticCode::InvalidArgument, "arm angles must be finite", "fan"));
1214 float degrees = std::fmod(armAnglesDeg[index], 360.f);
1215 if (degrees < 0.f) degrees += 360.f;
1216 specs.push_back({degrees, intoHub.empty() || intoHub[index]});
1217 }
1218 std::sort(specs.begin(), specs.end(),
1219 [](const ArmSpec& first, const ArmSpec& second) { return first.degrees < second.degrees; });
1220 if (intoHub.empty())
1221 for (std::size_t index = 0; index < specs.size(); ++index) specs[index].intoHub = index % 2u == 0u;
1222 std::vector<ArmSpec> unique;
1223 unique.reserve(specs.size());
1224 for (const auto& spec : specs)
1225 if (unique.empty() || std::fabs(spec.degrees - unique.back().degrees) > 1.f) unique.push_back(spec);
1226 if (unique.size() >= 2u && std::fabs(unique.front().degrees + 360.f - unique.back().degrees) <= 1.f)
1227 unique.pop_back();
1228 if (unique.size() < 2u)
1230 Diagnostic::error(DiagnosticCode::InvalidArgument, "fan needs at least two distinct arm angles", "fan"));
1231
1232 RoadNetwork network;
1233 const float half = span * 0.5f;
1234 RoadStyle style = groundStyle();
1235 style.deckThickness = 0.08f;
1236 style.sidewalkWidth = 1.2f;
1237 style.curbWidth = 0.35f;
1238 style.curbHeight = 0.45f;
1239 const float asphaltHalf = 0.5f * style.laneWidth * static_cast<float>(lanes);
1240 auto center = network.addNode(0.f, 0.f, 0.f, asphaltHalf + 2.8f);
1241 if (!center.ok()) return Result<RoadNetwork>::failure(center.status());
1242 for (const auto& spec : unique) {
1243 const float radians = spec.degrees * 0.01745329252f;
1244 const float x = half * std::cos(radians), z = half * std::sin(radians);
1245 auto leaf = network.addNode(x, 0.f, z, 2.f);
1246 if (!leaf.ok()) return Result<RoadNetwork>::failure(leaf.status());
1247 auto edge = spec.intoHub
1248 ? network.addEdge(leaf.value(), center.value(),
1249 {P(x, 0.f, z), P(x * 0.5f, 0.f, z * 0.5f), P(0.f, 0.f, 0.f)}, lanes, 0,
1250 style)
1251 : network.addEdge(center.value(), leaf.value(),
1252 {P(0.f, 0.f, 0.f), P(x * 0.5f, 0.f, z * 0.5f), P(x, 0.f, z)}, lanes, 0,
1253 style);
1254 if (!edge.ok()) return Result<RoadNetwork>::failure(edge.status());
1255 }
1256 auto turns = network.connectAllTurns(center.value());
1257 if (!turns.ok()) return Result<RoadNetwork>::failure(turns.status());
1258 return Result<RoadNetwork>::success(std::move(network));
1259}
1260
1262 return makeFan(span, lanes, {60.f, 120.f, 270.f});
1263}
1264
1266 return makeFan(span, lanes, {0.f, 135.f, 270.f});
1267}
1268
1270 if (!std::isfinite(span) || span < 32.f || lanes < 1 || lanes > 3)
1272 DiagnosticCode::InvalidArgument, "span>=32, lanes in [1,3] required", "roundabout"));
1273 RoadNetwork network;
1274 const float outerRadius = span * 0.5f;
1275 const float ringRadius = std::clamp(span * 0.25f, 8.f, 16.f);
1276 constexpr float pi = 3.14159265358979323846f;
1277 constexpr int ringCount = 8;
1278 constexpr float ringStep = 2.f * pi / static_cast<float>(ringCount);
1279 std::vector<std::uint32_t> ringNodes;
1280 ringNodes.reserve(ringCount);
1281 for (int i = 0; i < ringCount; ++i) {
1282 const float angle = -0.5f * pi + static_cast<float>(i) * ringStep;
1283 const float seamRadius = (i % 2 == 0) ? 1.5f : 0.25f;
1284 auto node = network.addNode(std::cos(angle) * ringRadius, 0.f, std::sin(angle) * ringRadius, seamRadius);
1285 if (!node.ok()) return Result<RoadNetwork>::failure(node.status());
1286 ringNodes.push_back(node.value());
1287 }
1288
1289 RoadStyle ringStyle = groundStyle();
1290 ringStyle.laneWidth = 3.25f;
1291 ringStyle.curbWidth = 0.3f;
1292 ringStyle.sidewalkWidth = 0.75f;
1293 ringStyle.trafficPriority = 10;
1294 RoadStyle approachStyle = groundStyle();
1295 approachStyle.laneWidth = 3.f;
1296 approachStyle.trafficPriority = 1;
1297 for (int i = 0; i < ringCount; ++i) {
1298 const float a0 = -0.5f * pi + static_cast<float>(i) * ringStep;
1299 const float a3 = a0 + ringStep;
1300 const float handle = 2.f * ringRadius * std::sin(ringStep * 0.5f) / 3.f;
1301 const RoadControlPoint p0 = P(std::cos(a0) * ringRadius, 0.f, std::sin(a0) * ringRadius);
1302 const RoadControlPoint p3 = P(std::cos(a3) * ringRadius, 0.f, std::sin(a3) * ringRadius);
1303 const RoadControlPoint p1 = P(p0.x - std::sin(a0) * handle, 0.f, p0.z + std::cos(a0) * handle);
1304 const RoadControlPoint p2 = P(p3.x + std::sin(a3) * handle, 0.f, p3.z - std::cos(a3) * handle);
1305 auto edge = network.addEdge(ringNodes[static_cast<std::size_t>(i)],
1306 ringNodes[static_cast<std::size_t>((i + 1) % ringCount)],
1307 {p0, p1, p2, p3},
1308 lanes, 0, ringStyle);
1309 if (!edge.ok()) return Result<RoadNetwork>::failure(edge.status());
1310 }
1311 for (int i = 0; i < ringCount; i += 2) {
1312 const float angle = -0.5f * pi + static_cast<float>(i) * ringStep;
1313 auto outer = network.addNode(std::cos(angle) * outerRadius, 0.f, std::sin(angle) * outerRadius, 2.f);
1314 if (!outer.ok()) return Result<RoadNetwork>::failure(outer.status());
1315 auto approach = network.addEdge(outer.value(), ringNodes[static_cast<std::size_t>(i)], {{}, {}}, lanes, lanes,
1316 approachStyle);
1317 if (!approach.ok()) return Result<RoadNetwork>::failure(approach.status());
1318 }
1319 for (int i = 0; i < ringCount; i += 2) {
1320 const auto node = ringNodes[static_cast<std::size_t>(i)];
1322 if (!control.ok()) return Result<RoadNetwork>::failure(control.status());
1323 auto connected = network.connectAllTurns(node);
1324 if (!connected.ok()) return Result<RoadNetwork>::failure(connected.status());
1325 }
1326 return Result<RoadNetwork>::success(std::move(network));
1327}
1328
1329Result<RoadNetwork> RoadNetwork::makeScene(const std::string& scene, float span, float bridgeHeight, int lanes,
1330 std::uint32_t seed) {
1331 if (scene == "straight") return makeStraight(span, lanes);
1332 if (scene == "curve") return makeCurve(std::max(8.f, span * 0.5f), lanes);
1333 if (scene == "bridge") return makeBridge(span, bridgeHeight, lanes);
1334 if (scene == "cross") return makeCross(span, lanes);
1335 if (scene == "tee" || scene == "t") return makeTee(span, lanes);
1336 if (scene == "y") return makeY(span, lanes);
1337 if (scene == "fork") return makeFork(span, lanes);
1338 if (scene == "skew") return makeSkew(span, lanes);
1339 if (scene == "t-junction" || scene == "y-junction") return makeThreeArmScene(scene, lanes);
1340 if (scene == "sloped-t") return makeSlopedJunctionScene(lanes, false);
1341 if (scene == "curve-uphill") return makeSlopedJunctionScene(lanes, true);
1342 if (scene == "tight-turn") return makeTightTurnScene(lanes);
1343 if (scene == "roundabout") return makeRoundabout(span, std::min(lanes, 3));
1344 if (scene == "interchange" || scene.empty()) return makeInterchange(span, bridgeHeight, lanes, seed);
1347 "scene must be straight|curve|bridge|cross|tee|t|y|fork|skew|t-junction|y-junction|sloped-t|curve-uphill|tight-turn|roundabout|interchange",
1348 "scene"));
1349}
1350
1351Result<RoadNetwork> RoadNetwork::makeInterchange(float span, float bridgeHeight, int lanes, std::uint32_t seed) {
1352 if (!std::isfinite(span) || span < 16.f || !std::isfinite(bridgeHeight) || bridgeHeight < 1.f || lanes < 1 ||
1353 lanes > 4)
1355 DiagnosticCode::InvalidArgument, "span>=16, bridgeHeight>=1, lanes in [1,4] required", "interchange"));
1356
1357 RoadNetwork network;
1358 const float half = span * 0.5f;
1359 const float h = bridgeHeight;
1360 // Tiny seed wobble on elevated mid-handles only — ground cross stays axis-aligned
1361 // so bakeJunction keeps the same outward-center curb returns as makeCross.
1362 const float wobble = 0.25f * static_cast<float>(static_cast<int>(seed % 7u) - 3);
1363
1364 RoadStyle ground = groundStyle();
1365 ground.deckThickness = 0.08f;
1366 ground.sidewalkWidth = 1.2f;
1367 ground.curbWidth = 0.35f;
1368 ground.curbHeight = 0.45f;
1369 const float asphaltHalf = 0.5f * ground.laneWidth * static_cast<float>(lanes);
1370 const float cornerR = 2.8f;
1371 const float jr = asphaltHalf + cornerR;
1372
1373 RoadStyle bridge = bridgeStyle();
1374 bridge.pierSpacing = 14.0f;
1375 bridge.sidewalkWidth = 1.0f;
1376 bridge.curbWidth = 0.38f;
1377 bridge.curbHeight = 0.50f;
1378 RoadStyle ramp = bridge;
1379 ramp.sidewalkWidth = 0.5f;
1380 ramp.pierSpacing = 10.f;
1381
1382 auto nGround = network.addNode(0.f, 0.f, 0.f, jr);
1383 auto nN = network.addNode(0.f, 0.f, -half, 2.f);
1384 auto nS = network.addNode(0.f, 0.f, half, 2.f);
1385 auto nW = network.addNode(-half, 0.f, 0.f, 2.f);
1386 auto nE = network.addNode(half, 0.f, 0.f, 2.f);
1387
1388 // The elevated diagonal crosses the ground graph in XZ but has its own node
1389 // at bridge height. Equal planar coordinates never imply connectivity.
1390 auto nBridgeNW = network.addNode(-half, h, -half, 2.0f);
1391 auto nElev = network.addNode(0.f, h, 0.f, 2.0f);
1392 auto nBridgeSE = network.addNode(half, h, half, 2.0f);
1393 for (auto* res : {&nGround, &nN, &nS, &nW, &nE, &nBridgeNW, &nElev, &nBridgeSE}) {
1394 if (!res->ok()) return Result<RoadNetwork>::failure(res->status());
1395 }
1396
1397 auto add = [&](std::uint32_t a, std::uint32_t b, std::vector<RoadControlPoint> pts, const RoadStyle& style) {
1398 return network.addEdge(a, b, std::move(pts), lanes, 0, style);
1399 };
1400
1401 // Ground cross — same fillet construction as makeCross.
1402 const auto g = nGround.value();
1403 auto e1 = add(nN.value(), g, {P(0.f, 0.f, -half), P(0.f, 0.f, 0.f)}, ground);
1404 auto e2 = add(g, nS.value(), {P(0.f, 0.f, 0.f), P(0.f, 0.f, half)}, ground);
1405 auto e3 = add(nW.value(), g, {P(-half, 0.f, 0.f), P(0.f, 0.f, 0.f)}, ground);
1406 auto e4 = add(g, nE.value(), {P(0.f, 0.f, 0.f), P(half, 0.f, 0.f)}, ground);
1407
1408 // The central split is a smooth continuation and therefore creates no
1409 // junction apron. Seed wobble remains bounded and tangent-continuous.
1410 auto e5 = add(nBridgeNW.value(), nElev.value(),
1411 {P(-half, h, -half), P(-half * 0.45f + wobble, h, -half * 0.45f), P(0.f, h, 0.f)}, bridge);
1412 auto e6 = add(nElev.value(), nBridgeSE.value(),
1413 {P(0.f, h, 0.f), P(half * 0.45f, h, half * 0.45f + wobble), P(half, h, half)}, bridge);
1414
1415 // Four outer ramps connect both ground approaches to the elevated diagonal
1416 // without inventing planar connectivity at the central grade separation.
1417 auto r1 = network.addEdge(nN.value(), nBridgeNW.value(),
1418 {P(0.f, 0.f, -half), P(-half * 0.35f, h * 0.2f, -half),
1419 P(-half * 0.75f, h * 0.75f, -half), P(-half, h, -half)},
1420 1, 1, ramp);
1421 auto r2 = network.addEdge(nW.value(), nBridgeNW.value(),
1422 {P(-half, 0.f, 0.f), P(-half, h * 0.2f, -half * 0.35f),
1423 P(-half, h * 0.75f, -half * 0.75f), P(-half, h, -half)},
1424 1, 1, ramp);
1425 auto r3 = network.addEdge(nBridgeSE.value(), nS.value(),
1426 {P(half, h, half), P(half * 0.75f, h * 0.75f, half),
1427 P(half * 0.35f, h * 0.2f, half), P(0.f, 0.f, half)},
1428 1, 1, ramp);
1429 auto r4 = network.addEdge(nBridgeSE.value(), nE.value(),
1430 {P(half, h, half), P(half, h * 0.75f, half * 0.75f),
1431 P(half, h * 0.2f, half * 0.35f), P(half, 0.f, 0.f)},
1432 1, 1, ramp);
1433
1434 for (auto* edge : {&e1, &e2, &e3, &e4, &e5, &e6, &r1, &r2, &r3, &r4}) {
1435 if (!edge->ok()) return Result<RoadNetwork>::failure(edge->status());
1436 }
1437
1438 // Route links exist only at the ground intersection. The elevated road is
1439 // a separate graph despite sharing the same XZ crossing coordinate.
1440 auto turns = network.connectAllTurns(g);
1441 if (!turns.ok()) return Result<RoadNetwork>::failure(turns.status());
1442 for (const auto node : {nN.value(), nW.value(), nBridgeNW.value(), nBridgeSE.value(), nS.value(), nE.value()}) {
1443 turns = network.connectAllTurns(node);
1444 if (!turns.ok()) return Result<RoadNetwork>::failure(turns.status());
1445 }
1446
1447 return Result<RoadNetwork>::success(std::move(network));
1448}
1449
1450} // namespace eve::procgen::road
bool & active
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string from
float degrees
Definition CardTypes.cpp:35
Vec3 projected
Definition CaveMesh.cpp:122
float length
Definition CaveMesh.cpp:94
bool split
Definition CaveMesh.cpp:123
float py
float pz
glm::vec4 p[6]
Stable, structured diagnostics shared by engine modules.
std::string nodeId
tensor::Graph g
Definition GpuGraph.cpp:7
float u
Definition Grass.cpp:233
double r
HexVec3 left
HexVec3 right
std::int32_t second
std::int32_t first
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
int h
std::vector< Colorf > px
std::uint32_t height
TokenKind kind
std::string local
std::array< float, 3 > position
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
graphics::Canvas * previous
size_t directions
Definition OnnxLstm.cpp:29
int idx
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::uint32_t seed
Definition PointSet.cpp:807
std::shared_ptr< const std::vector< glm::vec2 > > points
std::weak_ptr< PrimitiveScene > scene
PrimitiveHandle handle
const RoadNode * node
RoadLaneDirection direction
const RoadEdge * edge
bool found
int created
int removed
double current
float dz
float dy
float dx
TacticalUnit::TurnResources turn
uint32_t index
int turns
glm::vec3 point
float angle
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Owning directed road graph with lane connectivity.
Definition RoadNetwork.h:19
Result< int > reconnectEdgeEndpoint(std::uint32_t edgeId, bool fromEndpoint, std::uint32_t nodeId)
Reconnect one edge endpoint to an existing node and prune connections left at the old junction.
Result< void > removeNode(std::uint32_t nodeId)
Remove an isolated node.
Result< void > unblockLaneLink(const RoadLaneConnection &link)
Remove one exact persistent lane-link prohibition.
Result< void > setEdgeStyle(std::uint32_t edgeId, RoadStyle style)
Replace edge style atomically.
Result< void > addLaneLink(RoadLaneConnection link)
Register one legal turn inside a shared junction node.
static Result< RoadNetwork > makeStraight(float length=36.f, int lanes=2)
Scene 1: single flat straight segment (no junction).
static Result< RoadNetwork > makeCross(float span=32.f, int lanes=2)
Scene 4: simple ground-level 4-way cross (one junction disc).
Result< RoadEdgeSplitResult > splitEdgeAtSplineParameter(std::uint32_t edgeId, float parameter, float junctionRadius=6.f, std::uint32_t nodeId=0, std::uint32_t secondEdgeId=0)
Split at a normalized parameter on the same Catmull-Rom centerline used by road baking.
Result< std::uint32_t > detachEdgeEndpoint(std::uint32_t edgeId, bool fromEndpoint)
Detach one edge endpoint onto a new coincident stable node.
static Result< RoadNetwork > makeInterchange(float span=48.f, float bridgeHeight=8.f, int lanes=2, std::uint32_t seed=1)
Build a multi-level interchange demo graph (ground cross + elevated loop + ramps).
static Result< RoadNetwork > makeBridge(float length=36.f, float height=6.f, int lanes=2)
Scene 3: elevated straight with piers.
Result< void > removeLaneLink(const RoadLaneConnection &link)
Remove one exact lane-to-lane connection.
Result< void > restoreNode(RoadNode node)
Restore a node with an explicit stable id for undo/import paths.
Result< RoadEdge > edgeResult(std::uint32_t id) const
Edge result.
Result< void > reverseEdge(std::uint32_t edgeId)
Atomically reverse one edge while preserving its physical lanes, links, and side-object intervals.
Result< void > validate() const
Validate graph, endpoint and lane-link invariants without mutation.
Result< RoadEdgeSplitResult > splitEdgeAtPosition(std::uint32_t edgeId, RoadControlPoint position, float maxDistance, float junctionRadius=6.f)
Project a world position onto the baked Catmull-Rom centerline and split at the nearest point.
static Result< RoadNetwork > makeY(float span=32.f, int lanes=2)
Ground-level Y-junction with three arms separated by 120 degrees.
Result< void > setNodeJunctionRadius(std::uint32_t nodeId, float junctionRadius)
Replace one node's positive junction trim radius.
static Result< RoadNetwork > makeFork(float span=32.f, int lanes=2)
Three-way scene with a 60-degree acute fork.
static Result< RoadNetwork > makeSkew(float span=32.f, int lanes=2)
Three-way scene with 135-degree skew corners.
Result< void > setNodeJunctionControl(std::uint32_t nodeId, RoadJunctionControl control)
Replace one node's authoritative traffic-control policy.
Result< std::uint32_t > addEdge(std::uint32_t from, std::uint32_t to, std::vector< RoadControlPoint > controlPoints, int lanesForward=2, int lanesBackward=0, RoadStyle style={})
Insert a non-degenerate directed edge between two distinct live nodes.
Result< void > setNodePosition(std::uint32_t nodeId, float x, float y, float z)
Move a node and atomically re-anchor every incident edge endpoint.
static Result< RoadNetwork > makeRoundabout(float span=48.f, int lanes=1)
Build a four-entry roundabout from ordinary nodes, curved edges and lane links.
static Result< RoadNetwork > makeScene(const std::string &scene, float span=36.f, float bridgeHeight=6.f, int lanes=2, std::uint32_t seed=1)
Dispatch a named debug/demo scene.
Result< bool > blockLaneLink(RoadLaneConnection link)
Persistently forbid one exact legal lane connection and remove it if currently active.
static Result< RoadNetwork > makeFan(float span, int lanes, std::vector< float > armAnglesDeg, std::vector< bool > intoHub={})
Build a ground-level N-way fan from absolute arm angles in degrees.
Result< RoadEdgeSplitResult > splitEdge(std::uint32_t edgeId, std::size_t controlPointIndex, float junctionRadius=6.f)
Split an edge at one authored interior control point as a single topology mutation.
Result< int > connectAllTurns(std::uint32_t nodeId)
Auto-connect all forward and backward lane ports at one node.
Result< std::uint32_t > addNode(float x, float y, float z, float junctionRadius=6.f)
Insert a junction node after validating finite coordinates.
Result< void > setEdgeControlPoints(std::uint32_t edgeId, std::vector< RoadControlPoint > controlPoints)
Replace one edge spline while preserving its node-owned endpoint positions.
void clear()
Clear every node, edge and link.
Result< RoadNode > nodeResult(std::uint32_t id) const
Node result.
Result< void > removeEdge(std::uint32_t edgeId)
Remove an edge and every lane link that references it.
static Result< RoadNetwork > makeTee(float span=32.f, int lanes=2)
Ground-level T-junction with an east-west through road and south stem.
static Result< RoadNetwork > makeCurve(float radius=18.f, int lanes=2)
Scene 2: gentle horizontal curve (no junction).
Result< void > restoreEdge(RoadEdge edge)
Restore an edge with an explicit stable id for undo/import paths.
Result< int > setEdgeLaneCounts(std::uint32_t edgeId, int lanesForward, int lanesBackward)
Atomically replace directional lane counts and remove links that become out of range.
Result< int > mergeNodes(std::uint32_t keepNodeId, std::uint32_t removeNodeId, float maxDistance)
Atomically merge one nearby endpoint node into another stable node.
Result< int > invalid(std::string message)
Invalid.
Result< RoadProfile > makeRoadProfile(const RoadStyle &style, int lanesForward, int lanesBackward)
Build a mathematical road cross-section for the given lane counts.
RoadJunctionControl
Authoritative traffic-control policy applied to every approach of one junction node.
Definition RoadTypes.h:43
@ Signal
Every incoming approach is controlled by a traffic signal.
@ Uncontrolled
Edge trafficPriority resolves right-of-way without a mandatory stop.
@ Yield
Lower-priority approaches yield; equal-priority approaches all yield.
int mapLaneByLateralRank(int inLane, int inLaneCount, int outLaneCount)
bool validJunctionControl(RoadJunctionControl control)
RoadLaneDirection
Travel direction of a lane relative to the authored edge centerline.
Definition RoadTypes.h:79
@ Forward
Travels from RoadEdge::from to RoadEdge::to.
@ Backward
Travels from RoadEdge::to to RoadEdge::from.
One authored control point on a road centerline (world space, Y-up).
Definition RoadTypes.h:12
Stable identities produced by one atomic edge split.
Definition RoadTypes.h:72
Directed centerline edge with optional reverse lanes.
Definition RoadTypes.h:61
std::vector< RoadControlPoint > controlPoints
Definition RoadTypes.h:65
One legal lane-to-lane connection inside a junction.
Definition RoadTypes.h:90
Junction node owned by a road network.
Definition RoadTypes.h:51
Cross-section and structural style for one road edge.
Definition RoadTypes.h:19
float curbWidth
Jersey-barrier thickness.
Definition RoadTypes.h:21
float curbHeight
Raised barrier height (readable asphalt channel).
Definition RoadTypes.h:22
int trafficPriority
Higher incoming-road values win uncontrolled junction priority.
Definition RoadTypes.h:35