21 float x = 0.f,
y = 0.f,
z = 0.f;
27float dot(V3
a, V3
b) {
return a.x *
b.x +
a.y *
b.y +
a.z *
b.z; }
28V3
cross(V3
a, V3
b) {
return {
a.y *
b.z -
a.z *
b.y,
a.z *
b.x -
a.x *
b.z,
a.x *
b.y -
a.y *
b.x}; }
32 return l > 1e-8f ?
a * (1.f / l) : V3{0.f, 1.f, 0.f};
40bool exceedsMeshBudget(
const MeshBuild&
mesh,
const RoadBakeOptions&
options) {
41 return static_cast<std::size_t
>(
mesh.getVertexCount()) +
static_cast<std::size_t
>(
mesh.getIndexCount()) >
45float junctionTrimDistance(
float pathLength,
float requestedRadius) {
46 const float maximum = std::max(0.f, pathLength * 0.5f - 0.5f);
47 return std::min(std::max(0.f, requestedRadius),
maximum);
50int circularArcSegments(
float radius,
float angle,
float maximumError) {
51 if (
radius <= maximumError)
return 4;
52 const float halfStep = std::acos(std::clamp(1.f - maximumError /
radius, -1.f, 1.f));
53 if (!std::isfinite(halfStep) || halfStep <= 1e-5f)
return 64;
54 return std::clamp(
static_cast<int>(std::ceil(
angle / (2.f * halfStep))), 4, 64);
57int quadraticSegments(V3
start, V3 control, V3
end,
float angle,
float maximumError) {
58 const float secondDifference =
length(
start - control * 2.f +
end);
59 const int errorSegments =
60 std::max(1,
static_cast<int>(std::ceil(std::sqrt(secondDifference / (8.f * maximumError)))));
61 const int angleSegments = 3 +
static_cast<int>(
angle * 3.f);
62 return std::clamp(std::max(errorSegments, angleSegments), 4, 64);
65Result<SplinePath> edgeToSpline(
const RoadEdge&
edge) {
67 auto kind =
path.setKindResult(
"catmullRom");
69 path.setClosed(
false);
70 for (
const auto&
p :
edge.controlPoints) {
75 auto added =
path.addPointResult(sp);
81V3 authoredOutwardDirection(
const RoadNode&
node,
const RoadEdge&
edge) {
83 const auto& adjacent =
88float asphaltHalfWidth(
const RoadEdge&
edge) {
93float junctionSocketDistance(
const RoadNetwork& network,
const RoadNode&
node,
const RoadEdge&
edge,
float pathLength) {
101 const RoadEdge* clockwise =
nullptr;
102 const RoadEdge* counterClockwise =
nullptr;
103 float clockwiseAngle = std::numeric_limits<float>::max();
104 float counterClockwiseAngle = std::numeric_limits<float>::max();
105 for (
const auto& other : network.
edges()) {
107 const V3 otherDirection = authoredOutwardDirection(
node, other);
108 const float cosine = std::clamp(
dot(
direction, otherDirection), -1.f, 1.f);
109 const float signedAngle =
111 const float angle = std::fabs(signedAngle);
112 if (signedAngle < 0.f &&
angle < clockwiseAngle) {
114 clockwiseAngle =
angle;
115 }
else if (signedAngle >= 0.f &&
angle < counterClockwiseAngle) {
116 counterClockwise = &other;
117 counterClockwiseAngle =
angle;
121 auto constrainTo = [&](
const RoadEdge* other) {
122 if (other ==
nullptr)
return;
123 const V3 otherDirection = authoredOutwardDirection(
node, *other);
124 const float cosine = std::clamp(
dot(
direction, otherDirection), -1.f, 1.f);
125 const float sine = std::sqrt(std::max(0.f, 1.f - cosine * cosine));
128 if (sine < 0.08f)
return;
129 const float tangentDistance = (asphaltHalfWidth(*other) +
halfWidth * std::fabs(cosine)) / sine;
132 constrainTo(clockwise);
133 if (counterClockwise != clockwise) constrainTo(counterClockwise);
134 return junctionTrimDistance(pathLength,
required);
137struct JunctionPlane {
141 V3
up{0.f, 1.f, 0.f};
144float planeHeight(
const JunctionPlane& plane,
float x,
float z) {
145 return plane.origin.y + plane.gradeX * (
x - plane.origin.x) + plane.gradeZ * (
z - plane.origin.z);
149JunctionPlane fitJunctionPlane(
const RoadNetwork& network,
const RoadNode&
node) {
151 float xx = 0.f, xz = 0.f, zz = 0.f, xy = 0.f, zy = 0.f;
152 for (
const auto&
edge : network.
edges()) {
155 if (!
spline.ok())
continue;
156 auto pathLength =
spline.value().lengthResult(16);
157 if (!pathLength.ok())
continue;
158 const float trim = junctionSocketDistance(network,
node,
edge, pathLength.value());
159 const float d =
edge.
from ==
node.
id ? trim : pathLength.value() - trim;
160 auto frame =
spline.value().travelFrameResult(
d,
"clamp", 16);
161 if (!frame.ok())
continue;
162 const float dx = frame.value().sample.x -
node.
x;
163 const float dz = frame.value().sample.z -
node.
z;
164 const float dy = frame.value().sample.y -
node.
y;
173 const float determinant = xx * zz - xz * xz;
174 if (std::fabs(determinant) > 1e-5f) {
175 plane.gradeX = (xy * zz - zy * xz) / determinant;
176 plane.gradeZ = (zy * xx - xy * xz) / determinant;
180 const float grade = std::sqrt(plane.gradeX * plane.gradeX + plane.gradeZ * plane.gradeZ);
182 plane.gradeX *= 0.25f / grade;
183 plane.gradeZ *= 0.25f / grade;
185 plane.up =
normalize(V3{-plane.gradeX, 1.f, -plane.gradeZ});
189Result<void> validateJunctionGeometry(
const RoadNetwork& network,
const RoadNode&
node) {
190 std::vector<const RoadEdge*> incident;
192 for (
const auto&
edge : network.
edges()) {
194 incident.push_back(&
edge);
201 "junction contains two arms with indistinguishable outgoing directions",
204 const JunctionPlane plane = fitJunctionPlane(network,
node);
206 for (
const RoadEdge*
edge : incident) {
209 auto pathLength =
spline.value().lengthResult(16);
211 const float trim = junctionSocketDistance(network,
node, *
edge, pathLength.value());
213 auto frame =
spline.value().travelFrameResult(
distance,
"clamp", 16);
215 const float expected = planeHeight(plane, frame.value().sample.x, frame.value().sample.z);
216 const float tolerance = std::max(0.35f, asphaltHalfWidth(*
edge) * 0.15f);
217 if (std::fabs(frame.value().sample.y - expected) > tolerance)
219 "junction arm sockets cannot share one bounded construction plane",
"junction");
224void blendFrameToJunctionPlane(SplineFrameSample& frame,
const JunctionPlane& plane,
float weight) {
226 const float targetY = planeHeight(plane, frame.sample.x, frame.sample.z);
227 frame.sample.y += (targetY - frame.sample.y) *
weight;
229 const V3 oldForward =
normalize(V3{frame.forwardX, frame.forwardY, frame.forwardZ});
230 const V3 oldSide =
normalize(V3{frame.sideX, frame.sideY, frame.sideZ});
232 normalize(V3{oldForward.x, plane.gradeX * oldForward.x + plane.gradeZ * oldForward.z, oldForward.z});
234 if (
dot(targetSide, oldSide) < 0.f) targetSide = targetSide * -1.f;
236 if (targetUp.y < 0.f) targetUp = targetUp * -1.f;
241 if (
dot(
up, targetUp) < 0.f)
up =
up * -1.f;
245 frame.sideX =
side.x;
246 frame.sideY =
side.y;
247 frame.sideZ =
side.z;
253float signedAreaXZ(
const std::vector<V3>& polygon) {
255 for (std::size_t i = 0; i < polygon.size(); ++i) {
256 const V3&
a = polygon[i];
257 const V3&
b = polygon[(i + 1) % polygon.size()];
263float crossXZ(V3
a, V3
b, V3
c) {
return (
b.x -
a.x) * (
c.z -
a.z) - (
b.z -
a.z) * (
c.x -
a.x); }
265bool pointInTriangleXZ(V3
p, V3
a, V3
b, V3
c,
float orientation) {
266 constexpr float epsilon = 1e-5f;
274bool triangulateBoundary(
const std::vector<V3>& polygon, std::vector<std::array<std::size_t, 3>>&
triangles) {
275 if (polygon.size() < 3)
return false;
276 const float area = signedAreaXZ(polygon);
277 if (std::fabs(
area) < 1e-5f)
return false;
278 const float orientation =
area > 0.f ? 1.f : -1.f;
279 std::vector<std::size_t> remaining(polygon.size());
280 for (std::size_t i = 0; i < remaining.size(); ++i) remaining[i] = i;
283 std::size_t guard = polygon.size() * polygon.size();
284 while (remaining.size() > 3 && guard-- > 0) {
285 bool clipped =
false;
286 for (std::size_t i = 0; i < remaining.size(); ++i) {
287 const std::size_t
previous = remaining[(i + remaining.size() - 1) % remaining.size()];
288 const std::size_t
current = remaining[i];
289 const std::size_t
next = remaining[(i + 1) % remaining.size()];
290 if (crossXZ(polygon[
previous], polygon[
current], polygon[next]) * orientation <= 1e-5f)
continue;
291 bool contains =
false;
292 for (
const std::size_t candidate : remaining) {
293 if (candidate ==
previous || candidate ==
current || candidate == next)
continue;
294 if (pointInTriangleXZ(polygon[candidate], polygon[
previous], polygon[
current], polygon[next],
300 if (contains)
continue;
302 remaining.erase(remaining.begin() +
static_cast<std::ptrdiff_t
>(i));
306 if (!clipped)
return false;
308 if (remaining.size() == 3)
triangles.push_back({remaining[0], remaining[1], remaining[2]});
309 return triangles.size() + 2 == polygon.size();
312float laneCenterOffset(
const RoadStyle& style,
int lanesForward,
int lanesBackward,
int laneIndex,
314 const float asphaltHalf = 0.5f * style.laneWidth *
static_cast<float>(lanesForward + lanesBackward);
316 return -asphaltHalf + style.laneWidth * (
static_cast<float>(laneIndex) + 0.5f);
317 const float opposingBoundary = -asphaltHalf + style.laneWidth *
static_cast<float>(lanesBackward);
318 return opposingBoundary + style.laneWidth * (
static_cast<float>(laneIndex) + 0.5f);
321void appendBox(MeshBuild&
mesh, V3
center, V3
side, V3
up, V3 forward,
float hx,
float hy,
float hz,
324 const V3 corners[8] = {
330 const int faces[6][4] = {{0, 1, 2, 3}, {4, 7, 6, 5}, {0, 4, 5, 1}, {3, 2, 6, 7}, {0, 3, 7, 4}, {1, 5, 6, 2}};
332 for (
int f = 0;
f < 6; ++
f) {
333 const auto base =
static_cast<std::uint32_t
>(
mesh.getVertexCount());
334 for (
int c = 0;
c < 4; ++
c) {
335 const V3&
p = corners[faces[
f][
c]];
337 static_cast<float>((
c >> 1) & 1));
339 mesh.addTriangle(base, base + 1, base + 2);
340 mesh.addTriangle(base, base + 2, base + 3);
344void appendStripQuad(MeshBuild&
mesh, V3
a, V3
b, V3
c, V3
d, V3
normal,
float u0,
float u1,
float v0,
float v1,
347 const auto base =
static_cast<std::uint32_t
>(
mesh.getVertexCount());
352 const V3 firstAreaNormal =
cross(
b -
a,
c -
a);
353 const V3 secondAreaNormal =
cross(
c -
a,
d -
a);
354 if (
dot(firstAreaNormal, firstAreaNormal) > 1e-12f)
mesh.addTriangle(base, base + 1, base + 2);
355 if (
dot(secondAreaNormal, secondAreaNormal) > 1e-12f)
mesh.addTriangle(base, base + 2, base + 3);
359void appendOrientedQuad(MeshBuild&
mesh, V3
a, V3
b, V3
c, V3
d, V3
normal,
float u0,
float u1,
float v0,
float v1,
365 if (
dot(geometricNormal, geometricNormal) <= 1e-12f)
return;
366 if (
dot(geometricNormal,
n) < 0.f) {
367 appendStripQuad(
mesh,
a,
d,
c,
b,
n, u0, u1, v1, v0,
material);
369 appendStripQuad(
mesh,
a,
b,
c,
d,
n, u0, u1, v0, v1,
material);
375 const V3 areaNormal =
cross(
b -
a,
c -
a);
376 if (
dot(areaNormal, areaNormal) <= 1e-12f)
return;
377 const float uvScale = 1.f / std::max(uvMeters, 0.1f);
379 const auto base =
static_cast<std::uint32_t
>(
mesh.getVertexCount());
380 mesh.addVertex(
a.x,
a.y,
a.z,
n.x,
n.y,
n.z,
a.x * uvScale,
a.z * uvScale);
381 mesh.addVertex(
b.x,
b.y,
b.z,
n.x,
n.y,
n.z,
b.x * uvScale,
b.z * uvScale);
382 mesh.addVertex(
c.x,
c.y,
c.z,
n.x,
n.y,
n.z,
c.x * uvScale,
c.z * uvScale);
383 if (
dot(areaNormal,
n) < 0.f)
384 mesh.addTriangle(base, base + 2, base + 1);
386 mesh.addTriangle(base, base + 1, base + 2);
389Result<void> loftProfileClean(MeshBuild&
mesh,
const std::vector<SplineFrameSample>& frames,
const RoadProfile& profile,
390 float pathLength,
float uvMeters) {
391 if (frames.size() < 2 || profile.points.size() < 2)
394 const int ringCount =
static_cast<int>(frames.size());
395 const int profileCount =
static_cast<int>(profile.points.size());
396 std::vector<V3> ringPositions(
static_cast<std::size_t
>(ringCount * profileCount));
398 for (
int ring = 0; ring < ringCount; ++ring) {
399 const auto& frame = frames[
static_cast<std::size_t
>(ring)];
400 const V3
origin{frame.sample.x, frame.sample.y, frame.sample.z};
401 const V3
side{frame.sideX, frame.sideY, frame.sideZ};
402 const V3
up{frame.upX, frame.upY, frame.upZ};
403 for (
int i = 0; i < profileCount; ++i) {
404 const auto& pp = profile.points[
static_cast<std::size_t
>(i)];
405 ringPositions[
static_cast<std::size_t
>(ring * profileCount + i)] =
origin +
side * pp.side +
up * pp.up;
409 for (
int ring = 0; ring < ringCount - 1; ++ring) {
413 const float u0 = frames[
static_cast<std::size_t
>(ring)].
sample.normalizedDistance * pathLength / uvMeters;
414 const float u1 = frames[
static_cast<std::size_t
>(ring + 1)].
sample.normalizedDistance * pathLength / uvMeters;
415 for (
int i = 0; i < profileCount - 1; ++i) {
416 const auto&
a = profile.points[
static_cast<std::size_t
>(i)];
417 const auto&
b = profile.points[
static_cast<std::size_t
>(i + 1)];
421 if (std::fabs(
a.up -
b.up) <= 1e-3f &&
425 const V3& p00 = ringPositions[
static_cast<std::size_t
>(ring * profileCount + i)];
426 const V3& p01 = ringPositions[
static_cast<std::size_t
>(ring * profileCount + i + 1)];
427 const V3& p10 = ringPositions[
static_cast<std::size_t
>((ring + 1) * profileCount + i)];
428 const V3& p11 = ringPositions[
static_cast<std::size_t
>((ring + 1) * profileCount + i + 1)];
430 const float v0 =
a.side / uvMeters;
431 const float v1 =
b.side / uvMeters;
432 appendStripQuad(
mesh, p00, p10, p11, p01,
normal, u0, u1, v0, v1, mat);
439void capProfileRing(MeshBuild&
mesh,
const SplineFrameSample& frame,
const RoadProfile& profile,
bool outward) {
440 if (profile.points.size() < 3)
return;
441 const V3
origin{frame.sample.x, frame.sample.y, frame.sample.z};
442 const V3
side{frame.sideX, frame.sideY, frame.sideZ};
443 const V3
up{frame.upX, frame.upY, frame.upZ};
444 V3 fwd{frame.forwardX, frame.forwardY, frame.forwardZ};
445 if (!outward) fwd = fwd * -1.f;
447 std::vector<V3> ring;
448 ring.reserve(profile.points.size());
452 const auto base =
static_cast<std::uint32_t
>(
mesh.getVertexCount());
453 for (
const V3&
p : ring)
mesh.addVertex(
p.
x,
p.
y,
p.
z, nrm.
x, nrm.
y, nrm.
z, 0.f, 0.f);
454 for (std::size_t i = 1; i + 1 < ring.size(); ++i) {
455 const V3 areaNormal =
cross(ring[i] - ring[0], ring[i + 1] - ring[0]);
456 if (
dot(areaNormal, areaNormal) <= 1e-12f)
continue;
458 mesh.addTriangle(base, base +
static_cast<std::uint32_t
>(i), base +
static_cast<std::uint32_t
>(i + 1));
460 mesh.addTriangle(base, base +
static_cast<std::uint32_t
>(i + 1), base +
static_cast<std::uint32_t
>(i));
471void capProfileShoulders(MeshBuild&
mesh,
const SplineFrameSample& frame,
const RoadProfile& profile,
bool outward) {
472 if (profile.points.size() < 6)
return;
473 const V3
origin{frame.sample.x, frame.sample.y, frame.sample.z};
474 const V3
side{frame.sideX, frame.sideY, frame.sideZ};
475 const V3
up{frame.upX, frame.upY, frame.upZ};
476 V3 fwd{frame.forwardX, frame.forwardY, frame.forwardZ};
477 if (!outward) fwd = fwd * -1.f;
481 std::size_t asphaltBegin = profile.points.size();
482 for (std::size_t i = 0; i < profile.points.size(); ++i) {
488 if (asphaltBegin == 0 || asphaltBegin >= profile.points.size())
return;
493 const auto base =
static_cast<std::uint32_t
>(
mesh.getVertexCount());
494 for (std::size_t i =
begin; i <
end; ++i) {
495 const auto& pp = profile.points[i];
497 mesh.addVertex(
p.x,
p.y,
p.z, nrm.x, nrm.y, nrm.z, 0.f, 0.f);
500 for (std::uint32_t i = 1; i + 1 <
count; ++i) {
501 const auto& originPoint = profile.points[
begin];
502 const auto& pointA = profile.points[
begin + i];
503 const auto& pointB = profile.points[
begin + i + 1];
504 const V3 edgeA =
side * (pointA.side - originPoint.side) +
up * (pointA.up - originPoint.up);
505 const V3 edgeB =
side * (pointB.side - originPoint.side) +
up * (pointB.up - originPoint.up);
506 const V3 areaNormal =
cross(edgeA, edgeB);
507 if (
dot(areaNormal, areaNormal) <= 1e-12f)
continue;
509 mesh.addTriangle(base, base + i, base + i + 1);
511 mesh.addTriangle(base, base + i + 1, base + i);
521Result<void> addDeckAndPiers(MeshBuild&
mesh,
const std::vector<SplineFrameSample>& frames,
const RoadStyle& style,
525 for (
int ring = 0; ring < static_cast<int>(frames.size()) - 1; ++ring) {
526 const auto& f0 = frames[
static_cast<std::size_t
>(ring)];
527 const auto& f1 = frames[
static_cast<std::size_t
>(ring + 1)];
528 const V3 o0{f0.sample.x, f0.sample.y, f0.sample.z};
529 const V3 o1{f1.sample.x, f1.sample.y, f1.sample.z};
530 const V3 s0{f0.sideX, f0.sideY, f0.sideZ};
531 const V3 s1{f1.sideX, f1.sideY, f1.sideZ};
532 const V3 u0{f0.upX, f0.upY, f0.upZ};
533 const V3 u1{f1.upX, f1.upY, f1.upZ};
534 const V3
a = o0 + s0 * -
halfWidth + u0 * -style.deckThickness;
535 const V3
b = o0 + s0 *
halfWidth + u0 * -style.deckThickness;
536 const V3
c = o1 + s1 *
halfWidth + u1 * -style.deckThickness;
537 const V3
d = o1 + s1 * -
halfWidth + u1 * -style.deckThickness;
538 appendStripQuad(
mesh,
a,
b,
c,
d,
normalize((u0 + u1) * -0.5f), 0.f, 1.f, 0.f, 1.f,
RoadMaterial::Deck);
543 float traveled = 0.f;
544 float nextPier = style.pierSpacing * 0.5f;
545 for (std::size_t i = 1; i < frames.size(); ++i) {
546 const auto&
a = frames[i - 1];
547 const auto&
b = frames[i];
548 const V3 pa{
a.sample.x,
a.sample.y,
a.sample.z};
549 const V3 pb{
b.sample.x,
b.sample.y,
b.sample.z};
550 const float seg =
length(pb - pa);
551 if (seg <= 1e-5f)
continue;
552 while (nextPier <= traveled + seg + 1e-4f) {
553 const float t = std::clamp((nextPier - traveled) / seg, 0.f, 1.f);
554 const V3
pos = pa + (pb - pa) *
t;
555 if (
pos.y > style.pierClearance) {
557 V3 fwdFlat{pb.x - pa.x, 0.f, pb.z - pa.z};
558 const float fwdLen =
length(fwdFlat);
559 fwdFlat = fwdLen > 1e-5f ? fwdFlat * (1.f / fwdLen) : V3{1.f, 0.f, 0.f};
560 const V3
up{0.f, 1.f, 0.f};
562 const float pierH =
pos.y - style.deckThickness;
565 appendBox(
mesh,
center,
side,
up, fwdFlat, style.pierWidth * 0.5f, pierH * 0.5f,
569 nextPier += style.pierSpacing;
570 if (nextPier > 1.0e7f)
break;
577Result<void> addLaneMarkings(MeshBuild&
mesh,
const std::vector<SplineFrameSample>& frames,
const RoadEdge&
edge,
578 float pathLength,
const RoadBakeOptions&
options) {
584 const bool continuous = !dashed || style.dashGap <= 1e-5f;
585 for (std::size_t i = 1; i < frames.size(); ++i) {
586 const auto&
a = frames[i - 1];
587 const auto&
b = frames[i];
588 const V3 pa{
a.sample.x,
a.sample.y,
a.sample.z};
589 const V3 pb{
b.sample.x,
b.sample.y,
b.sample.z};
590 const V3 sa{
a.sideX,
a.sideY,
a.sideZ};
591 const V3 sb{
b.sideX,
b.sideY,
b.sideZ};
592 const V3 ua{
a.upX,
a.upY,
a.upZ};
593 const V3 ub{
b.upX,
b.upY,
b.upZ};
594 const float seg =
length(pb - pa);
595 if (seg <= 1e-5f)
continue;
596 auto emitSpan = [&](
float t0,
float t1) -> Result<void> {
597 constexpr std::size_t quadElements = 10u;
598 const std::size_t currentElements =
static_cast<std::size_t
>(
mesh.getVertexCount()) +
599 static_cast<std::size_t
>(
mesh.getIndexCount());
600 if (currentElements >
options.maximumMeshElements ||
601 options.maximumMeshElements - currentElements < quadElements)
603 "road markings exceed the mesh element budget",
"mesh");
604 const float hw = style.markingWidth * 0.5f;
605 const V3 p0 = pa + (pb - pa) * t0;
606 const V3 p1 = pa + (pb - pa) * t1;
608 const V3 s0 =
normalize(sa + (sb - sa) * t0);
609 const V3 s1 =
normalize(sa + (sb - sa) * t1);
610 const V3 u0 =
normalize(ua + (ub - ua) * t0);
611 const V3 u1 =
normalize(ua + (ub - ua) * t1);
612 const V3 a0 = p0 + s0 * (lateral - hw) + u0 * 0.035f;
613 const V3 a1 = p0 + s0 * (lateral + hw) + u0 * 0.035f;
614 const V3 b1 = p1 + s1 * (lateral + hw) + u1 * 0.035f;
615 const V3 b0 = p1 + s1 * (lateral - hw) + u1 * 0.035f;
616 appendStripQuad(
mesh, a0, b0, b1, a1,
normalize(u0 + u1), 0.f, 1.f, 0.f, 1.f,
material);
620 auto emitted = emitSpan(0.f, 1.f);
621 if (!emitted.ok())
return emitted;
624 const double cycle =
static_cast<double>(style.dashLength) +
static_cast<double>(style.dashGap);
625 const double startDistance =
static_cast<double>(
a.sample.normalizedDistance) * pathLength;
626 const double endDistance =
static_cast<double>(
b.sample.normalizedDistance) * pathLength;
627 const double distanceSpan = std::max(0.0, endDistance - startDistance);
629 while (
local < distanceSpan - 1e-7) {
630 const double phase = std::fmod(startDistance +
local, cycle);
631 const bool draw = phase < static_cast<double>(style.dashLength);
636 "road marking period is too small to advance",
"style.dashLength");
638 auto emitted = emitSpan(
static_cast<float>(
local / distanceSpan),
639 static_cast<float>(next / distanceSpan));
640 if (!emitted.ok())
return emitted;
649 if (!leftEdge.ok())
return leftEdge;
651 if (!rightEdge.ok())
return rightEdge;
652 const float divider = -asphaltHalf + style.laneWidth *
static_cast<float>(
edge.
lanesBackward);
655 style.laneWidth *
static_cast<float>(lane);
660 const float lateral = -asphaltHalf + style.laneWidth *
static_cast<float>(lane);
665 const float offset = std::max(0.08f, style.markingWidth);
674void appendArrow(MeshBuild&
mesh, V3
pos, V3 forward, V3
up,
float size) {
680 const auto base =
static_cast<std::uint32_t
>(
mesh.getVertexCount());
684 mesh.addTriangle(base, base + 1, base + 2);
686 const auto back =
static_cast<std::uint32_t
>(
mesh.getVertexCount());
687 const V3 nd =
up * -1.f;
688 mesh.addVertex(
tip.x,
tip.y,
tip.z, nd.x, nd.y, nd.z, 0.5f, 1.f);
691 mesh.addTriangle(back, back + 1, back + 2);
694Result<void> bakeEdgeGeometry(MeshBuild&
mesh, RoadOverlay& overlay,
const RoadNetwork& network,
const RoadEdge&
edge,
695 const RoadBakeOptions&
options,
696 const std::unordered_set<std::uint32_t>& activeJunctionNodes,
697 const std::unordered_set<std::uint32_t>& terminalNodes) {
700 auto pathLength =
spline.value().lengthResult(24);
703 auto fromNode = network.nodeResult(
edge.
from);
704 auto toNode = network.nodeResult(
edge.
to);
705 if (!fromNode.ok() || !toNode.ok())
710 const float trimStart = activeJunctionNodes.contains(fromNode.value().id)
711 ? junctionSocketDistance(network, fromNode.value(),
edge, pathLength.value())
713 const float trimEnd = activeJunctionNodes.contains(toNode.value().id)
714 ? junctionSocketDistance(network, toNode.value(),
edge, pathLength.value())
716 if (trimStart + trimEnd >= pathLength.value() - 0.5f)
719 const int segments = std::max(4,
options.pathSegmentsPerEdge);
720 auto frames =
spline.value().sampleFramesResult(segments,
true, 0.f, 24);
725 std::vector<SplineFrameSample> trimmed;
726 trimmed.reserve(frames.value().size() + 2);
727 if (trimStart > 1e-3f) {
728 auto tip =
spline.value().travelFrameResult(trimStart,
"clamp", 24);
730 trimmed.push_back(std::move(
tip).takeValue());
732 const float endDist = pathLength.value() - trimEnd;
733 for (
const auto& frame : frames.
value()) {
734 const float d = frame.sample.normalizedDistance * pathLength.value();
735 if (
d > trimStart + 1e-3f &&
d < endDist - 1e-3f) trimmed.push_back(frame);
737 if (trimEnd > 1e-3f) {
738 auto tip =
spline.value().travelFrameResult(endDist,
"clamp", 24);
740 trimmed.push_back(std::move(
tip).takeValue());
747 if (trimStart > 1e-3f) {
748 const JunctionPlane plane = fitJunctionPlane(network, fromNode.value());
749 const float transition = std::max(3.f, fromNode.value().junctionRadius);
750 for (
auto& frame : trimmed) {
751 const float distance = frame.sample.normalizedDistance * pathLength.value() - trimStart;
752 if (
distance > transition)
continue;
753 const float t = std::clamp(1.f - std::max(0.f,
distance) / transition, 0.f, 1.f);
754 blendFrameToJunctionPlane(frame, plane,
t *
t * (3.f - 2.f *
t));
757 if (trimEnd > 1e-3f) {
758 const JunctionPlane plane = fitJunctionPlane(network, toNode.value());
759 const float transition = std::max(3.f, toNode.value().junctionRadius);
760 for (
auto& frame : trimmed) {
761 const float distance = endDist - frame.sample.normalizedDistance * pathLength.value();
762 if (
distance > transition)
continue;
763 const float t = std::clamp(1.f - std::max(0.f,
distance) / transition, 0.f, 1.f);
764 blendFrameToJunctionPlane(frame, plane,
t *
t * (3.f - 2.f *
t));
771 auto lofted = loftProfileClean(
mesh, trimmed, profile.value(), pathLength.value(),
edge.
style.
uvMeters);
772 if (!lofted.ok())
return lofted;
775 if (trimStart <= 0.05f && terminalNodes.contains(fromNode.value().id)) {
776 capProfileRing(
mesh, trimmed.front(), profile.value(),
false);
777 }
else if (trimStart > 0.05f) {
778 if (trimStart < 2.5f)
779 capProfileRing(
mesh, trimmed.front(), profile.value(),
false);
781 capProfileShoulders(
mesh, trimmed.front(), profile.value(),
false);
783 if (trimEnd <= 0.05f && terminalNodes.contains(toNode.value().id)) {
784 capProfileRing(
mesh, trimmed.back(), profile.value(),
true);
785 }
else if (trimEnd > 0.05f) {
787 capProfileRing(
mesh, trimmed.back(), profile.value(),
true);
789 capProfileShoulders(
mesh, trimmed.back(), profile.value(),
true);
791 auto deck = addDeckAndPiers(
mesh, trimmed,
edge.
style, profile.value().halfWidth,
options.includePiers);
792 if (!deck.ok())
return deck;
794 auto marks = addLaneMarkings(
mesh, trimmed,
edge, pathLength.value(),
options);
795 if (!marks.ok())
return marks;
798 if (
options.includeNavigation) {
800 const float lateral =
806 poly.width =
options.navRibbonHalfWidth * 2.f;
813 float traveled = 0.f;
814 float nextArrow =
options.arrowSpacing * 0.5f;
816 bool hasPrev =
false;
817 for (
const auto& frame : trimmed) {
818 const V3
origin{frame.sample.x, frame.sample.y, frame.sample.z};
819 const V3
side{frame.sideX, frame.sideY, frame.sideZ};
820 const V3
up{frame.upX, frame.upY, frame.upZ};
821 if (!std::isfinite(
origin.x) || !std::isfinite(
origin.y) || !std::isfinite(
origin.z))
continue;
823 poly.xyz.push_back(
pos.x);
824 poly.xyz.push_back(
pos.y);
825 poly.xyz.push_back(
pos.z);
828 if (std::isfinite(
step) &&
step > 1e-5f &&
step < 1.0e4f) {
832 const float hw =
options.navRibbonHalfWidth;
833 appendStripQuad(
mesh, prev + lat * -hw,
pos + lat * -hw,
pos + lat * hw, prev + lat * hw,
up,
836 while (traveled >= nextArrow && arrowGuard++ < 64) {
838 nextArrow += std::max(
options.arrowSpacing, 0.5f);
840 if (arrowGuard >= 64) nextArrow = traveled + std::max(
options.arrowSpacing, 0.5f);
846 overlay.lanes.push_back(std::move(poly));
849 const float lateral =
855 poly.width =
options.navRibbonHalfWidth * 2.f;
862 for (
auto it = trimmed.rbegin(); it != trimmed.rend(); ++it) {
863 const V3
origin{it->sample.x, it->sample.y, it->sample.z};
864 const V3
side{it->sideX, it->sideY, it->sideZ};
865 const V3
up{it->upX, it->upY, it->upZ};
867 poly.xyz.insert(poly.xyz.end(), {
pos.x,
pos.y,
pos.z});
869 overlay.lanes.push_back(std::move(poly));
875Result<void> bakeJunction(MeshBuild&
mesh,
const RoadNetwork& network,
const RoadNode&
node,
876 const RoadBakeOptions&
options) {
883 float asphaltHalf = 0.f;
885 float curbWidth = 0.f;
886 float curbHeight = 0.f;
887 float sidewalkW = 0.f;
888 float sidewalkH = 0.f;
889 float uvMeters = 8.f;
891 std::vector<ArmTip> arms;
892 float maxHalfWidth = 0.f;
893 float maxAsphalt = 0.f;
894 float maxCurbWidth = 0.f;
895 float maxCurbHeight = 0.f;
896 float maxSidewalkWidth = 0.f;
897 float maxSidewalkHeight = 0.f;
898 const JunctionPlane junctionPlane = fitJunctionPlane(network,
node);
899 for (
const auto&
edge : network.
edges()) {
902 if (!
spline.ok())
continue;
903 auto len =
spline.value().lengthResult(16);
904 if (!len.ok())
continue;
905 const float trim = junctionSocketDistance(network,
node,
edge, len.value());
906 const float d =
edge.
to ==
node.
id ? len.value() - trim : trim;
907 auto frame =
spline.value().travelFrameResult(
d,
"clamp", 16);
908 if (!frame.ok())
continue;
909 blendFrameToJunctionPlane(frame.value(), junctionPlane, 1.f);
911 if (!profile.ok())
continue;
912 const float asphaltHalf =
914 const auto&
f = frame.value();
916 tip.origin = V3{
f.sample.x,
f.sample.y,
f.sample.z};
918 V3 tangentOut{
f.forwardX,
f.forwardY,
f.forwardZ};
921 V3{tangentOut.x, junctionPlane.gradeX * tangentOut.x + junctionPlane.gradeZ * tangentOut.z, tangentOut.z});
922 tip.authoredOutward = authoredOutwardDirection(
node,
edge);
923 const V3 canonicalSide{-outward.z, 0.f, outward.x};
928 if (
dot(
tip.side, canonicalSide) < 0.f)
tip.side =
tip.side * -1.f;
930 if (
tip.up.y < 0.f)
tip.up =
tip.up * -1.f;
931 tip.asphaltHalf = asphaltHalf;
932 tip.halfWidth = profile.value().halfWidth;
939 maxHalfWidth = std::max(maxHalfWidth,
tip.halfWidth);
940 maxAsphalt = std::max(maxAsphalt,
tip.asphaltHalf);
952 bool axisCross = arms.size() == 4;
955 for (
const auto&
tip : arms) {
956 const float dx = std::fabs(
tip.origin.x -
node.
x);
957 const float dz = std::fabs(
tip.origin.z -
node.
z);
958 if ((dx < 0.35f && dz > jr * 0.4f) || (dz < 0.35f && dx > jr * 0.4f)) ++axisHits;
960 axisCross = axisHits == 4;
968 const float ah = maxAsphalt;
969 const float cornerR = std::max(0.75f, jr - ah);
970 const int segs = circularArcSegments(cornerR, 1.5707963f,
options.junctionChordError);
971 const V3 upN = junctionPlane.up;
972 auto surfacePoint = [&](
float x,
float z,
float offset = 0.f) {
973 return V3{
x, planeHeight(junctionPlane,
x,
z),
z} + upN *
offset;
978 std::vector<std::pair<float, float>> rim;
979 rim.reserve(
static_cast<std::size_t
>(4 * (segs + 3)));
980 auto pushPt = [&](
float x,
float z) {
981 if (!rim.empty() && std::fabs(rim.back().first -
x) < 1e-4f && std::fabs(rim.back().second -
z) < 1e-4f)
983 rim.emplace_back(
x,
z);
985 auto pushCornerArc = [&](
int sx,
int sz,
bool reverse) {
986 const float sxf =
static_cast<float>(
sx);
987 const float szf =
static_cast<float>(
sz);
988 const float cx =
node.
x + sxf * jr;
989 const float cz =
node.
z + szf * jr;
990 for (
int i = 0; i <= segs; ++i) {
991 const int ii = reverse ? (segs - i) : i;
992 const float t =
static_cast<float>(ii) /
static_cast<float>(segs);
993 const float th =
t * 1.5707963f;
994 pushPt(
cx - sxf * cornerR * std::sin(th), cz - szf * cornerR * std::cos(th));
999 pushCornerArc(+1, +1,
false);
1001 pushCornerArc(-1, +1,
true);
1003 pushCornerArc(-1, -1,
false);
1005 pushCornerArc(+1, -1,
true);
1007 const V3 hub = surfacePoint(
node.
x,
node.
z, 0.01f);
1008 for (std::size_t i = 0; i < rim.size(); ++i) {
1009 const auto& p0 = rim[i];
1010 const auto& p1 = rim[(i + 1) % rim.size()];
1011 appendOrientedTri(
mesh, hub, surfacePoint(p0.first, p0.second, 0.01f),
1012 surfacePoint(p1.first, p1.second, 0.01f), upN, arms.front().uvMeters,
1017 const float seal = 0.12f;
1018 auto addSeal = [&](
float x0,
float z0,
float x1,
float z1,
float x2,
float z2,
float x3,
float z3) {
1019 appendOrientedQuad(
mesh, surfacePoint(x0, z0, 0.012f), surfacePoint(x1, z1, 0.012f),
1020 surfacePoint(x2, z2, 0.012f), surfacePoint(x3, z3, 0.012f), upN, 0.f, 1.f, 0.f,
1033 const float curbW = 0.35f;
1034 const float walkW = std::max(0.4f, maxHalfWidth - ah - curbW);
1035 float curbH = 0.45f;
1036 float walkH = 0.14f;
1037 if (!arms.empty()) {
1038 curbH = arms.front().curbHeight;
1039 walkH = arms.front().sidewalkH;
1041 const float r0 = cornerR;
1042 const float r1 = std::max(0.2f, cornerR - curbW);
1043 const float r2 = std::max(0.15f, cornerR - curbW - walkW);
1044 for (
int sx : {-1, 1}) {
1045 for (
int sz : {-1, 1}) {
1046 const float sxf =
static_cast<float>(
sx);
1047 const float szf =
static_cast<float>(
sz);
1048 const float cx =
node.
x + sxf * jr;
1049 const float cz =
node.
z + szf * jr;
1050 auto arcPt = [&](
float radius,
float t) {
1051 const float th =
t * 1.5707963f;
1052 return V3{
cx - sxf *
radius * std::sin(th), 0.f, cz - szf *
radius * std::cos(th)};
1054 auto wall = [&](V3 p0, V3 p1,
float offset0,
float offset1, V3
n,
RoadMaterial mat) {
1055 appendOrientedQuad(
mesh, surfacePoint(p0.x, p0.z, offset0), surfacePoint(p1.x, p1.z, offset0),
1056 surfacePoint(p1.x, p1.z, offset1), surfacePoint(p0.x, p0.z, offset1),
n, 0.f,
1057 1.f, 0.f, 1.f, mat);
1058 appendOrientedQuad(
mesh, surfacePoint(p0.x, p0.z, offset0), surfacePoint(p1.x, p1.z, offset0),
1059 surfacePoint(p1.x, p1.z, offset1), surfacePoint(p0.x, p0.z, offset1),
n * -1.f,
1060 0.f, 1.f, 0.f, 1.f, mat);
1062 for (
int i = 0; i < segs; ++i) {
1063 const float t0 =
static_cast<float>(i) /
static_cast<float>(segs);
1064 const float t1 =
static_cast<float>(i + 1) /
static_cast<float>(segs);
1065 const V3 a0 = arcPt(r0, t0);
1066 const V3 a1 = arcPt(r0, t1);
1067 const V3 b0 = arcPt(r1, t0);
1068 const V3 b1 = arcPt(r1, t1);
1069 const V3 s0 = arcPt(r2, t0);
1070 const V3 s1 = arcPt(r2, t1);
1072 const V3 c0 = arcPt(std::max(0.05f, r2 - 0.06f), t0);
1073 const V3 c1 = arcPt(std::max(0.05f, r2 - 0.06f), t1);
1074 const V3 b0i = arcPt(r1 - 0.02f, t0);
1075 const V3 b1i = arcPt(r1 - 0.02f, t1);
1076 const V3 b0o = arcPt(r1 + 0.02f, t0);
1077 const V3 b1o = arcPt(r1 + 0.02f, t1);
1078 const V3 inToCorner =
normalize(V3{
cx - a0.x, 0.f, cz - a0.z});
1079 const V3 outToRoad = inToCorner * -1.f;
1081 appendOrientedQuad(
mesh, surfacePoint(a0.x, a0.z, curbH), surfacePoint(b0o.x, b0o.z, curbH),
1082 surfacePoint(b1o.x, b1o.z, curbH), surfacePoint(a1.x, a1.z, curbH), upN, 0.f,
1084 appendOrientedQuad(
mesh, surfacePoint(b0i.x, b0i.z, walkH), surfacePoint(c0.x, c0.z, walkH),
1085 surfacePoint(c1.x, c1.z, walkH), surfacePoint(b1i.x, b1i.z, walkH), upN, 0.f,
1100 std::sort(arms.begin(), arms.end(), [&](
const ArmTip&
a,
const ArmTip&
b) {
1101 return std::atan2(a.origin.z - node.z, a.origin.x - node.x) <
1102 std::atan2(b.origin.z - node.z, b.origin.x - node.x);
1107 float sidewalkWidth;
1108 float sidewalkHeight;
1110 std::vector<V3> rim;
1111 std::vector<V3> rimUps;
1112 std::vector<RimStyle> rimStyles;
1113 rim.reserve(arms.size() * 8);
1114 rimUps.reserve(arms.size() * 8);
1115 rimStyles.reserve(arms.size() * 8);
1116 auto pushRim = [&](V3
point, RimStyle style) {
1118 if (!rim.empty() &&
length(
point - rim.back()) < 1e-4f)
return;
1119 rim.push_back(
point);
1120 rimUps.push_back(junctionPlane.up);
1121 rimStyles.push_back(style);
1123 auto armStyle = [](
const ArmTip&
tip) {
1124 return RimStyle{
tip.curbWidth,
tip.curbHeight,
tip.sidewalkW,
tip.sidewalkH};
1126 auto mixStyle = [](RimStyle
a, RimStyle
b,
float t) {
1127 const float s =
t *
t * (3.f - 2.f *
t);
1128 return RimStyle{
a.curbWidth + (
b.curbWidth -
a.curbWidth) *
s,
1129 a.curbHeight + (
b.curbHeight -
a.curbHeight) *
s,
1130 a.sidewalkWidth + (
b.sidewalkWidth -
a.sidewalkWidth) *
s,
1131 a.sidewalkHeight + (
b.sidewalkHeight -
a.sidewalkHeight) *
s};
1133 for (std::size_t armIndex = 0; armIndex < arms.size(); ++armIndex) {
1134 const auto&
tip = arms[armIndex];
1135 const auto&
next = arms[(armIndex + 1) % arms.size()];
1136 V3 clockwise =
tip.origin -
tip.side *
tip.asphaltHalf;
1137 V3 counterClockwise =
tip.origin +
tip.side *
tip.asphaltHalf;
1138 const RimStyle fromStyle = armStyle(
tip);
1139 const RimStyle toStyle = armStyle(next);
1140 pushRim(clockwise, fromStyle);
1141 pushRim(counterClockwise, fromStyle);
1148 const float denominator =
tip.outward.x *
next.outward.z -
tip.outward.z *
next.outward.x;
1149 if (arms.size() >= 3 && std::fabs(denominator) > 1e-4f) {
1150 const V3 delta =
end - counterClockwise;
1151 const float alongTip = (delta.x *
next.outward.z - delta.z *
next.outward.x) / denominator;
1152 const float alongNext = (delta.x *
tip.outward.z - delta.z *
tip.outward.x) / denominator;
1154 if (alongTip > 0.f && alongNext > 0.f && alongTip < maximumMiter && alongNext < maximumMiter) {
1155 V3 control = counterClockwise +
tip.outward * alongTip;
1156 control.y = planeHeight(junctionPlane, control.x, control.z);
1157 const float angle = std::acos(std::clamp(
dot(
tip.outward,
next.outward), -1.f, 1.f));
1158 const int segments = quadraticSegments(counterClockwise, control,
end,
angle,
1160 for (
int segment = 1; segment < segments; ++segment) {
1161 const float t =
static_cast<float>(segment) /
static_cast<float>(segments);
1162 const float it = 1.f -
t;
1163 const V3
point = counterClockwise * (it * it) + control * (2.f * it *
t) +
end * (
t *
t);
1164 pushRim(
point, mixStyle(fromStyle, toStyle,
t));
1172 const V3 upN = junctionPlane.up;
1173 std::vector<std::array<std::size_t, 3>>
triangles;
1174 if (!triangulateBoundary(rim,
triangles)) {
1182 for (
const auto& arm : arms) {
1183 const RimStyle style = armStyle(arm);
1184 pushRim(arm.origin - arm.side * arm.asphaltHalf, style);
1185 pushRim(arm.origin + arm.side * arm.asphaltHalf, style);
1187 if (!triangulateBoundary(rim,
triangles))
1189 "junction sockets form a self-intersecting boundary",
"junction");
1200 const auto&
a = arms[
first];
1202 if (
dot(
a.authoredOutward,
b.authoredOutward) > -0.999f)
continue;
1203 const V3 lift = upN * 0.01f;
1204 std::array<V3, 4> corners = {
a.origin -
a.side *
a.asphaltHalf + lift,
1205 a.origin +
a.side *
a.asphaltHalf + lift,
1206 b.origin -
b.side *
b.asphaltHalf + lift,
1207 b.origin +
b.side *
b.asphaltHalf + lift};
1209 for (
const auto& corner : corners)
center =
center + corner * 0.25f;
1210 std::sort(corners.begin(), corners.end(), [&](
const V3& lhs,
const V3& rhs) {
1211 return std::atan2(lhs.z - center.z, lhs.x - center.x) <
1212 std::atan2(rhs.z - center.z, rhs.x - center.x);
1214 appendOrientedQuad(
mesh, corners[0], corners[1], corners[2], corners[3], upN, 0.f, 1.f, 0.f, 1.f,
1222 auto sameXZ = [](
const V3&
a,
const V3&
b) {
1223 return std::fabs(
a.x -
b.x) < 1e-3f && std::fabs(
a.z -
b.z) < 1e-3f;
1225 auto isMouth = [&](
const V3&
a,
const V3&
b) {
1226 for (
const auto& arm : arms) {
1227 const V3
left = arm.origin - arm.side * arm.asphaltHalf;
1228 const V3
right = arm.origin + arm.side * arm.asphaltHalf;
1233 const float orientation = signedAreaXZ(rim) >= 0.f ? 1.f : -1.f;
1234 auto edgeOutward = [&](std::size_t i) {
1235 const V3
edge = rim[(i + 1) % rim.size()] - rim[i];
1237 return orientation >= 0.f ? outward : outward * -1.f;
1239 auto offsetRing = [&](
const std::vector<float>& distances) {
1240 std::vector<V3> result;
1241 result.reserve(rim.size());
1242 for (std::size_t i = 0; i < rim.size(); ++i) {
1243 bool alignedToMouth =
false;
1244 for (
const auto& arm : arms) {
1245 const V3
left = arm.origin - arm.side * arm.asphaltHalf;
1246 const V3
right = arm.origin + arm.side * arm.asphaltHalf;
1247 if (sameXZ(rim[i],
left)) {
1248 V3
point = arm.origin - arm.side * (arm.asphaltHalf + distances[i]);
1249 result.push_back(
point);
1250 alignedToMouth =
true;
1253 if (sameXZ(rim[i],
right)) {
1254 V3
point = arm.origin + arm.side * (arm.asphaltHalf + distances[i]);
1255 result.push_back(
point);
1256 alignedToMouth =
true;
1260 if (alignedToMouth)
continue;
1261 const V3
previous = edgeOutward((i + rim.size() - 1) % rim.size());
1262 const V3
next = edgeOutward(i);
1264 const float projection = std::max(0.25f,
dot(miter, next));
1265 result.push_back(rim[i] + miter * (distances[i] / projection));
1269 std::vector<float> curbDistances;
1270 std::vector<float> walkDistances;
1271 curbDistances.reserve(rim.size());
1272 walkDistances.reserve(rim.size());
1273 for (
const auto& style : rimStyles) {
1274 curbDistances.push_back(style.curbWidth);
1275 walkDistances.push_back(style.curbWidth + style.sidewalkWidth);
1277 const auto curbRing = offsetRing(curbDistances);
1278 const auto walkRing = offsetRing(walkDistances);
1279 for (std::size_t i = 0; i < rim.size(); ++i) {
1280 const std::size_t
next = (i + 1) % rim.size();
1281 if (isMouth(rim[i], rim[next]))
continue;
1282 const V3 outward = edgeOutward(i);
1283 const V3 upA = rimUps[i];
1284 const V3 upB = rimUps[
next];
1285 const auto& styleA = rimStyles[i];
1286 const auto& styleB = rimStyles[
next];
1287 const V3 innerA{rim[i].x, rim[i].y, rim[i].z};
1288 const V3 innerB{rim[
next].x, rim[
next].y, rim[
next].z};
1289 const V3 surfaceA = innerA;
1290 const V3 surfaceB = innerB;
1291 const V3 innerTopA = surfaceA + upA * styleA.curbHeight;
1292 const V3 innerTopB = surfaceB + upB * styleB.curbHeight;
1293 const V3 curbTopA = curbRing[i] + upA * styleA.curbHeight;
1294 const V3 curbTopB = curbRing[
next] + upB * styleB.curbHeight;
1295 const V3 curbWalkA = curbRing[i] + upA * styleA.sidewalkHeight;
1296 const V3 curbWalkB = curbRing[
next] + upB * styleB.sidewalkHeight;
1297 const V3 walkA = walkRing[i] + upA * styleA.sidewalkHeight;
1298 const V3 walkB = walkRing[
next] + upB * styleB.sidewalkHeight;
1299 if (styleA.curbWidth > 1e-4f || styleB.curbWidth > 1e-4f) {
1300 appendOrientedQuad(
mesh, innerA, innerB, innerTopB, innerTopA, outward * -1.f, 0.f, 1.f, 0.f, 1.f,
1302 appendOrientedQuad(
mesh, innerTopA, innerTopB, curbTopB, curbTopA, upN, 0.f, 1.f, 0.f, 1.f,
1304 appendOrientedQuad(
mesh, curbWalkA, curbWalkB, curbTopB, curbTopA, outward, 0.f, 1.f, 0.f, 1.f,
1307 if (styleA.sidewalkWidth > 1e-4f || styleB.sidewalkWidth > 1e-4f) {
1308 appendOrientedQuad(
mesh, curbWalkA, curbWalkB, walkB, walkA, upN, 0.f, 1.f, 0.f, 1.f,
1310 appendOrientedQuad(
mesh, walkRing[i], walkRing[next], walkB, walkA, outward, 0.f, 1.f, 0.f, 1.f,
1319 for (
const auto&
edge : network.
edges()) {
1322 if (!
spline.ok())
continue;
1323 auto len =
spline.value().lengthResult(16);
1324 if (!len.ok())
continue;
1325 const float trim = junctionSocketDistance(network,
node,
edge, len.value());
1326 if (len.value() < trim + 1.5f)
continue;
1327 const float alongDist =
edge.
to ==
node.
id ? (len.value() - trim - 1.6f) : (trim + 1.6f);
1328 auto frame =
spline.value().travelFrameResult(alongDist,
"clamp", 16);
1329 if (!frame.ok())
continue;
1330 const auto&
f = frame.value();
1331 const V3
origin{
f.sample.x,
f.sample.y,
f.sample.z};
1332 const V3
side{
f.sideX,
f.sideY,
f.sideZ};
1334 V3 fwd{
f.forwardX,
f.forwardY,
f.forwardZ};
1337 const float stripeW =
1339 for (
int s = 0;
s < 4; ++
s) {
1340 const float along =
static_cast<float>(
s) * 0.5f;
1341 const V3
c =
origin - fwd * along + nrm * 0.04f;
1342 const V3
a =
c +
side * -stripeW;
1343 const V3
b =
c +
side * stripeW;
1344 const V3
d =
a - fwd * 0.25f;
1345 const V3 e =
b - fwd * 0.25f;
1346 appendOrientedQuad(
mesh,
a,
b, e,
d, nrm, 0.f, 1.f, 0.f, 1.f,
RoadMaterial::Marking);
1352Result<void> bakeTurnOverlays(MeshBuild&
mesh, RoadOverlay& overlay,
const RoadNetwork& network,
1353 const RoadBakeOptions&
options,
1354 const std::unordered_set<std::uint32_t>& activeJunctionNodes) {
1355 std::unordered_map<std::uint32_t, SplinePath>
paths;
1356 for (
const auto&
edge : network.
edges()) {
1362 for (
const auto& link : network.laneLinks()) {
1363 auto inEdge = network.edgeResult(link.inEdge);
1364 auto outEdge = network.edgeResult(link.outEdge);
1365 if (!inEdge.ok() || !outEdge.ok())
continue;
1366 const std::uint32_t
nodeId =
1369 if (!
node.ok())
continue;
1371 auto& inPath =
paths[link.inEdge];
1372 auto& outPath =
paths[link.outEdge];
1373 auto inLen = inPath.lengthResult(16);
1374 auto outLen = outPath.lengthResult(16);
1375 if (!inLen.ok() || !outLen.ok())
continue;
1377 const bool activeJunction = activeJunctionNodes.contains(
node.value().
id);
1378 const float inTrim =
1379 activeJunction ? junctionSocketDistance(network,
node.value(), inEdge.value(), inLen.value()) : 0.f;
1380 const float outTrim =
1381 activeJunction ? junctionSocketDistance(network,
node.value(), outEdge.value(), outLen.value()) : 0.f;
1384 auto inFrame = inPath.travelFrameResult(inDist,
"clamp", 16);
1385 auto outFrame = outPath.travelFrameResult(outDist,
"clamp", 16);
1386 if (!inFrame.ok() || !outFrame.ok())
continue;
1387 if (activeJunction) {
1388 const JunctionPlane plane = fitJunctionPlane(network,
node.value());
1389 blendFrameToJunctionPlane(inFrame.value(), plane, 1.f);
1390 blendFrameToJunctionPlane(outFrame.value(), plane, 1.f);
1393 const float inLat = laneCenterOffset(inEdge.value().style, inEdge.value().lanesForward,
1394 inEdge.value().lanesBackward, link.inLane, link.inDirection);
1395 const float outLat = laneCenterOffset(outEdge.value().style, outEdge.value().lanesForward,
1396 outEdge.value().lanesBackward, link.outLane, link.outDirection);
1398 const auto& fi = inFrame.value();
1399 const auto& fo = outFrame.value();
1400 const V3 p0{fi.sample.x, fi.sample.y, fi.sample.z};
1401 const V3 p1{fo.sample.x, fo.sample.y, fo.sample.z};
1402 const V3 si{fi.sideX, fi.sideY, fi.sideZ};
1403 const V3 so{fo.sideX, fo.sideY, fo.sideZ};
1404 const V3 ui{fi.upX, fi.upY, fi.upZ};
1405 const V3 uo{fo.upX, fo.upY, fo.upZ};
1406 V3 ti{fi.forwardX, fi.forwardY, fi.forwardZ};
1407 V3
to{fo.forwardX, fo.forwardY, fo.forwardZ};
1410 const V3
a = p0 + si * inLat + ui * 0.09f;
1411 const V3
d = p1 + so * outLat + uo * 0.09f;
1423 poly.width =
options.navRibbonHalfWidth * 2.f;
1424 poly.inEdge = link.inEdge;
1425 poly.outEdge = link.outEdge;
1426 poly.inLane = link.inLane;
1427 poly.outLane = link.outLane;
1428 poly.inDirection = link.inDirection;
1429 poly.outDirection = link.outDirection;
1430 poly.speedLimitMps = std::min(inEdge.value().style.speedLimitMps, outEdge.value().style.speedLimitMps);
1431 poly.trafficPriority = inEdge.value().style.trafficPriority;
1432 const int samples = std::max(4,
options.turnSamples);
1434 bool hasPrev =
false;
1435 for (
int i = 0; i <= samples; ++i) {
1436 const float t =
static_cast<float>(i) /
static_cast<float>(samples);
1437 const float u = 1.f -
t;
1438 const V3
p =
a * (
u *
u *
u) +
b * (3.f *
u *
u *
t) +
c * (3.f *
u *
t *
t) +
d * (
t *
t *
t);
1439 poly.xyz.push_back(
p.x);
1440 poly.xyz.push_back(
p.y);
1441 poly.xyz.push_back(
p.z);
1443 const V3 delta =
p - prev;
1444 if (
length(delta) > 1e-5f) {
1448 const float hw =
options.navRibbonHalfWidth * 0.9f;
1449 appendStripQuad(
mesh, prev + lat * -hw,
p + lat * -hw,
p + lat * hw, prev + lat * hw,
up,
1453 if (i == samples / 2 &&
length(
d -
a) > 1e-5f) {
1456 const V3 derivative = (
b -
a) * (3.f *
u *
u) + (
c -
b) * (6.f *
u *
t) +
1457 (
d -
c) * (3.f *
t *
t);
1464 overlay.turns.push_back(std::move(poly));
1467 "road navigation exceeds the mesh element budget",
"mesh");
1472Result<void> bakePlacementPoints(PointSet& placements,
const RoadNetwork& network,
const RoadBakeOptions&
options,
1473 const std::unordered_set<std::uint32_t>& activeJunctionNodes) {
1474 if (!std::isfinite(
options.sideObjectSpacing) ||
options.sideObjectSpacing < 0.25f ||
1475 !std::isfinite(
options.sideObjectOffset) ||
options.sideObjectOffset < 0.f ||
options.maximumPlacements < 1)
1477 if (!std::isfinite(
options.sideObjectClearance) ||
options.sideObjectClearance < 0.f)
1479 "placements.clearance");
1482 const RoadEdge*
edge =
nullptr;
1486 float sideStart = 0.f;
1487 float sideEnd = 0.f;
1489 std::unordered_map<std::uint32_t, std::vector<std::uint32_t>> adjacency;
1491 for (
const auto&
edge : network.
edges()) {
1496 std::vector<std::vector<std::uint32_t>> roundabouts;
1497 auto canonicalCycle = [](std::vector<std::uint32_t> cycle) {
1498 const auto minimum = std::min_element(cycle.begin(), cycle.end());
1499 std::rotate(cycle.begin(),
minimum, cycle.end());
1500 std::vector<std::uint32_t> reversed{cycle.front()};
1501 for (
auto it = cycle.rbegin(); it != cycle.rend() - 1; ++it) reversed.push_back(*it);
1502 return reversed < cycle ? reversed : cycle;
1504 for (
const auto&
node : network.
nodes()) {
1505 if (adjacency[
node.
id].size() != 3u)
continue;
1507 std::function<void(std::uint32_t)> visit = [&](std::uint32_t
current) {
1508 for (
const auto next : adjacency[
current]) {
1509 if (next ==
path.front()) {
1510 if (
path.size() < 3u ||
path.size() > 12u)
continue;
1511 auto cycle = canonicalCycle(
path);
1512 if (std::find(roundabouts.begin(), roundabouts.end(), cycle) != roundabouts.end())
continue;
1513 float centerX = 0.f, centerZ = 0.f, minY = std::numeric_limits<float>::max();
1514 float maxY = -std::numeric_limits<float>::max();
1517 for (
const auto id : cycle) {
1518 if (adjacency[
id].
size() < 2u || adjacency[
id].
size() > 3u) {
1522 if (adjacency[
id].
size() == 3u) ++entryCount;
1523 auto value = network.nodeResult(
id);
1528 centerX +=
value.value().x;
1529 centerZ +=
value.value().z;
1530 minY = std::min(minY,
value.value().y);
1531 maxY = std::max(maxY,
value.value().y);
1533 if (!
valid || entryCount < 3 || maxY - minY > 1.f)
continue;
1534 for (std::size_t i = 0; i < cycle.size(); ++i) {
1535 const auto a = cycle[i];
1536 const auto b = cycle[(i + 1u) % cycle.size()];
1537 const auto ringEdge = std::find_if(network.edges().begin(), network.edges().end(),
1538 [&](
const RoadEdge&
edge) {
1539 return (edge.from == a && edge.to == b) ||
1540 (edge.from == b && edge.to == a);
1542 if (ringEdge == network.edges().end() || ringEdge->controlPoints.size() < 3u) {
1547 if (!
valid)
continue;
1548 centerX /=
static_cast<float>(cycle.size());
1549 centerZ /=
static_cast<float>(cycle.size());
1550 float minRadius = std::numeric_limits<float>::max(), maxRadius = 0.f;
1551 for (
const auto id : cycle) {
1552 const auto value = network.nodeResult(
id).value();
1555 minRadius = std::min(minRadius,
radius);
1556 maxRadius = std::max(maxRadius,
radius);
1558 if (minRadius >= 1.f && maxRadius <= minRadius * 1.8f) roundabouts.push_back(std::move(cycle));
1561 if (
path.size() >= 12u || adjacency[next].size() < 2u || adjacency[next].size() > 3u ||
1562 std::find(
path.begin(),
path.end(), next) !=
path.end())
1564 path.push_back(next);
1571 std::sort(roundabouts.begin(), roundabouts.end());
1572 std::unordered_set<std::uint32_t> roundaboutNodes;
1573 for (
const auto& cycle : roundabouts)
1574 for (const auto
id : cycle) roundaboutNodes.insert(
id);
1576 std::vector<EdgeSource> sources;
1577 sources.reserve(network.edges().size());
1579 for (
const auto& cycle : roundabouts) {
1581 for (
const auto id : cycle)
1584 for (
const auto&
node : network.
nodes()) {
1585 if (roundaboutNodes.contains(
node.
id))
continue;
1586 const auto arms =
static_cast<std::size_t
>(std::count_if(network.edges().begin(), network.edges().end(),
1587 [&](
const RoadEdge&
edge) {
1588 return edge.from == node.id || edge.to == node.id;
1590 if (arms >= 3u)
required += arms == 3u ? 2u : 1u;
1592 for (
const auto&
edge : network.
edges()) {
1595 auto lengthResult =
spline.value().lengthResult(32);
1599 const float length = lengthResult.value();
1602 if (activeJunctionNodes.contains(
edge.
from)) {
1605 sideStart = std::max(sideStart, junctionSocketDistance(network,
node.value(),
edge,
length));
1607 if (activeJunctionNodes.contains(
edge.
to)) {
1608 const auto node = network.nodeResult(
edge.
to);
1610 sideEnd = std::min(sideEnd,
length - junctionSocketDistance(network,
node.value(),
edge,
length));
1612 sideEnd = std::max(sideStart, sideEnd);
1613 const float placementLength = sideEnd - sideStart;
1614 const float firstSideDistance =
options.sideObjectSpacing * 0.5f;
1615 const auto sideSamples = placementLength <= firstSideDistance
1617 :
static_cast<std::size_t
>(
1618 std::floor((placementLength - firstSideDistance) /
1623 required += 2u + sideSamples * enabledSides;
1628 {&
edge, std::move(
spline).takeValue(),
length, profile.value().halfWidth, sideStart, sideEnd});
1632 for (
const auto& cycle : roundabouts) {
1633 float centerX = 0.f, centerY = 0.f, centerZ = 0.f;
1634 for (
const auto id : cycle) {
1635 const auto node = network.nodeResult(
id).value();
1640 const float divisor =
static_cast<float>(cycle.size());
1644 const std::uint32_t roundaboutId = cycle.front();
1645 const auto entryCount =
static_cast<std::int64_t
>(
1646 std::count_if(cycle.begin(), cycle.end(), [&](std::uint32_t
id) { return adjacency[id].size() == 3u; }));
1647 const auto pointNamespace = 0x524e444200000000ull | roundaboutId;
1648 auto appendRoundaboutPoint = [&](
const RoadNode&
node, V3
heading,
const char* role,
1649 std::uint64_t ordinal) -> Result<void> {
1651 placements.setNormal(row, 0.f, 1.f, 0.f);
1652 placements.setYaw(row, std::atan2(
heading.x,
heading.z) * 57.2957795f);
1653 placements.setPointSeed(row,
deriveSeed(roundaboutId, std::string(role) + std::to_string(
node.
id)));
1656 status = placements.trySetStringAttribute(row,
"road_role", role);
1658 status = placements.trySetIntAttribute(row,
"road_node_id",
node.
id);
1660 status = placements.trySetIntAttribute(row,
"road_roundabout_id", roundaboutId);
1662 status = placements.trySetIntAttribute(row,
"road_arm_count", entryCount);
1671 auto status = appendRoundaboutPoint(
center, V3{0.f, 0.f, 1.f},
"junction.roundabout.center", 1u);
1673 std::uint64_t entryOrdinal = 2u;
1674 for (std::size_t i = 0; i < cycle.size(); ++i) {
1675 const auto node = network.nodeResult(cycle[i]).value();
1676 if (adjacency[
node.
id].size() != 3u)
continue;
1678 for (
const auto neighbor : adjacency[
node.
id]) {
1679 if (std::find(cycle.begin(), cycle.end(), neighbor) != cycle.end())
continue;
1680 const auto outside = network.nodeResult(neighbor).value();
1689 for (
const auto&
node : network.
nodes()) {
1690 if (roundaboutNodes.contains(
node.
id))
continue;
1692 for (
const auto&
edge : network.
edges()) {
1693 auto appendDirection = [&](
const RoadControlPoint&
point) {
1709 int oppositePairs = 0;
1714 const char* role =
"junction.multi";
1715 if (
directions.size() == 3u) role = oppositePairs > 0 ?
"junction.t" :
"junction.y";
1716 if (
directions.size() == 4u) role = oppositePairs >= 2 ?
"junction.x" :
"junction.multi";
1719 if (
directions.size() == 3u && oppositePairs > 0) {
1720 float leastOpposed = -2.f;
1722 float closest = 1.f;
1724 if (&candidate != &other) closest = std::min(closest,
dot(candidate, other));
1726 if (closest > leastOpposed) {
1727 leastOpposed = closest;
1736 placements.setNormal(row, 0.f, 1.f, 0.f);
1737 placements.setYaw(row, std::atan2(
heading.x,
heading.z) * 57.2957795f);
1741 status = placements.trySetStringAttribute(row,
"road_role", role);
1743 status = placements.trySetIntAttribute(row,
"road_node_id",
node.
id);
1745 status = placements.trySetIntAttribute(row,
"road_arm_count",
static_cast<std::int64_t
>(
directions.size()));
1747 status = placements.trySetIntAttribute(row,
"road_side", 0);
1754 const int islandRow = placements.add(
node.
x +
heading.x * islandDistance,
node.
y,
1756 placements.setNormal(islandRow, 0.f, 1.f, 0.f);
1757 placements.setYaw(islandRow, std::atan2(
heading.x,
heading.z) * 57.2957795f);
1758 constexpr const char* islandRole =
"junction.channelizing.island";
1760 status = placements.trySetPointId(islandRow,
1763 status = placements.trySetStringAttribute(islandRow,
"road_role", islandRole);
1765 status = placements.trySetIntAttribute(islandRow,
"road_node_id",
node.
id);
1767 status = placements.trySetIntAttribute(islandRow,
"road_arm_count", 3);
1769 status = placements.trySetFloatAttribute(islandRow,
"road_distance", islandDistance);
1777 std::vector<V3> acceptedSidePoints;
1778 for (
const auto&
source : sources) {
1779 std::uint64_t ordinal = 1;
1780 const auto pointNamespace = 0x524f414400000000ull |
source.edge->id;
1781 auto append = [&](
float distance,
float lateral,
const char* role,
int side,
1782 bool avoidConflicts) -> Result<bool> {
1783 auto frame =
source.spline.travelFrameResult(
distance,
"clamp", 32);
1785 const auto&
f = frame.
value();
1786 const V3
point{
f.sample.x +
f.sideX * lateral,
f.sample.y +
f.sideY * lateral,
1787 f.sample.z +
f.sideZ * lateral};
1788 if (avoidConflicts) {
1789 for (
const auto& other : sources) {
1790 if (other.edge->id ==
source.edge->id)
continue;
1791 auto closest = other.spline.closestPointResult(
point.x,
point.y,
point.z, 32);
1793 const V3 delta{
point.x - closest.value().x,
point.y - closest.value().y,
1794 point.z - closest.value().z};
1795 if (
length(delta) < other.halfWidth +
options.sideObjectClearance)
1798 for (
const auto& accepted : acceptedSidePoints)
1802 placements.setNormal(row,
f.upX,
f.upY,
f.upZ);
1803 placements.setYaw(row, std::atan2(
f.forwardX,
f.forwardZ) * 57.2957795f);
1804 placements.setPointSeed(row,
deriveSeed(
source.edge->id, std::string(role) + std::to_string(ordinal)));
1805 auto identified = placements.trySetPointId(row,
derivePointId(pointNamespace, ordinal++));
1807 auto edgeId = placements.trySetIntAttribute(row,
"road_edge_id",
source.edge->id);
1809 auto pointRole = placements.trySetStringAttribute(row,
"road_role", role);
1811 auto pointSide = placements.trySetIntAttribute(row,
"road_side",
side);
1813 auto along = placements.trySetFloatAttribute(row,
"road_distance",
distance);
1815 auto clearance = placements.trySetFloatAttribute(row,
"road_clearance",
options.sideObjectClearance);
1817 auto direction = placements.trySetVectorAttribute(row,
"road_direction",
f.forwardX,
f.forwardY,
1820 if (avoidConflicts) acceptedSidePoints.push_back(
point);
1824 auto start = append(0.f, 0.f,
"transition.start", 0,
false);
1826 auto end = append(
source.length, 0.f,
"transition.end", 0,
false);
1828 const float lateral =
source.halfWidth +
options.sideObjectOffset;
1831 if (
source.edge->style.sideObjectsLeft) {
1832 auto left = append(
distance, -lateral,
"side.left", -1,
true);
1835 if (
source.edge->style.sideObjectsRight) {
1836 auto right = append(
distance, lateral,
"side.right", 1,
true);
1852 return junctionSocketDistance(network,
node,
edge, pathLength);
1859struct MaterialColor {
1863MaterialColor colorForGroup(
const std::string&
name) {
1864 if (
name ==
"asphalt")
return {0.28f, 0.28f, 0.30f, 1.f};
1865 if (
name ==
"curb")
return {0.78f, 0.78f, 0.76f, 1.f};
1866 if (
name ==
"sidewalk")
return {0.86f, 0.86f, 0.84f, 1.f};
1867 if (
name ==
"deck")
return {0.48f, 0.48f, 0.46f, 1.f};
1868 if (
name ==
"pier")
return {0.70f, 0.68f, 0.64f, 1.f};
1869 if (
name ==
"marking")
return {0.96f, 0.96f, 0.94f, 1.f};
1870 if (
name ==
"markingYellow")
return {0.96f, 0.80f, 0.10f, 1.f};
1871 if (
name ==
"nav")
return {0.10f, 0.95f, 1.f, 1.f};
1872 return {0.55f, 0.55f, 0.55f, 1.f};
1875Result<void> paintGroupVertexColors(MeshBuild&
mesh) {
1876 const int verts =
mesh.getVertexCount();
1878 std::vector<float>
colors(
static_cast<std::size_t
>(
verts) * 4u, 1.f);
1879 const int triCount =
mesh.getIndexCount() / 3;
1880 for (
int t = 0;
t < triCount; ++
t) {
1882 const MaterialColor
c =
1883 group >= 0 ? colorForGroup(
mesh.getGroupName(
group)) : MaterialColor{0.5f, 0.5f, 0.5f, 1.f};
1884 for (
int k = 0; k < 3; ++k) {
1885 const int vi =
mesh.getIndex(
t * 3 + k);
1886 if (vi < 0 || vi >=
verts)
continue;
1887 const auto base =
static_cast<std::size_t
>(vi) * 4u;
1894 return mesh.setVertexColors(std::move(
colors));
1902 constexpr int maximumSamples = 4096;
1903 if (
options.pathSegmentsPerEdge < 2 ||
options.turnSamples < 2 ||
1904 options.pathSegmentsPerEdge > maximumSamples ||
options.turnSamples > maximumSamples)
1906 "road sample counts must be in [2,4096]",
"options");
1907 if (
options.includeNavigation &&
1908 (!std::isfinite(
options.navRibbonHalfWidth) ||
options.navRibbonHalfWidth <= 0.f ||
1909 !std::isfinite(
options.arrowSpacing) ||
options.arrowSpacing <= 0.f))
1911 "road navigation dimensions must be finite and positive",
"options");
1912 if (!std::isfinite(
options.junctionChordError) ||
options.junctionChordError <= 0.f ||
1913 options.junctionChordError > 1.f)
1915 "road junction chord error must be finite and in (0,1] metres",
"options");
1916 if (
options.maximumMeshElements == 0
u)
1918 "road mesh element budget must be positive",
"options");
1923 std::unordered_set<std::uint32_t> activeJunctionNodes;
1924 std::unordered_set<std::uint32_t> terminalNodes;
1925 std::unordered_map<std::uint32_t, std::vector<const RoadEdge*>> incidentEdges;
1926 incidentEdges.reserve(network.
nodeCount());
1927 for (
const auto&
edge : network.
edges()) {
1931 for (
const auto&
node : network.
nodes()) {
1933 if (
found != incidentEdges.end() &&
found->second.size() == 1) terminalNodes.insert(
node.
id);
1935 if (
options.includeJunctions) {
1936 for (
const auto&
node : network.
nodes()) {
1938 if (
found == incidentEdges.end() ||
found->second.size() < 2)
continue;
1939 activeJunctionNodes.insert(
node.
id);
1940 auto solvable = validateJunctionGeometry(network,
node);
1944 for (
const auto&
edge : network.
edges()) {
1947 auto pathLength =
spline.value().lengthResult(16);
1951 if (!
from.ok() || !
to.ok())
1953 "edge endpoint disappeared during junction preflight",
"edge");
1955 ? junctionSocketDistance(network,
from.value(),
edge, pathLength.value())
1957 const float end = activeJunctionNodes.contains(
edge.
to)
1958 ? junctionSocketDistance(network,
to.value(),
edge, pathLength.value())
1961 if (laneCount >= 6 && pathLength.value() -
start -
end <= 1.001f)
1963 "junction socket trims consume the complete road edge",
"edge");
1965 for (
const auto&
edge : network.
edges()) {
1967 bakeEdgeGeometry(result.
mesh, result.
overlay, network,
edge,
options, activeJunctionNodes, terminalNodes);
1971 "road edges exceed the mesh element budget",
"mesh");
1973 if (
options.includeJunctions) {
1974 for (
const auto&
node : network.
nodes()) {
1975 if (!activeJunctionNodes.contains(
node.
id))
continue;
1980 "road junctions exceed the mesh element budget",
"mesh");
1983 if (
options.includeNavigation) {
1987 if (
options.includePlacements) {
1988 auto placements = bakePlacementPoints(result.
placements, network,
options, activeJunctionNodes);
1992 auto painted = paintGroupVertexColors(result.
mesh);
1996 result.
mesh.
setMeta(
"schema",
"eve.procgen.roadNetwork");