s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=redsnail.roadtool2&take=20
Showing code results for query:
*
(71 total matches found)
Game
library
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// How one side's border tapers at either end of a segment: over the given length from the node its relief (kerb
/// height, verge drop) runs to the share of itself given for that end — nothing when the other border is of another
/// kind, the height the two meet at when it is of the same kind — and its width runs to the width the two meet at,
/// so the two borders meet flush at the node. A zero length is no taper. An end marked as a spill has a country road
/// on the other side, whose gravel is blended onto the border there. A gore end is the nose where this road and
/// another diverge from one edge: the border there grows from nothing as the square of the distance, the way the gap
/// between two roads parting from a common tangent opens up, so the two borders fill the gore between them without
/// crossing.
/// </summary>
/// <param name="StartRelief">
/// The share of the profile's own relief the border has at the start node: 0 flattens it into the road, 1 leaves
/// it as it is, and anything else brings it to a height it shares with the border on the other side (above 1 for
/// the lower of the two).
/// </param>
/// <param name="StartOffset">
/// How far from the start node the taper ends: 0 at a plain pass-through, the arm's trim at a junction, where the
/// corner starts. Everything nearer the node than that — the corner itself — is built in the shape the taper runs
/// to, so the two roads round a corner meet in one cross-section instead of turning into each other on the arc.
/// </param>
public readonly record struct RoadBorderTaper(float StartLength, float StartWidth, float StartRelief, bool StartSpill, float EndLength, float EndWidth, float EndRelief, bool EndSpill, bool StartGore = false, bool EndGore = false, float StartOffset = 0.0f, float EndOffset = 0.0f)
{
/// <summary>
/// The share of the profile's relief in force at a curve distance (1 away from any taper, that end's relief at
/// its node, and beyond its offset), how far along a taper the point is (0 outside one), the width the border is
/// that far towards, and whether that taper's neighbour spills onto it. The two ends' tapers, offsets included,
/// are each held to half the segment, so no point is ever inside both.
/// </summary>
public float ReliefAt(float _Along, float _Length, out float _Towards, out float _TargetWidth, out bool _Spill)
{
float relief = 1.0f;
_Towards = 0.0f;
_TargetWidth = 0.0f;
_Spill = false;
if (StartLength > 0.0f && _Along < StartOffset + StartLength)
{
float progress = Math.Clamp((_Along - StartOffset) / StartLength, 0.0f, 1.0f);
_Towards = Towards(progress, StartGore);
_TargetWidth = StartWidth;
_Spill = StartSpill;
relief = MathX.Lerp(StartRelief, 1.0f, progress);
}
if (EndLength > 0.0f && _Length - _Along < EndOffset + EndLength)
{
float progress = Math.Clamp((_Length - _Along - EndOffset) / EndLength, 0.0f, 1.0f);
float towards = Towards(progress, EndGore);
if (towards > _Towards)
{
_Towards = towards;
_TargetWidth = EndWidth;
_Spill = EndSpill;
relief = MathX.Lerp(EndRelief, 1.0f, progress);
}
}
return relief;
}
/// <summary>
/// How far towards the node's width the border is at a progress along the taper (0 at the node, 1 at its far
/// end): straight along a plain taper, and along a gore the square, so the width is the square of the progress.
/// </summary>
private static float Towards(float _Progress, bool _Gore)
{
return _Gore ? 1.0f - _Progress * _Progress : 1.0f - _Progress;
}
/// <summary>
/// The factor the border's nominal width is scaled by at a curve distance, on its way to the neighbour's.
/// </summary>
public float WidthScaleAt(float _Along, float _Length, float _NominalWidth)
{
ReliefAt(_Along, _Length, out float towards, out float targetWidth, out _);
if (towards <= 0.0f || _NominalWidth <= 0.001f)
return 1.0f;
return (_NominalWidth + (targetWidth - _NominalWidth) * towards) / _NominalWidth;
}
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(StartLength);
_Hash.Add(StartWidth);
_Hash.Add(StartRelief);
_Hash.Add(StartSpill ? 1 : 0);
_Hash.Add(StartGore ? 1 : 0);
_Hash.Add(StartOffset);
_Hash.Add(EndLength);
_Hash.Add(EndWidth);
_Hash.Add(EndRelief);
_Hash.Add(EndSpill ? 1 : 0);
_Hash.Add(EndGore ? 1 : 0);
_Hash.Add(EndOffset);
}
/// <summary>
/// Whether the border tapers at the end a node is at.
/// </summary>
public bool TapersAt(bool _AtStart)
{
return _AtStart ? StartLength > 0.0f : EndLength > 0.0f;
}
}
/// <summary>
/// The tapers of a segment's two borders, looking along the segment.
/// </summary>
public readonly record struct SegmentBorderTapers(RoadBorderTaper Left, RoadBorderTaper Right)
{
public RoadBorderTaper Side(bool _Left)
{
return _Left ? Left : Right;
}
public void AddToHash(ref StableHash _Hash)
{
Left.AddToHash(ref _Hash);
Right.AddToHash(ref _Hash);
}
}
Game
library
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// Generates the road surface of one chunk: a strip of quads between cross-section frames at the rows the chunk is
/// built on (even distances along the segment's curve, thinned where the road type allows it).
/// </summary>
internal static class RoadSurfaceBuilder
{
/// <summary>
/// The two edge points of the road at a distance along the curve, each its own distance out from the centreline
/// there. Junctions use this for the rows they share with the arm chunks, so both sides of the seam are computed
/// the same way.
/// </summary>
public static void CrossSection(RoadCurveTable _Table, float _Distance, in RoadWidth _Width, float _Lift, out Vector3 _Left, out Vector3 _Right)
{
_Table.Frame(_Distance, out Vector3 position, out Vector3 tangent);
position += Vector3.Up * _Lift;
Vector3 right = RoadCurve.RightOf(tangent);
_Left = position - right * _Width.LeftAt(_Distance);
_Right = position + right * _Width.RightAt(_Distance);
}
public static PolygonMesh Build(RoadCurveTable _Table, float _StartDistance, float _EndDistance, in RoadWidth _Width, float _Spacing, in RoadSimplify _Simplify, float _TextureRepeat, float _SurfaceOffset, Material _Material)
{
var mesh = new PolygonMesh();
// The rows the borders and the bridge are built on too, so the pieces meet exactly.
var rows = RoadRows.Build(_Table, _StartDistance, _EndDistance, _Spacing, _Width, _Simplify);
float repeat = MathF.Max(1.0f, _TextureRepeat);
var left = new HalfEdgeMesh.VertexHandle[rows.Count];
var right = new HalfEdgeMesh.VertexHandle[rows.Count];
var uvLeft = new Vector2[rows.Count];
var uvRight = new Vector2[rows.Count];
for (int i = 0; i < rows.Count; i++)
{
float distance = rows[i];
CrossSection(_Table, distance, _Width, _SurfaceOffset, out Vector3 leftPos, out Vector3 rightPos);
var handles = mesh.AddVertices(leftPos, rightPos);
left[i] = handles[0];
right[i] = handles[1];
// U runs across the road from the centreline, V along it, so a tiling material follows the road instead
// of the world, and an edge that moves out takes its texture with it.
float v = distance / repeat;
uvLeft[i] = new Vector2(-_Width.LeftAt(distance) / repeat, v);
uvRight[i] = new Vector2(_Width.RightAt(distance) / repeat, v);
}
// Two triangles rather than a quad: a road that turns as it climbs has cells whose four corners are not
// coplanar, and then it matters exactly how the cell is split, both here and for the paint laid on top.
for (int i = 0; i < rows.Count - 1; i++)
{
MeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i], right[i + 1], uvLeft[i], uvRight[i], uvRight[i + 1]);
MeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i + 1], left[i + 1], uvLeft[i], uvRight[i + 1], uvLeft[i + 1]);
}
return mesh;
}
/// <summary>
/// The height of one cell of road surface, as it is built, over a point in plan: the cell is split from its
/// near left corner to its far right one, so which of the two triangles the point falls on is which side of
/// that diagonal it lies. The triangle's plane is used even for a point just off the cell (paint hanging over
/// the road's edge), which keeps that paint on the surface's own plane.
/// </summary>
public static float CellHeight(Vector3 _NearLeft, Vector3 _NearRight, Vector3 _FarRight, Vector3 _FarLeft, Vector2 _Point)
{
Vector2 diagonal = Flat(_FarRight) - Flat(_NearLeft);
float side = Cross(diagonal, _Point - Flat(_NearLeft));
float nearRightSide = Cross(diagonal, Flat(_NearRight) - Flat(_NearLeft));
return side * nearRightSide >= 0.0f
? PlaneHeight(_NearLeft, _NearRight, _FarRight, _Point)
: PlaneHeight(_NearLeft, _FarRight, _FarLeft, _Point);
}
/// <summary>
/// The height of the plane through three points over a point in plan.
/// </summary>
private static float PlaneHeight(Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _Point)
{
Vector2 a = Flat(_A);
Vector2 b = Flat(_B);
Vector2 c = Flat(_C);
float area = Cross(b - a, c - a);
if (MathF.Abs(area) < 0.0001f)
return _A.z;
float weightA = Cross(b - _Point, c - _Point) / area;
float weightB = Cross(c - _Point, a - _Point) / area;
return weightA * _A.z + weightB * _B.z + (1.0f - weightA - weightB) * _C.z;
}
private static Vector2 Flat(Vector3 _Vector)
{
return new Vector2(_Vector.x, _Vector.y);
}
private static float Cross(Vector2 _A, Vector2 _B)
{
return _A.x * _B.y - _A.y * _B.x;
}
}
Editor
library
using System;
using System.Collections.Generic;
using System.IO;
using Editor;
using Sandbox;
namespace RedSnail.RoadTool2.Editor;
/// <summary>
/// Writes each network's border debug record to Libraries/RoadTool2/debug/borders.txt whenever its border pieces
/// have been rebuilt with the overlay on, so the numbers can be read outside the editor.
/// </summary>
public static class RoadBorderDebugDump
{
private static readonly Dictionary<Guid, int> s_Written = new();
[EditorEvent.Frame]
public static void OnFrame()
{
var scene = SceneEditorSession.Active?.Scene;
if (scene is null)
return;
var network = scene.Get<RoadNetwork>();
if (!network.IsValid() || !network.DebugBorders || network.IsRebuilding)
return;
if (s_Written.TryGetValue(network.Id, out int revision) && revision == network.DebugRevision)
return;
s_Written[network.Id] = network.DebugRevision;
try
{
string path = GetPath(network);
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllText(path, network.BuildDebugRecord());
Log.Info($"Road Tool 2: border debug written to {path}");
}
catch (Exception exception)
{
Log.Warning($"Road Tool 2: could not write the border debug record: {exception.Message}");
}
}
private static string GetPath(RoadNetwork _Network)
{
string root = Project.Current.GetRootPath();
string library = Path.Combine(root, "Libraries", "RoadTool2");
// The library folder may have been renamed; find it by its project file.
if (!Directory.Exists(library))
{
foreach (string directory in Directory.GetDirectories(Path.Combine(root, "Libraries")))
{
if (File.Exists(Path.Combine(directory, "roadtool2.sbproj")))
{
library = directory;
break;
}
}
}
string name = string.Concat(_Network.GameObject.Name.Split(Path.GetInvalidFileNameChars())).Replace(' ', '_');
return Path.Combine(library, "debug", $"borders_{name}.txt");
}
}
Game
library
using System.Text.Json.Serialization;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// What stands along the two edges of a bridge deck.
/// </summary>
public enum BridgeSideStyle
{
/// <summary>
/// Nothing above the deck: its side face is all there is.
/// </summary>
None,
/// <summary>
/// A solid parapet wall.
/// </summary>
Wall,
/// <summary>
/// Posts at intervals with horizontal rails running between them.
/// </summary>
Railing,
/// <summary>
/// A prefab of your own repeated along the edge (a fence panel, a lamp, a cable anchor...).
/// </summary>
Prefab
}
/// <summary>
/// What holds a bridge deck up.
/// </summary>
public enum BridgeSupportStyle
{
None,
/// <summary>
/// Pillars at intervals, from the underside of the deck down into the ground.
/// </summary>
Pillars,
/// <summary>
/// A prefab of your own at intervals under the deck (a pier, a tower...).
/// </summary>
Prefab
}
public enum BridgePillarShape
{
Square,
Round
}
/// <summary>
/// One pillar under the middle of the deck, or one under each edge.
/// </summary>
public enum BridgePillarLayout
{
Single,
Pair
}
/// <summary>
/// How a stretch of road becomes a bridge: the deck under it, what stands along the deck's edges, and what holds it
/// up. Each part is picked from a set of built-in designs or handed to a prefab of your own, so new kinds of bridge
/// (cable-stayed, trusses...) are new designs of a part rather than a new kind of road. Roads reference a style, so
/// every bridge built from it changes together when the asset is edited.
/// </summary>
[AssetType(Name = "Bridge Style", Extension = "bridg", Category = "Road Tool 2")]
public sealed class BridgeStyle : GameResource
{
private const string DefaultMaterial = "roadtool2/materials/concrete.vmat";
/// <summary>
/// Material of the deck's sides and underside (and of the walls and pillars when they have none of their own).
/// </summary>
[Group("Deck")] public Material DeckMaterial { get; set; }
[Group("Deck"), Range(10.0f, 10000.0f)] public float DeckTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 500.0f;
/// <summary>
/// The deck's thickness below the road surface.
/// </summary>
[Group("Deck"), Range(5.0f, 500.0f)] public float DeckDepth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 60.0f;
/// <summary>
/// A bare ledge of deck outside the side (wall, railing) on each edge. 0 ends the deck at the side's outer face.
/// </summary>
[Group("Deck"), Range(0.0f, 300.0f)] public float DeckOverhang { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 0.0f;
[Group("Side")] public BridgeSideStyle Side { get; set; } = BridgeSideStyle.Wall;
/// <summary>
/// Material of the wall or the railing. Empty uses the deck's.
/// </summary>
[Group("Side"), ShowIf(nameof(HasBuiltSide), true)] public Material SideMaterial { get; set; }
[Group("Side"), ShowIf(nameof(HasBuiltSide), true), Range(10.0f, 10000.0f)] public float SideTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 100.0f;
/// <summary>
/// The wall's thickness, or the railing's post size; also the width of the strip a prefab side stands on.
/// </summary>
[Group("Side"), HideIf(nameof(Side), BridgeSideStyle.None), Range(1.0f, 200.0f)] public float SideWidth { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 20.0f;
/// <summary>
/// Height of the wall or railing above the road's border (the pavement top when there is one).
/// </summary>
[Group("Side"), ShowIf(nameof(HasBuiltSide), true), Range(5.0f, 300.0f)] public float SideHeight { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 45.0f;
[Group("Railing"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(50.0f, 1000.0f)] public float PostSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;
[Group("Railing"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1, 6)] public int RailCount { get; set { field = value.Clamp(1, 32); } } = 2;
[Group("Railing"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1.0f, 50.0f)] public float RailSize { get; set { field = value.Clamp(0.1f, 1000.0f); } } = 6.0f;
/// <summary>
/// Placed on the deck's edge strip with its forward along the road and its right pointing away from the road.
/// </summary>
[Group("Side"), ShowIf(nameof(Side), BridgeSideStyle.Prefab)] public GameObject SidePrefab { get; set; }
[Group("Side"), ShowIf(nameof(Side), BridgeSideStyle.Prefab), Range(10.0f, 2000.0f)] public float SideSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;
[Group("Supports")] public BridgeSupportStyle Supports { get; set; } = BridgeSupportStyle.Pillars;
[Group("Supports"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(200.0f, 10000.0f)] public float SupportSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1500.0f;
/// <summary>
/// The pillars' material. Empty uses the deck's.
/// </summary>
[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public Material SupportMaterial { get; set; }
[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarShape PillarShape { get; set; } = BridgePillarShape.Square;
[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarLayout PillarLayout { get; set; } = BridgePillarLayout.Single;
[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(10.0f, 1000.0f)] public float PillarSize { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 100.0f;
/// <summary>
/// For a pair of pillars: how far in from the deck's edges they stand.
/// </summary>
[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(0.0f, 500.0f)] public float PillarInset { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 20.0f;
[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(3, 32)] public int PillarSegments { get; set { field = value.Clamp(3, 64); } } = 12;
/// <summary>
/// How far a pillar carries on below the ground it lands on, so it never floats off a slope.
/// </summary>
[Group("Supports"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(0.0f, 500.0f)] public float SupportSink { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 50.0f;
/// <summary>
/// How far below the deck a support reaches when there is no ground under it to land on.
/// </summary>
[Group("Supports"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(100.0f, 10000.0f)] public float SupportFallbackHeight { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1000.0f;
/// <summary>
/// Placed on the ground under the middle of the deck (or the fallback height below it), with its forward along
/// the road. It is up to the prefab to reach the deck.
/// </summary>
[Group("Supports"), ShowIf(nameof(Supports), BridgeSupportStyle.Prefab)] public GameObject SupportPrefab { get; set; }
[Hide, JsonIgnore] public bool HasBuiltSide => Side is BridgeSideStyle.Wall or BridgeSideStyle.Railing;
[Hide, JsonIgnore] public Material ResolvedDeckMaterial => DeckMaterial ?? Material.Load(DefaultMaterial);
[Hide, JsonIgnore] public Material ResolvedSideMaterial => SideMaterial ?? ResolvedDeckMaterial;
[Hide, JsonIgnore] public Material ResolvedSupportMaterial => SupportMaterial ?? ResolvedDeckMaterial;
/// <summary>
/// How wide the strip along each deck edge is that the side occupies (nothing for a bare deck).
/// </summary>
[Hide, JsonIgnore] public float SideStripWidth => Side == BridgeSideStyle.None ? 0.0f : SideWidth;
/// <summary>
/// Folds everything the bridge geometry is built from into a hash, so pieces built with an older version of
/// the style are noticed.
/// </summary>
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(DeckMaterial?.ResourcePath);
_Hash.Add(DeckTextureRepeat);
_Hash.Add(DeckDepth);
_Hash.Add(DeckOverhang);
_Hash.Add((int)Side);
_Hash.Add(SideMaterial?.ResourcePath);
_Hash.Add(SideTextureRepeat);
_Hash.Add(SideWidth);
_Hash.Add(SideHeight);
_Hash.Add(PostSpacing);
_Hash.Add(RailCount);
_Hash.Add(RailSize);
_Hash.Add(RoadPropSet.PrefabKey(SidePrefab));
_Hash.Add(SideSpacing);
_Hash.Add((int)Supports);
_Hash.Add(SupportSpacing);
_Hash.Add(SupportMaterial?.ResourcePath);
_Hash.Add((int)PillarShape);
_Hash.Add((int)PillarLayout);
_Hash.Add(PillarSize);
_Hash.Add(PillarInset);
_Hash.Add(PillarSegments);
_Hash.Add(SupportSink);
_Hash.Add(SupportFallbackHeight);
_Hash.Add(RoadPropSet.PrefabKey(SupportPrefab));
}
/// <summary>
/// The asset was saved: every network with a bridge in this style has stale pieces now.
/// </summary>
protected override void PostReload()
{
base.PostReload();
RoadNetwork.Editor_MarkDirty();
}
protected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)
{
return CreateSimpleAssetTypeIcon("architecture", _Width, _Height, "#7fb2ff", "black");
}
}
Game
library
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.RoadTool2;
public static class MeshUtility
{
public static HalfEdgeMesh.VertexHandle GetOrAddVertex(PolygonMesh _Mesh, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _Pos)
{
if (!_Cache.TryGetValue(_Pos, out var handle))
{
handle = _Mesh.AddVertices(_Pos)[0];
_Cache[_Pos] = handle;
}
return handle;
}
/// <summary>
/// Adds a quad face; returns an invalid handle when the mesh refused it (a duplicate vertex, a topology clash).
/// </summary>
public static HalfEdgeMesh.FaceHandle AddTexturedQuad(PolygonMesh _Mesh, Material _Material,
HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,
HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D,
Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)
{
if (HasDuplicateVertex(_A, _B, _C, _D))
return default;
var face = _Mesh.AddFace(_A, _B, _C, _D);
if (!face.IsValid)
return default;
_Mesh.SetFaceMaterial(face, _Material);
_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC, _UvD });
return face;
}
/// <summary>
/// Adds a triangle face; returns an invalid handle when the mesh refused it.
/// </summary>
public static HalfEdgeMesh.FaceHandle AddTexturedTriangle(PolygonMesh _Mesh, Material _Material,
HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,
HalfEdgeMesh.VertexHandle _C,
Vector2 _UvA, Vector2 _UvB, Vector2 _UvC)
{
if (HasDuplicateVertex(_A, _B, _C))
return default;
var face = _Mesh.AddFace(_A, _B, _C);
if (!face.IsValid)
return default;
_Mesh.SetFaceMaterial(face, _Material);
_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC });
return face;
}
/// <summary>
/// Paints a face's vertices with blend weights (what a blendable material mixes its layers by), in the order
/// the face's vertices were given.
/// </summary>
public static void SetFaceBlends(PolygonMesh _Mesh, HalfEdgeMesh.FaceHandle _Face, IReadOnlyList<Color32> _Blends)
{
if (!_Face.IsValid || !_Mesh.GetFaceVerticesConnectedToFace(_Face, out var edges))
return;
int count = System.Math.Min(edges.Length, _Blends.Count);
for (int i = 0; i < count; i++)
_Mesh.SetVertexBlend(edges[i], _Blends[i]);
}
private static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C)
{
return _A.Equals(_B) || _A.Equals(_C) || _B.Equals(_C);
}
private static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D)
{
return _A.Equals(_B) || _A.Equals(_C) || _A.Equals(_D) ||
_B.Equals(_C) || _B.Equals(_D) ||
_C.Equals(_D);
}
}
Game
library
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// Arc-length table over a <see cref="RoadCurve"/>, so the road can be sampled at even distances (mesh rows, chunk
/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a
/// polyline for picking.
/// </summary>
public sealed class RoadCurveTable
{
private readonly Vector3[] m_Positions;
private readonly float[] m_Distances;
public RoadCurve Curve { get; }
public float Length => m_Distances[^1];
public int SampleCount => m_Positions.Length;
/// <summary>
/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road
/// altogether when the cursor's ray misses it, which is most of them in a city.
/// </summary>
public BBox Bounds { get; }
public RoadCurveTable(RoadCurve _Curve, int _Samples)
{
Curve = _Curve;
int count = Math.Max(1, _Samples);
m_Positions = new Vector3[count + 1];
m_Distances = new float[count + 1];
m_Positions[0] = _Curve.P0;
var bounds = new BBox(_Curve.P0, _Curve.P0);
for (int i = 1; i <= count; i++)
{
m_Positions[i] = _Curve.Evaluate((float)i / count);
m_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);
bounds = bounds.AddPoint(m_Positions[i]);
}
Bounds = bounds;
}
/// <summary>
/// Builds a table with a sample density that suits the curve's length.
/// </summary>
public static RoadCurveTable Create(RoadCurve _Curve)
{
int samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);
return new RoadCurveTable(_Curve, samples);
}
public Vector3 SamplePosition(int _Index)
{
return m_Positions[_Index];
}
public float SampleDistance(int _Index)
{
return m_Distances[_Index];
}
public float DistanceToT(float _Distance)
{
if (_Distance <= 0.0f)
return 0.0f;
if (_Distance >= Length)
return 1.0f;
int low = 0;
int high = m_Distances.Length - 1;
while (high - low > 1)
{
int mid = (low + high) / 2;
if (m_Distances[mid] <= _Distance)
low = mid;
else
high = mid;
}
float span = m_Distances[high] - m_Distances[low];
float fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;
return (low + fraction) / (m_Distances.Length - 1);
}
public Vector3 PositionAt(float _Distance)
{
return Curve.Evaluate(DistanceToT(_Distance));
}
public Vector3 TangentAt(float _Distance)
{
return Curve.Tangent(DistanceToT(_Distance));
}
/// <summary>
/// Where the road is at a distance and which way it runs there, in one go: everything that lays something
/// across the road wants both, and asking for them separately searches the table twice.
/// </summary>
public void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)
{
float t = DistanceToT(_Distance);
_Position = Curve.Evaluate(t);
_Tangent = Curve.Tangent(t);
}
}
Game
library
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the
/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.
/// </summary>
internal static class RoadJunctionBuilder
{
public static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)
{
var mesh = new PolygonMesh();
var polygon = _Layout.Polygon;
if (polygon.Count < 3)
return mesh;
var centre = mesh.AddVertices(_Layout.Centre)[0];
var ring = mesh.AddVertices(polygon.ToArray());
Vector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);
for (int i = 0; i < polygon.Count; i++)
{
int next = (i + 1) % polygon.Count;
MeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],
centreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));
}
return mesh;
}
/// <summary>
/// Junctions have no single direction to run a texture along, so they use a top-down projection.
/// </summary>
private static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)
{
return new Vector2(_Position.x, _Position.y) / _TextureRepeat;
}
}
Game
library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Road Tool" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "roadtool2" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "redsnail" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "redsnail.roadtool2" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "30" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-10-18T00:54:30.0847204Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.166.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.166.0")]
Game
library
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// Arc-length table over a <see cref="RoadCurve"/>, so the road can be sampled at even distances (mesh rows, chunk
/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a
/// polyline for picking.
/// </summary>
public sealed class RoadCurveTable
{
private readonly Vector3[] m_Positions;
private readonly float[] m_Distances;
public RoadCurve Curve { get; }
public float Length => m_Distances[^1];
public int SampleCount => m_Positions.Length;
/// <summary>
/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road
/// altogether when the cursor's ray misses it, which is most of them in a city.
/// </summary>
public BBox Bounds { get; }
public RoadCurveTable(RoadCurve _Curve, int _Samples)
{
Curve = _Curve;
int count = Math.Max(1, _Samples);
m_Positions = new Vector3[count + 1];
m_Distances = new float[count + 1];
m_Positions[0] = _Curve.P0;
var bounds = new BBox(_Curve.P0, _Curve.P0);
for (int i = 1; i <= count; i++)
{
m_Positions[i] = _Curve.Evaluate((float)i / count);
m_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);
bounds = bounds.AddPoint(m_Positions[i]);
}
Bounds = bounds;
}
/// <summary>
/// Builds a table with a sample density that suits the curve's length.
/// </summary>
public static RoadCurveTable Create(RoadCurve _Curve)
{
int samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);
return new RoadCurveTable(_Curve, samples);
}
public Vector3 SamplePosition(int _Index)
{
return m_Positions[_Index];
}
public float SampleDistance(int _Index)
{
return m_Distances[_Index];
}
public float DistanceToT(float _Distance)
{
if (_Distance <= 0.0f)
return 0.0f;
if (_Distance >= Length)
return 1.0f;
int low = 0;
int high = m_Distances.Length - 1;
while (high - low > 1)
{
int mid = (low + high) / 2;
if (m_Distances[mid] <= _Distance)
low = mid;
else
high = mid;
}
float span = m_Distances[high] - m_Distances[low];
float fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;
return (low + fraction) / (m_Distances.Length - 1);
}
public Vector3 PositionAt(float _Distance)
{
return Curve.Evaluate(DistanceToT(_Distance));
}
public Vector3 TangentAt(float _Distance)
{
return Curve.Tangent(DistanceToT(_Distance));
}
/// <summary>
/// Where the road is at a distance and which way it runs there, in one go: everything that lays something
/// across the road wants both, and asking for them separately searches the table twice.
/// </summary>
public void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)
{
float t = DistanceToT(_Distance);
_Position = Curve.Evaluate(t);
_Tangent = Curve.Tangent(t);
}
}
Game
library
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// How far a road's two edges sit from its centreline along a segment: half the road's width on each side, plus
/// whatever extra each side has at the segment's start and end, blended smoothly between the two. That is what lets
/// a lane appear on one side over a taper (a highway exit or entry), a ramp start as a point and grow, or a road
/// flare out to a wider one.
/// </summary>
public readonly struct RoadWidth
{
/// <summary>
/// No road is built narrower than this: two edges on one point make faces with no area.
/// </summary>
private const float MinWidth = 2.0f;
public readonly float Base;
public readonly float LeftStart;
public readonly float LeftEnd;
public readonly float RightStart;
public readonly float RightEnd;
public readonly float Length;
public RoadWidth(float _Base, float _LeftStart, float _LeftEnd, float _RightStart, float _RightEnd, float _Length)
{
Base = _Base;
LeftStart = _LeftStart;
LeftEnd = _LeftEnd;
RightStart = _RightStart;
RightEnd = _RightEnd;
Length = MathF.Max(0.001f, _Length);
}
public RoadWidth(RoadSegment _Segment, float _Length)
: this(_Segment.Width, _Segment.LeftExtraStart, _Segment.LeftExtraEnd, _Segment.RightExtraStart, _Segment.RightExtraEnd, _Length)
{
}
/// <summary>
/// A plain road of one width all along.
/// </summary>
public static RoadWidth Uniform(float _Width)
{
return new RoadWidth(_Width, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f);
}
/// <summary>
/// True when both edges sit at half the width all along: the road every piece of the tool was first built for.
/// </summary>
public bool IsUniform => LeftStart == 0.0f && LeftEnd == 0.0f && RightStart == 0.0f && RightEnd == 0.0f;
/// <summary>
/// The furthest either edge gets from the centreline anywhere along the segment.
/// </summary>
public float MaxHalf => MathF.Max(MathF.Max(Base * 0.5f + LeftStart, Base * 0.5f + LeftEnd), MathF.Max(Base * 0.5f + RightStart, Base * 0.5f + RightEnd));
/// <summary>
/// Distance of the left edge from the centreline at a curve distance, looking along the segment.
/// </summary>
public float LeftAt(float _Distance)
{
float left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);
float right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);
return left + right < MinWidth ? left + (MinWidth - left - right) * 0.5f : left;
}
public float RightAt(float _Distance)
{
float left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);
float right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);
return left + right < MinWidth ? right + (MinWidth - left - right) * 0.5f : right;
}
/// <summary>
/// The road's full width at a curve distance.
/// </summary>
public float WidthAt(float _Distance)
{
return LeftAt(_Distance) + RightAt(_Distance);
}
/// <summary>
/// How much wider than the base half width the left edge is at a curve distance (negative when narrower).
/// </summary>
public float LeftExtraAt(float _Distance)
{
return LeftAt(_Distance) - Base * 0.5f;
}
public float RightExtraAt(float _Distance)
{
return RightAt(_Distance) - Base * 0.5f;
}
private float Blend(float _Start, float _End, float _Distance)
{
if (_Start == _End)
return _Start;
float t = Math.Clamp(_Distance / Length, 0.0f, 1.0f);
// Eased, so the edge leaves and arrives without a kink.
t = t * t * (3.0f - 2.0f * t);
return _Start + (_End - _Start) * t;
}
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(Base);
_Hash.Add(LeftStart);
_Hash.Add(LeftEnd);
_Hash.Add(RightStart);
_Hash.Add(RightEnd);
}
}
Game
library
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// 64-bit FNV-1a hash that is stable across editor sessions, unlike <see cref="HashCode"/> which is seeded per
/// process. Used to stamp generated meshes with the inputs they were built from, so a scene reopened days later
/// still knows which chunks are current.
/// </summary>
public struct StableHash
{
private const ulong OffsetBasis = 14695981039346656037UL;
private const ulong Prime = 1099511628211UL;
private ulong m_Hash;
public long Value => unchecked((long)m_Hash);
public static StableHash Create()
{
return new StableHash { m_Hash = OffsetBasis };
}
public void Add(int _Value)
{
unchecked
{
for (int i = 0; i < 4; i++)
{
m_Hash ^= (byte)(_Value >> (i * 8));
m_Hash *= Prime;
}
}
}
public void Add(long _Value)
{
Add(unchecked((int)_Value));
Add(unchecked((int)(_Value >> 32)));
}
public void Add(float _Value)
{
Add(BitConverter.SingleToInt32Bits(_Value));
}
public void Add(Vector3 _Value)
{
Add(_Value.x);
Add(_Value.y);
Add(_Value.z);
}
public void Add(string _Value)
{
if (_Value is not null)
{
foreach (char c in _Value)
Add((int)c);
}
// Terminator, so "ab" + "c" never collides with "a" + "bc".
Add(-1);
}
}
Game
library
using Sandbox;
namespace RedSnail.RoadTool2;
public enum RoadChunkKind
{
/// <summary>
/// The road surface itself.
/// </summary>
Surface,
/// <summary>
/// What runs along the road's sides: sidewalks, verges.
/// </summary>
Border,
/// <summary>
/// The painted lines on the surface: lane lines, centre line, edge lines.
/// </summary>
Marking,
/// <summary>
/// The bridge under a road: deck, sides, supports.
/// </summary>
Bridge,
/// <summary>
/// The prefabs standing along a road and before its junctions.
/// </summary>
Props,
/// <summary>
/// The rounded end of a road that stops: surface and border together, in one piece per end.
/// </summary>
Cap
}
/// <summary>
/// Marks a generated mesh child of a <see cref="RoadNetwork"/> and remembers what it covers: a stretch of a segment,
/// or the junction at a node, and whether it is the surface or the border of it. The network uses it to replace
/// exactly the pieces that went stale and leave the rest alone.
/// </summary>
[Icon("view_week")]
public sealed class RoadChunk : Component
{
/// <summary>
/// The segment this chunk is a stretch of, or -1 for a junction piece.
/// </summary>
[Property, ReadOnly] public int SegmentId { get; set; } = -1;
/// <summary>
/// The node this chunk is the junction of, or -1 for a road stretch.
/// </summary>
[Property, ReadOnly] public int NodeId { get; set; } = -1;
/// <summary>
/// The parking lot this chunk is a piece of, or -1 for anything else.
/// </summary>
[Property, ReadOnly] public int LotId { get; set; } = -1;
[Property, ReadOnly] public RoadChunkKind Kind { get; set; } = RoadChunkKind.Surface;
[Property, ReadOnly] public int ChunkIndex { get; set; }
[Property, ReadOnly] public int ChunkCount { get; set; }
/// <summary>
/// A locked chunk is never regenerated, so its mesh can be edited by hand. It keeps its geometry even if the road
/// under it changes; unlock it to have it rebuilt.
/// </summary>
[Property]
public bool Locked
{
get;
set
{
field = value;
// Unlocking is a request to catch up with the graph.
GameObject?.Parent?.Components.Get<RoadNetwork>()?.MarkDirty();
}
}
/// <summary>
/// Hash of everything this chunk was generated from. When it no longer matches what the graph says it should be
/// the chunk is stale.
/// </summary>
[Property, Hide] public long Signature { get; set; }
/// <summary>
/// The rows a border piece was built from, when the network's debug overlay is on. Not saved.
/// </summary>
public BorderDebug Debug { get; set; }
public bool IsJunction => NodeId >= 0;
public bool IsLot => LotId >= 0;
protected override void DrawGizmos()
{
if (Debug is null)
return;
var network = GameObject?.Parent?.Components.Get<RoadNetwork>();
if (network is null || !network.DebugBorders)
return;
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.LineThickness = 2.0f;
foreach (var line in Debug.Lines)
{
Gizmo.Draw.Color = line.Color;
Gizmo.Draw.Line(line.From, line.To);
}
foreach (var marker in Debug.Markers)
{
Gizmo.Draw.Color = marker.Color;
Gizmo.Draw.SolidSphere(marker.Position, 2.0f);
}
}
}
Game
library
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace RedSnail.RoadTool2;
public sealed partial class RoadNetwork
{
/// <summary>
/// Bump whenever the generated geometry changes (UV layout, sampling, profile...), so pieces built by an older
/// generator are replaced on the next reconcile instead of lingering until their road is edited.
/// </summary>
private const int GeneratorVersion = 57;
/// <summary>
/// A stretch of road shorter than this is not worth a chunk (it happens when a junction swallows a whole chunk).
/// </summary>
private const float MinChunkLength = 1.0f;
/// <summary>
/// Where a segment's painted lines may run: between <see cref="Start"/> and <see cref="End"/>, skipping the
/// gap between <see cref="GapStart"/> and <see cref="GapEnd"/> when there is one (a crossing in the middle of
/// a short road between two junctions).
/// </summary>
private readonly record struct LineRange(float Start, float End, float GapStart, float GapEnd)
{
public bool HasGap => GapEnd > GapStart;
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(Start);
_Hash.Add(End);
_Hash.Add(GapStart);
_Hash.Add(GapEnd);
}
}
/// <summary>
/// A segment's pedestrian crossings: at either end, or one shared crossing halfway along a short road between
/// two junctions (recorded on the start end). Centres are curve distances from the segment's start.
/// </summary>
private readonly record struct SegmentCrossings(bool AtStart, float StartCentre, bool AtEnd, float EndCentre);
/// <summary>
/// One piece of a segment to build. <see cref="Lines"/> bounds the painted lines; <see cref="Taper"/> is the
/// border's run-out into a different road type at either end; <see cref="Bridge"/> is the whole built range
/// and the ends of a bridge piece; the other kinds ignore them.
/// </summary>
private readonly record struct ChunkPlan(int Index, int Count, float Start, float End, RoadChunkKind Kind, BorderCaps Caps, RoadBorderBuilder.SegmentBorderBlends Blends, long Signature, LineRange Lines = default, SegmentBorderTapers Taper = default, RoadBridgeBuilder.BridgeSpan Bridge = default, RoadPropBuilder.PropSpan Props = default, RoadMarkingBuilder.DashPhase Dash = default, SegmentCrossings Crossings = default);
private readonly record struct PendingGroup(long Signature, int Remaining);
/// <summary>
/// One piece to (re)build: a chunk of a segment, or, when <see cref="NodeId"/> is set, a piece of a junction.
/// <see cref="ArmBlends"/> and <see cref="ArmTapers"/> carry, for a junction's border piece, what each arm's
/// chunks were built with, keyed by segment.
/// </summary>
private readonly record struct RebuildJob(int SegmentId, int NodeId, RoadChunkKind Kind, long GroupSignature, ChunkPlan Plan, RoadCurveTable Table, RoadWidth Width, RoadProfile Profile, RoadJunctionLayout Junction, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> ArmBlends, Dictionary<int, SegmentBorderTapers> ArmTapers, RoadLot Lot);
private bool m_IsDirty;
private readonly Queue<RebuildJob> m_Jobs = new();
private readonly Dictionary<int, PendingGroup> m_PendingSegments = new();
private readonly Dictionary<(int NodeId, RoadChunkKind Kind), PendingGroup> m_PendingJunctions = new();
/// <summary>
/// Junction layouts keyed by node, each with a hash of what it was computed from. Finding corners on curved
/// edges is the one expensive step of a reconcile, and while a node is dragged only its own junction and its
/// neighbours' change.
/// </summary>
private readonly Dictionary<int, (long Key, RoadJunctionLayout Layout)> m_JunctionCache = new();
/// <summary>
/// True while pieces are still queued for (re)generation.
/// </summary>
public bool IsRebuilding => m_Jobs.Count > 0;
/// <summary>
/// Asks for a reconcile pass on the next editor frame: the graph is compared against the pieces that exist and
/// only what differs gets queued for regeneration.
/// </summary>
public void MarkDirty()
{
m_IsDirty = true;
// Undo, reloads and the inspector all land here with a graph nobody told us about piece by piece.
InvalidateIndex();
}
/// <summary>
/// Asks the network in the editor's scene for a reconcile. Editing a road type, a bridge, a prop set or a
/// parking layout changes roads that hold no copy of it, so the asset tells the network rather than the other
/// way round.
/// </summary>
public static void Editor_MarkDirty()
{
var scene = Application.Editor?.Scene;
if (!scene.IsValid())
return;
var network = scene.Get<RoadNetwork>();
if (!network.IsValid())
return;
network.MarkDirty();
}
/// <summary>
/// One reconcile pass, then as much of the queued work as this frame's budget allows. Driven from the
/// component's own update in RoadNetwork.cs.
/// </summary>
private void Tick()
{
if (m_IsDirty)
{
m_IsDirty = false;
Reconcile();
}
ProcessJobs();
}
public IEnumerable<RoadChunk> GetChunks()
{
foreach (var child in GameObject.Children)
{
if (!child.IsValid() || child.IsDestroyed)
continue;
var chunk = child.Components.Get<RoadChunk>();
if (chunk is not null)
yield return chunk;
}
}
private RoadChunk FindChunk(int _SegmentId, int _ChunkIndex, RoadChunkKind _Kind)
{
return GetChunks().FirstOrDefault(chunk => !chunk.IsJunction && chunk.SegmentId == _SegmentId && chunk.ChunkIndex == _ChunkIndex && chunk.Kind == _Kind);
}
private RoadChunk FindJunctionChunk(int _NodeId, RoadChunkKind _Kind)
{
return GetChunks().FirstOrDefault(chunk => chunk.IsJunction && chunk.NodeId == _NodeId && chunk.Kind == _Kind);
}
/// <summary>
/// Compares the graph with the pieces under this GameObject and queues work for everything that is missing,
/// stale or built from different settings. Stateless on purpose: it copes with undo, reloads, hand deleted
/// pieces and hot reloads alike, because the pieces themselves carry what they were built from.
/// </summary>
private void Reconcile()
{
PruneCurveTables();
// A lot's entrances belong to its edge, wherever the lot has been moved to since.
SnapAllOpenings();
var segmentChunks = new Dictionary<int, List<RoadChunk>>();
var junctionChunks = new Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk>();
var lotChunks = new Dictionary<(int LotId, RoadChunkKind Kind), RoadChunk>();
foreach (var chunk in GetChunks())
{
if (chunk.IsLot)
{
if (lotChunks.TryGetValue((chunk.LotId, chunk.Kind), out var duplicate))
duplicate.GameObject.Destroy();
lotChunks[(chunk.LotId, chunk.Kind)] = chunk;
continue;
}
if (chunk.IsJunction)
{
// Two pieces for one junction can only be leftovers; keep one, the reconcile sorts out whether it is current.
if (junctionChunks.TryGetValue((chunk.NodeId, chunk.Kind), out var duplicate))
duplicate.GameObject.Destroy();
junctionChunks[(chunk.NodeId, chunk.Kind)] = chunk;
continue;
}
if (!segmentChunks.TryGetValue(chunk.SegmentId, out var list))
{
list = new List<RoadChunk>();
segmentChunks[chunk.SegmentId] = list;
}
list.Add(chunk);
}
var layouts = ComputeJunctionLayouts();
var dashPhases = ComputeDashPhases();
var liveSegments = new HashSet<int>();
var segmentBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();
var segmentTapers = new Dictionary<int, SegmentBorderTapers>();
foreach (var segment in Segments)
{
var table = GetCurveTable(segment);
if (table is null)
continue;
liveSegments.Add(segment.Id);
var profile = ProfileOf(segment);
// A road opening into a parking lot starts where the lot's surface stops covering it.
float trimStart = MathF.Max(layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.StartNodeId));
float trimEnd = MathF.Max(layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.EndNodeId));
var caps = ComputeBorderCaps(segment, layouts);
var blends = ComputeBorderBlends(segment, table, layouts);
var taper = ComputeBorderTaper(segment, table, layouts, trimStart, trimEnd);
var crossings = ComputeCrossings(segment, table, layouts, trimStart, trimEnd);
var lines = ComputeLineRange(table, profile, trimStart, trimEnd, crossings);
var dash = dashPhases.GetValueOrDefault(segment.Id);
var plans = PlanChunks(segment, table, trimStart, trimEnd, profile, caps, blends, taper, ComputeSurfaceSignature(segment, table.Curve, profile), lines, dash, crossings);
long groupSignature = CombineSignatures(plans);
segmentBlends[segment.Id] = blends;
segmentTapers[segment.Id] = taper;
segmentChunks.TryGetValue(segment.Id, out var existing);
if (IsSegmentUpToDate(existing, plans))
continue;
// Already queued with these exact inputs: let those jobs finish rather than doubling them.
if (m_PendingSegments.TryGetValue(segment.Id, out var pending) && pending.Signature == groupSignature)
continue;
EnqueueSegment(segment, table, profile, plans, groupSignature, existing);
}
foreach (var (nodeId, layout) in layouts)
{
ReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Surface, ComputeJunctionSurfaceSignature(layout), junctionChunks, null);
bool anyBorder = layout.Arms.Any(arm => ProfileOf(arm.Segment).HasBorder);
if (anyBorder)
{
// The junction's border runs continue the arms' chunks, so they take the same lifts and tapers those
// were built with.
var armBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();
var armTapers = new Dictionary<int, SegmentBorderTapers>();
foreach (var arm in layout.Arms)
{
armBlends[arm.Segment.Id] = segmentBlends.GetValueOrDefault(arm.Segment.Id);
armTapers[arm.Segment.Id] = segmentTapers.GetValueOrDefault(arm.Segment.Id);
}
ReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Border, ComputeJunctionBorderSignature(layout, armBlends, armTapers), junctionChunks, armBlends, armTapers);
}
else if (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Border), out var border))
{
border.GameObject.Destroy();
}
// Crossings used to be decals hung on the junction; the roads paint their own now, so anything left
// here from then goes.
if (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Marking), out var marking))
marking.GameObject.Destroy();
}
ReconcileLots(lotChunks);
// Whatever no longer belongs to anything in the graph goes, locked or not.
foreach (var (segmentId, chunks) in segmentChunks)
{
if (liveSegments.Contains(segmentId))
continue;
foreach (var chunk in chunks)
chunk.GameObject.Destroy();
m_PendingSegments.Remove(segmentId);
}
foreach (var (key, chunk) in junctionChunks)
{
if (layouts.ContainsKey(key.NodeId))
continue;
chunk.GameObject.Destroy();
m_PendingJunctions.Remove(key);
}
}
private void ReconcileJunctionPiece(int _NodeId, RoadJunctionLayout _Layout, RoadChunkKind _Kind, long _Signature, Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk> _Existing, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers = null)
{
var key = (_NodeId, _Kind);
if (_Existing.TryGetValue(key, out var existing) && (existing.Locked || (existing.Signature == _Signature && !LacksDebugRecord(existing))))
return;
if (m_PendingJunctions.TryGetValue(key, out var pending) && pending.Signature == _Signature)
return;
m_Jobs.Enqueue(new RebuildJob(-1, _NodeId, _Kind, _Signature, default, null, RoadWidth.Uniform(0.0f), null, _Layout, _ArmBlends, _ArmTapers, null));
m_PendingJunctions[key] = new PendingGroup(_Signature, 1);
}
/// <summary>
/// Where every segment sits in its run: the stretch of road carried on through pass-through nodes until a
/// junction, a road end or a loop. Each run is walked once, from the lowest-numbered segment in it backwards
/// to its head and then forwards to its tail, in that segment's direction, so every segment of the run agrees
/// on where the run's distance starts and the dashes carry across the nodes.
/// </summary>
private Dictionary<int, RoadMarkingBuilder.DashPhase> ComputeDashPhases()
{
var phases = new Dictionary<int, RoadMarkingBuilder.DashPhase>();
foreach (var seed in Segments.OrderBy(segment => segment.Id))
{
if (phases.ContainsKey(seed.Id))
continue;
var seen = new HashSet<int> { seed.Id };
var run = new List<(RoadSegment Segment, bool Forward)>();
// Back to the head: a road carrying on through the node runs with the run when it ends at that node.
var segment = seed;
int nodeId = seed.StartNodeId;
while (RunContinuation(segment, nodeId) is { } previous && seen.Add(previous.Id))
{
bool forward = previous.EndNodeId == nodeId;
run.Add((previous, forward));
segment = previous;
nodeId = forward ? previous.StartNodeId : previous.EndNodeId;
}
run.Reverse();
run.Add((seed, true));
// On to the tail.
segment = seed;
nodeId = seed.EndNodeId;
while (RunContinuation(segment, nodeId) is { } next && seen.Add(next.Id))
{
bool forward = next.StartNodeId == nodeId;
run.Add((next, forward));
segment = next;
nodeId = forward ? next.EndNodeId : next.StartNodeId;
}
float offset = 0.0f;
foreach (var (member, forward) in run)
{
phases[member.Id] = new RoadMarkingBuilder.DashPhase(offset, !forward);
offset += GetCurveTable(member)?.Length ?? 0.0f;
}
}
return phases;
}
/// <summary>
/// The road a segment carries straight on into at a pass-through node, or null at anything else.
/// </summary>
private RoadSegment RunContinuation(RoadSegment _Segment, int _NodeId)
{
if (GetDegree(_NodeId) != 2)
return null;
return GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);
}
/// <summary>
/// How a segment's border runs out at each end where it meets another road type's: over the profile's transition
/// length, towards the cross-section the two share. Without the junction layouts only pass-through nodes are
/// known; the reconcile passes them, so junction corners taper too.
/// </summary>
public SegmentBorderTapers GetBorderTaper(RoadSegment _Segment)
{
var table = GetCurveTable(_Segment);
return table is null ? default : ComputeBorderTaper(_Segment, table);
}
/// <summary>
/// One end of a border's taper: how long it is, the width and share of relief it runs to, whether a country road's
/// gravel spills over it, and how far from the node it ends.
/// </summary>
private readonly record struct EndTaper(float Length, float Width, float Relief, bool Spill, float Offset);
private SegmentBorderTapers ComputeBorderTaper(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts = null, float _TrimStart = 0.0f, float _TrimEnd = 0.0f)
{
var profile = ProfileOf(_Segment);
if (!profile.HasBorder)
return default;
float limit = _Table.Length * 0.5f;
RoadBorderTaper left = default;
RoadBorderTaper right = default;
// A transition ends at its trim, so the two ends share the stretch BETWEEN the trims and each gets half of
// it; however far a junction reaches along the road (a ramp meeting a highway can trim most of it), the two
// never run into each other and neither is squeezed out.
float transitionLimit = MathF.Max(0.0f, _Table.Length - _TrimStart - _TrimEnd) * 0.5f;
if (profile.TransitionLength > 0.0f)
{
SideTapers(_Segment, profile, _Segment.StartNodeId, true, transitionLimit, _Layouts, ref left, ref right);
SideTapers(_Segment, profile, _Segment.EndNodeId, false, transitionLimit, _Layouts, ref left, ref right);
}
// At a nose, the border on the side facing the road this one parts from grows out of nothing instead.
if (GoreLength(_Segment, _Table, _Segment.StartNodeId, true, limit) is { } startLeft)
left = left with { StartLength = startLeft, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };
if (GoreLength(_Segment, _Table, _Segment.StartNodeId, false, limit) is { } startRight)
right = right with { StartLength = startRight, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };
if (GoreLength(_Segment, _Table, _Segment.EndNodeId, true, limit) is { } endLeft)
left = left with { EndLength = endLeft, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };
if (GoreLength(_Segment, _Table, _Segment.EndNodeId, false, limit) is { } endRight)
right = right with { EndLength = endRight, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };
return new SegmentBorderTapers(left, right);
}
/// <summary>
/// The tapers at one end of a segment, one per side, each towards the border across from it there. At a plain
/// pass-through node that is the other road on both sides. At a junction it is the road on the far side of each
/// corner, and the taper ends at the arm's trim, where the corner starts: the corner itself is then built in the
/// one cross-section both its roads have run to, so it is a clean curve rather than one border turning into
/// the other round the arc.
/// </summary>
private void SideTapers(RoadSegment _Segment, RoadProfile _Profile, int _NodeId, bool _AtStart, float _Limit, Dictionary<int, RoadJunctionLayout> _Layouts, ref RoadBorderTaper _Left, ref RoadBorderTaper _Right)
{
int degree = GetDegree(_NodeId);
if (degree < 2)
return;
RoadSegment leftNeighbour = null;
RoadSegment rightNeighbour = null;
float trim = 0.0f;
if (_Layouts is not null && _Layouts.TryGetValue(_NodeId, out var layout) && layout.FindArm(_Segment.Id) is { } arm)
{
trim = arm.Trim;
// A corner starting on this arm runs on its left looking out of the node, one ending on it on its right;
// looking along a segment that ends at the node swaps the two.
foreach (var path in layout.CornerPaths)
{
if (path.ArmA.Segment.Id == _Segment.Id)
{
if (_AtStart)
leftNeighbour = path.ArmB.Segment;
else
rightNeighbour = path.ArmB.Segment;
}
else if (path.ArmB.Segment.Id == _Segment.Id)
{
if (_AtStart)
rightNeighbour = path.ArmA.Segment;
else
leftNeighbour = path.ArmA.Segment;
}
}
}
else if (degree == 2)
{
leftNeighbour = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);
rightNeighbour = leftNeighbour;
}
if (SideTaper(_Profile, leftNeighbour, degree, trim, _Limit, out var leftEnd))
_Left = WithEnd(_Left, _AtStart, leftEnd);
if (SideTaper(_Profile, rightNeighbour, degree, trim, _Limit, out var rightEnd))
_Right = WithEnd(_Right, _AtStart, rightEnd);
}
/// <summary>
/// The taper one side of a border takes towards the border across from it at a node, or false when it takes none:
/// nothing across, a border that is already the same size, or — at a junction — a border of another kind, which
/// a junction still closes off against this one.
/// </summary>
private bool SideTaper(RoadProfile _Profile, RoadSegment _Neighbour, int _Degree, float _Trim, float _Limit, out EndTaper _Taper)
{
_Taper = default;
if (_Neighbour is null)
return false;
var other = ProfileOf(_Neighbour);
bool sameKind = other.Border == _Profile.Border;
if (_Degree > 2 && !sameKind)
return false;
if (sameKind && MathF.Abs(other.FlatBorderWidth - _Profile.FlatBorderWidth) < 0.01f && MathF.Abs(other.ReliefHeight - _Profile.ReliefHeight) < 0.01f)
return false;
// Held to its half of the stretch between the trims, so it never runs into the taper at the far end.
float length = MathF.Min(_Profile.TransitionLength, _Limit);
if (length <= 0.01f)
return false;
_Taper = new EndTaper(length, RoadProfile.MeetingWidth(_Profile, other), RoadProfile.MeetingRelief(_Profile, other), !sameKind && other.Border == RoadBorderStyle.CountrySide, _Trim);
return true;
}
private static RoadBorderTaper WithEnd(in RoadBorderTaper _Taper, bool _AtStart, in EndTaper _End)
{
return _AtStart
? _Taper with { StartLength = _End.Length, StartWidth = _End.Width, StartRelief = _End.Relief, StartSpill = _End.Spill, StartOffset = _End.Offset }
: _Taper with { EndLength = _End.Length, EndWidth = _End.Width, EndRelief = _End.Relief, EndSpill = _End.Spill, EndOffset = _End.Offset };
}
/// <summary>
/// How far from a node the gap between this segment's edge on one side (its own left or right) and the edge of
/// another road parting from the same point has opened enough for both roads' borders to fit side by side; null
/// when no road parts from that edge there. Sampled along both roads at equal distances from the node, which
/// is how two roads leaving a common tangent separate.
/// </summary>
private float? GoreLength(RoadSegment _Segment, RoadCurveTable _Table, int _NodeId, bool _Left, float _Limit)
{
const float step = 25.0f;
if (GetDegree(_NodeId) < 3)
return null;
bool atStart = _Segment.StartNodeId == _NodeId;
float own = ProfileOf(_Segment).FlatBorderWidth;
var width = new RoadWidth(_Segment, _Table.Length);
// This segment's edge point at a distance from the node, on the side asked for.
Vector3 OwnEdge(float _FromNode)
{
float distance = atStart ? _FromNode : _Table.Length - _FromNode;
RoadSurfaceBuilder.CrossSection(_Table, distance, width, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);
return _Left ? leftEdge : rightEdge;
}
Vector3 start = OwnEdge(0.0f);
foreach (var other in GetSegmentsAt(_NodeId))
{
if (other.Id == _Segment.Id)
continue;
var otherTable = GetCurveTable(other);
if (otherTable is null)
continue;
bool otherAtStart = other.StartNodeId == _NodeId;
var otherWidth = new RoadWidth(other, otherTable.Length);
Vector3 OtherEdge(float _FromNode, bool _OtherLeft)
{
float distance = otherAtStart ? _FromNode : otherTable.Length - _FromNode;
RoadSurfaceBuilder.CrossSection(otherTable, distance, otherWidth, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);
return _OtherLeft ? leftEdge : rightEdge;
}
// The other road's edge that starts on ours, if either does.
bool otherLeft;
if (OtherEdge(0.0f, true).Distance(start) < 1.0f)
otherLeft = true;
else if (OtherEdge(0.0f, false).Distance(start) < 1.0f)
otherLeft = false;
else
continue;
// Parting means leaving the node the same way; a road turning off at an angle is a corner, not a gore.
Vector3 ownWay = OwnEdge(step) - start;
Vector3 otherWay = OtherEdge(step, otherLeft) - start;
if (ownWay.IsNearZeroLength || otherWay.IsNearZeroLength || Vector3.Dot(ownWay.Normal, otherWay.Normal) < 0.7f)
continue;
float needed = own + ProfileOf(other).FlatBorderWidth;
float reach = MathF.Min(_Limit, MathF.Min(_Table.Length, otherTable.Length));
for (float fromNode = step; fromNode <= reach; fromNode += step)
{
if (OwnEdge(fromNode).Distance(OtherEdge(fromNode, otherLeft)) >= needed)
return MathF.Max(step, fromNode);
}
return MathF.Max(step, reach);
}
return null;
}
/// <summary>
/// Whether two road types' borders come to one cross-section where they meet, so nothing needs closing off
/// between them: of a kind, and both tapering into the width and height they share.
/// </summary>
private static bool MeetsFlush(RoadProfile _A, RoadProfile _B)
{
return _A.HasBorder && _A.Border == _B.Border && _A.TransitionLength > 0.0f && _B.TransitionLength > 0.0f;
}
/// <summary>
/// The filled area of every junction, in network space (lifted to the road surface): its centre and its
/// outline, which the junction surface fans between, plus how far a pavement reaches out from the outline
/// round it and how far above the outline that pavement's top sits (nothing when no arm has a sidewalk). For
/// tools that need to know where road is.
/// </summary>
public IEnumerable<(Vector3 Centre, IReadOnlyList<Vector3> Polygon, float PavementReach, float PavementTop)> GetJunctionPolygons()
{
// A parking lot is a junction as far as the ground under it is concerned.
foreach (var lot in Lots)
{
var profile = ProfileOf(lot);
bool pavement = profile.Border == RoadBorderStyle.Sidewalk;
yield return (lot.Centre, RoadLotOutline.Build(lot, OpeningsOf(lot)).Points, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);
}
// A rounded dead end reaches past its node, where no road piece covers the ground.
foreach (var segment in Segments)
{
var table = GetCurveTable(segment);
if (table is null || segment.Bridge is not null)
continue;
var profile = ProfileOf(segment);
bool pavement = profile.Border == RoadBorderStyle.Sidewalk;
var width = new RoadWidth(segment, table.Length);
foreach (bool atStart in new[] { true, false })
{
int nodeId = atStart ? segment.StartNodeId : segment.EndNodeId;
if (GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)
continue;
var outline = RoadCapBuilder.Outline(table, atStart ? 0.0f : table.Length, atStart, width, profile, SurfaceOffset, out Vector3 centre);
if (outline.Count >= 3)
yield return (centre, outline, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);
}
}
foreach (var layout in ComputeJunctionLayouts().Values)
{
if (layout.Polygon.Count < 3)
continue;
float reach = 0.0f;
float top = 0.0f;
foreach (var arm in layout.Arms)
{
var profile = ProfileOf(arm.Segment);
if (profile.Border != RoadBorderStyle.Sidewalk)
continue;
reach = MathF.Max(reach, profile.BorderWidth);
top = MathF.Max(top, profile.CurbHeight);
}
yield return (layout.Centre, layout.Polygon, reach, top);
}
}
/// <summary>
/// Lays out every junction. A road too short for the fillets at both its ends gets those fillets shrunk until
/// their tangent points fit on it (a trim past the tangent point would send the corner geometry back over the
/// road); only corners that cannot fit even sharp are clamped. Then the junction polygons are built.
/// </summary>
private Dictionary<int, RoadJunctionLayout> ComputeJunctionLayouts()
{
var radii = Nodes.ToDictionary(node => node.Id, node => MathF.Max(0.0f, node.CornerRadius));
var layouts = BuildJunctionLayouts(radii);
var shrink = new Dictionary<int, float>();
foreach (var segment in Segments)
{
var table = GetCurveTable(segment);
if (table is null)
continue;
var startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);
var endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);
float sharp = (startArm?.SharpTrim ?? 0.0f) + (endArm?.SharpTrim ?? 0.0f);
float fillet = (startArm is null ? 0.0f : startArm.RawTrim - startArm.SharpTrim) + (endArm is null ? 0.0f : endArm.RawTrim - endArm.SharpTrim);
float available = MathF.Max(0.0f, table.Length - MinChunkLength);
if (fillet <= 0.0f || sharp + fillet <= available)
continue;
// The fillet's share of a trim is proportional to the radius, so this factor makes it fit exactly.
float factor = Math.Clamp((available - sharp) / fillet, 0.0f, 1.0f);
foreach (int nodeId in new[] { segment.StartNodeId, segment.EndNodeId })
{
if (layouts.ContainsKey(nodeId))
shrink[nodeId] = MathF.Min(shrink.GetValueOrDefault(nodeId, 1.0f), factor);
}
}
if (shrink.Count > 0)
{
foreach (var (nodeId, factor) in shrink)
radii[nodeId] *= factor;
layouts = BuildJunctionLayouts(radii);
}
// Last resort, for corners that do not fit even when sharp: share what road there is.
foreach (var segment in Segments)
{
var table = GetCurveTable(segment);
if (table is null)
continue;
var startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);
var endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);
float total = (startArm?.Trim ?? 0.0f) + (endArm?.Trim ?? 0.0f);
float available = MathF.Max(0.0f, table.Length - MinChunkLength);
if (total <= available || total <= 0.0f)
continue;
float scale = available / total;
if (startArm is not null)
startArm.Trim *= scale;
if (endArm is not null)
endArm.Trim *= scale;
}
foreach (var layout in layouts.Values)
layout.BuildPolygon();
return layouts;
}
/// <summary>
/// The corner fold on one side of a segment at one of its nodes, and whether the segment is that fold's first
/// arm (its left edge) or second (its right edge). Looking out of the node, a segment starting there has its
/// left on the arm's left; one ending there has it on the arm's right.
/// </summary>
private static RoadJunctionLayout.CornerFold FindFold(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _SideA)
{
_SideA = _AtStart == _Left;
if (!_Layouts.TryGetValue(_NodeId, out var layout))
return null;
var path = _SideA
? layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmA.Segment.Id == _Segment.Id)
: layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmB.Segment.Id == _Segment.Id);
return path?.Fold;
}
/// <summary>
/// The junction layout of every node for the given fillet radii, from the cache where the inputs match.
/// </summary>
private Dictionary<int, RoadJunctionLayout> BuildJunctionLayouts(Dictionary<int, float> _Radii)
{
var layouts = new Dictionary<int, RoadJunctionLayout>();
var liveNodes = new HashSet<int>();
foreach (var node in Nodes)
{
liveNodes.Add(node.Id);
float radius = _Radii[node.Id];
long key = ComputeJunctionKey(node, radius);
RoadJunctionLayout layout;
// A cached layout is only as good as its references: undo and reloads swap the graph's objects.
if (m_JunctionCache.TryGetValue(node.Id, out var cached) && cached.Key == key && (cached.Layout is null || cached.Layout.Refresh(this)))
{
layout = cached.Layout;
}
else
{
layout = RoadJunctionLayout.Create(this, node, SurfaceOffset, radius);
m_JunctionCache[node.Id] = (key, layout);
}
if (layout is null)
continue;
layout.ResetTrims();
layouts[node.Id] = layout;
}
foreach (int nodeId in m_JunctionCache.Keys.Where(id => !liveNodes.Contains(id)).ToList())
m_JunctionCache.Remove(nodeId);
return layouts;
}
/// <summary>
/// Whether an end of a road asks for a pedestrian crossing: the road type has them, the road has not turned
/// this end's off, and the end is a real junction (three roads or more; a bend has a layout too, but nobody
/// crosses at a bend) that trims the road.
/// </summary>
private bool WantsCrossing(RoadSegment _Segment, int _NodeId, Dictionary<int, RoadJunctionLayout> _Layouts, float _Trim)
{
if (!ProfileOf(_Segment).Crossings || !_Segment.CrossingAt(_NodeId))
return false;
return _Layouts.TryGetValue(_NodeId, out var layout) && layout.Arms.Count >= 3 && _Trim > 0.01f;
}
/// <summary>
/// Places a road's crossings on the stretch left between its junctions. Two crossings need room for both and
/// a gap between; a shorter road between two junctions gets one crossing shared by both, halfway along; a road
/// shorter than a crossing gets none.
/// </summary>
private SegmentCrossings ComputeCrossings(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts, float _TrimStart, float _TrimEnd)
{
var profile = ProfileOf(_Segment);
float depth = profile.CrossingDepth;
float clear = _Table.Length - _TrimStart - _TrimEnd;
bool wantStart = WantsCrossing(_Segment, _Segment.StartNodeId, _Layouts, _TrimStart);
bool wantEnd = WantsCrossing(_Segment, _Segment.EndNodeId, _Layouts, _TrimEnd);
if (clear < depth || (!wantStart && !wantEnd))
return default;
// A ladder of stripes is laid on the road outside the junction; two lines mark the walk across the
// junction's own mouth, so that kind is pulled in far enough to put its outer line on the pavement's
// corner instead of a crossing's depth short of it.
bool inside = profile.CrossingStyle == RoadCrossingStyle.Lines;
float startCentre = inside
? MathF.Max(depth * 0.5f, _TrimStart - depth * 0.5f)
: _TrimStart + depth * 0.5f;
float endCentre = inside
? MathF.Min(_Table.Length - depth * 0.5f, _Table.Length - _TrimEnd + depth * 0.5f)
: _Table.Length - _TrimEnd - depth * 0.5f;
if (wantStart && wantEnd)
{
if (clear >= 2.0f * depth + profile.CrossingClearance)
return new SegmentCrossings(true, startCentre, true, endCentre);
return new SegmentCrossings(true, _TrimStart + clear * 0.5f, false, 0.0f);
}
return wantStart
? new SegmentCrossings(true, startCentre, false, 0.0f)
: new SegmentCrossings(false, 0.0f, true, endCentre);
}
/// <summary>
/// Where the painted lines run: the stretch between the junctions, kept clear of the crossings.
/// </summary>
private static LineRange ComputeLineRange(RoadCurveTable _Table, RoadProfile _Profile, float _TrimStart, float _TrimEnd, in SegmentCrossings _Crossings)
{
float reach = _Profile.CrossingDepth * 0.5f + _Profile.CrossingClearance;
float start = _TrimStart;
float end = _Table.Length - _TrimEnd;
float gapStart = 0.0f;
float gapEnd = 0.0f;
if (_Crossings.AtEnd)
end = MathF.Min(end, _Crossings.EndCentre - reach);
if (_Crossings.AtStart)
{
float before = _Crossings.StartCentre - reach;
float after = _Crossings.StartCentre + reach;
// A crossing at the very start just pushes the lines back; one further along cuts a gap in them.
if (before <= start + 1.0f)
{
start = MathF.Max(start, after);
}
else
{
gapStart = before;
gapEnd = after;
}
}
return new LineRange(start, end, gapStart, gapEnd);
}
/// <summary>
/// Which ends of a segment's borders nothing continues: a road end, a side of a junction with no corner on it,
/// or a pass-through node where the road changes type or width.
/// </summary>
private BorderCaps ComputeBorderCaps(RoadSegment _Segment, Dictionary<int, RoadJunctionLayout> _Layouts)
{
var caps = BorderCaps.None;
if (IsBorderOpen(_Segment, _Segment.StartNodeId, true, _Layouts, out bool startLeft, out bool startRight))
{
caps |= startLeft ? BorderCaps.StartLeft : BorderCaps.None;
caps |= startRight ? BorderCaps.StartRight : BorderCaps.None;
}
if (IsBorderOpen(_Segment, _Segment.EndNodeId, false, _Layouts, out bool endLeft, out bool endRight))
{
caps |= endLeft ? BorderCaps.EndLeft : BorderCaps.None;
caps |= endRight ? BorderCaps.EndRight : BorderCaps.None;
}
return caps;
}
/// <summary>
/// A bridge segment's built range and ends. An end is closed off unless it is a pass-through node onto
/// another bridge of the same style, width and border, where the two decks meet flush; there, only one of the
/// two segments provides the node's railing post: the one leaving the node, or the lower id when both leave
/// or both arrive.
/// </summary>
private RoadBridgeBuilder.BridgeSpan ComputeBridgeSpan(RoadSegment _Segment, float _RangeStart, float _RangeEnd)
{
var startNeighbour = DeckContinuation(_Segment, _Segment.StartNodeId);
var endNeighbour = DeckContinuation(_Segment, _Segment.EndNodeId);
bool postStart = startNeighbour is null || startNeighbour.EndNodeId == _Segment.StartNodeId || _Segment.Id < startNeighbour.Id;
bool postEnd = endNeighbour is null || (endNeighbour.EndNodeId == _Segment.EndNodeId && _Segment.Id < endNeighbour.Id);
return new RoadBridgeBuilder.BridgeSpan(_RangeStart, _RangeEnd, startNeighbour is null, postStart, endNeighbour is null, postEnd);
}
/// <summary>
/// The bridge segment the deck carries straight on to at a node, or null where the deck ends.
/// </summary>
private RoadSegment DeckContinuation(RoadSegment _Segment, int _NodeId)
{
if (GetDegree(_NodeId) != 2)
return null;
var other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);
if (other?.Bridge is null || other.Bridge.ResourcePath != _Segment.Bridge.ResourcePath || !SameEdgesAt(_Segment, other, _NodeId))
return null;
bool sameExtent = MathF.Abs(RoadBridgeBuilder.DeckExtent(ProfileOf(other), other.Bridge) - RoadBridgeBuilder.DeckExtent(ProfileOf(_Segment), _Segment.Bridge)) < 0.01f;
return sameExtent ? other : null;
}
/// <summary>
/// The lift lines of a segment's two borders: at each end, the corner's height difference on that side (or
/// nothing), run linearly along the road between the two.
/// </summary>
private RoadBorderBuilder.SegmentBorderBlends ComputeBorderBlends(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts)
{
return new RoadBorderBuilder.SegmentBorderBlends(SideBlendFor(_Segment, _Table, true, _Layouts), SideBlendFor(_Segment, _Table, false, _Layouts));
}
private RoadBorderBuilder.SideBlend SideBlendFor(RoadSegment _Segment, RoadCurveTable _Table, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts)
{
bool hasStart = CornerLift(_Segment, _Segment.StartNodeId, true, _Left, _Layouts, out float startDistance, out float startDelta);
bool hasEnd = CornerLift(_Segment, _Segment.EndNodeId, false, _Left, _Layouts, out float endDistance, out float endDelta);
if (!hasStart && !hasEnd)
return default;
// An end with no corner on this side holds the border at road height from where the border actually starts,
// which is past the trim: a road opening into a lot stops at the lot's flares, and a blend still running
// there hands the lot a pavement tilted by whatever it had left to shed while the lot's own is flat.
if (!hasStart)
{
startDistance = LotTrimAt(_Segment.StartNodeId);
startDelta = 0.0f;
}
if (!hasEnd)
{
endDistance = _Table.Length - LotTrimAt(_Segment.EndNodeId);
endDelta = 0.0f;
}
return new RoadBorderBuilder.SideBlend(true, startDistance, startDelta, endDistance, endDelta);
}
/// <summary>
/// Where along the segment the corner's tangent point on this side is, and how much higher the corner sits
/// than the road edge there; false when there is no inside corner on that side at that end.
/// </summary>
private static bool CornerLift(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out float _Distance, out float _Delta)
{
_Distance = 0.0f;
_Delta = 0.0f;
var fold = FindFold(_Segment, _NodeId, _AtStart, _Left, _Layouts, out bool sideA);
if (fold is null || !_Layouts.TryGetValue(_NodeId, out var layout))
return false;
var arm = layout.FindArm(_Segment.Id);
if (arm is null)
return false;
_Distance = arm.CurveDistance(sideA ? fold.TangentParamA : fold.TangentParamB);
_Delta = fold.ApexHeight - (sideA ? fold.TangentHeightA : fold.TangentHeightB);
return true;
}
private bool IsBorderOpen(RoadSegment _Segment, int _NodeId, bool _AtStart, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _Left, out bool _Right)
{
int degree = GetDegree(_NodeId);
// A road opening into a lot runs into the lot's own kerb, which closes it off; two caps in one place
// would only fight each other.
if (LotAtOpening(_NodeId) is { } lot && ProfileOf(lot).Border == ProfileOf(_Segment).Border)
{
_Left = false;
_Right = false;
return false;
}
if (degree <= 1)
{
// A road rounded off where it stops carries its border round the end; a cap across the road as well
// would sit inside that.
bool rounded = _Segment.Bridge is null && ProfileOf(_Segment).RoundedEnds;
_Left = !rounded;
_Right = !rounded;
return !rounded;
}
if (_Layouts.TryGetValue(_NodeId, out var layout))
{
// A corner path starting on this arm continues its left border (looking out of the node); one ending on
// it continues its right. Looking out of the node along a segment that ends there swaps the sides.
bool armLeftContinued = layout.CornerPaths.Any(path => path.ArmA.Segment.Id == _Segment.Id);
bool armRightContinued = layout.CornerPaths.Any(path => path.ArmB.Segment.Id == _Segment.Id);
_Left = !(_AtStart ? armLeftContinued : armRightContinued);
_Right = !(_AtStart ? armRightContinued : armLeftContinued);
return _Left || _Right;
}
if (degree == 2)
{
var other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);
bool continuous = other is not null && SameEdgesAt(_Segment, other, _NodeId)
&& (RoadBorderBuilder.SameProfile(ProfileOf(other), ProfileOf(_Segment)) || MeetsFlush(ProfileOf(other), ProfileOf(_Segment)));
_Left = !continuous;
_Right = !continuous;
return !continuous;
}
_Left = true;
_Right = true;
return true;
}
/// <summary>
/// Splits a segment into chunks at fixed distances from its start, clipped by the junction trims at each end,
/// with a border chunk over each stretch when the road type has borders. Anchoring the boundaries to the
/// segment rather than to the trimmed length means a junction change at one end only touches the chunk next
/// to it.
/// </summary>
private List<ChunkPlan> PlanChunks(RoadSegment _Segment, RoadCurveTable _Table, float _TrimStart, float _TrimEnd, RoadProfile _Profile, BorderCaps _Caps, in RoadBorderBuilder.SegmentBorderBlends _Blends, in SegmentBorderTapers _Taper, long _SurfaceSignature, in LineRange _Lines, in RoadMarkingBuilder.DashPhase _Dash, in SegmentCrossings _Crossings)
{
float length = _Table.Length;
int count = Math.Max(1, (int)MathF.Ceiling(length / ChunkLength));
float usableEnd = length - _TrimEnd;
var ranges = new List<(int Index, float Start, float End)>(count);
for (int i = 0; i < count; i++)
{
float start = MathF.Max(length * i / count, _TrimStart);
float end = MathF.Min(length * (i + 1) / count, usableEnd);
if (end - start >= MinChunkLength)
ranges.Add((i, start, end));
}
var plans = new List<ChunkPlan>(ranges.Count * 4);
var borderHash = StableHash.Create();
borderHash.Add(_SurfaceSignature);
_Profile.AddBorderToHash(ref borderHash);
_Blends.AddToHash(ref borderHash);
_Taper.AddToHash(ref borderHash);
borderHash.Add(DebugBorders ? 1 : 0);
long borderSignature = borderHash.Value;
// The deck follows the road and stands off its border, so it is stale when either changes; its posts and
// supports are spread over the whole built range, so every piece is stale when an end moves.
var bridge = _Segment.Bridge;
var bridgeSpan = bridge is null ? default : ComputeBridgeSpan(_Segment, _TrimStart, usableEnd);
long bridgeSignature = 0;
if (bridge is not null)
{
var bridgeHash = StableHash.Create();
bridgeHash.Add(_SurfaceSignature);
bridgeHash.Add(bridge.ResourcePath);
_Profile.AddBorderToHash(ref bridgeHash);
bridge.AddToHash(ref bridgeHash);
bridgeSpan.AddToHash(ref bridgeHash);
bridgeSignature = bridgeHash.Value;
}
// Props are spread over the whole built range too, and stand on the border, so they follow its shape.
var props = _Segment.Props;
var propSpan = props is null ? default : new RoadPropBuilder.PropSpan(_TrimStart, usableEnd, GetDegree(_Segment.StartNodeId) >= 3, GetDegree(_Segment.EndNodeId) >= 3);
long propSignature = 0;
if (props is not null && !props.IsEmpty)
{
var propHash = StableHash.Create();
propHash.Add(_SurfaceSignature);
propHash.Add(props.ResourcePath);
propHash.Add(bridge is null ? 0 : 1);
_Profile.AddBorderToHash(ref propHash);
props.AddToHash(ref propHash);
propSpan.AddToHash(ref propHash);
propSignature = propHash.Value;
}
// A road with no lines of its own still needs the piece when it has a crossing to paint.
bool markings = RoadMarkingBuilder.HasMarkings(_Segment, _Profile) || _Crossings.AtStart || _Crossings.AtEnd;
var markingHash = StableHash.Create();
markingHash.Add(_SurfaceSignature);
markingHash.Add(_Segment.LaneCount);
markingHash.Add(_Segment.ForwardLanes);
_Lines.AddToHash(ref markingHash);
_Dash.AddToHash(ref markingHash);
_Profile.AddMarkingsToHash(ref markingHash);
_Profile.AddCrossingsToHash(ref markingHash);
markingHash.Add(_Crossings.AtStart ? _Crossings.StartCentre : -1.0f);
markingHash.Add(_Crossings.AtEnd ? _Crossings.EndCentre : -1.0f);
long markingSignature = markingHash.Value;
for (int r = 0; r < ranges.Count; r++)
{
var (index, start, end) = ranges[r];
var hash = StableHash.Create();
hash.Add(_SurfaceSignature);
hash.Add(index);
hash.Add(count);
hash.Add(start);
hash.Add(end);
plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Surface, BorderCaps.None, default, hash.Value));
if (markings)
{
hash = StableHash.Create();
// A crossing belongs to one piece: the outermost, since its band may lie clear of every piece's
// own stretch of road.
var owned = new SegmentCrossings(_Crossings.AtStart && r == 0, _Crossings.StartCentre,
_Crossings.AtEnd && r == ranges.Count - 1, _Crossings.EndCentre);
hash.Add(markingSignature);
hash.Add(index);
hash.Add(count);
hash.Add(start);
hash.Add(end);
hash.Add(owned.AtStart ? 1 : 0);
hash.Add(owned.AtEnd ? 1 : 0);
plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Marking, BorderCaps.None, default, hash.Value, _Lines, Dash: _Dash, Crossings: owned));
}
if (bridge is not null)
{
// The piece works out for itself whether it is an outermost one, from its stretch against the span.
hash = StableHash.Create();
hash.Add(bridgeSignature);
hash.Add(index);
hash.Add(count);
hash.Add(start);
hash.Add(end);
plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Bridge, BorderCaps.None, default, hash.Value, Bridge: bridgeSpan));
}
if (propSignature != 0)
{
hash = StableHash.Create();
hash.Add(propSignature);
hash.Add(index);
hash.Add(count);
hash.Add(start);
hash.Add(end);
plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Props, BorderCaps.None, default, hash.Value, Props: propSpan));
}
if (!_Profile.HasBorder)
continue;
// Only the outermost stretches have open ends to close.
var caps = BorderCaps.None;
if (r == 0)
caps |= _Caps & (BorderCaps.StartLeft | BorderCaps.StartRight);
if (r == ranges.Count - 1)
caps |= _Caps & (BorderCaps.EndLeft | BorderCaps.EndRight);
hash = StableHash.Create();
hash.Add(borderSignature);
hash.Add(index);
hash.Add(count);
hash.Add(start);
hash.Add(end);
hash.Add((int)caps);
plans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Border, caps, _Blends, hash.Value, default, _Taper));
}
// A road that simply stops is rounded off there, in a piece of its own beyond the node.
AddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, true);
AddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, false);
return plans;
}
/// <summary>
/// The rounded end of a road, where it has one: at a node with nothing else at it, which is not a way into a
/// parking lot (the lot's own kerb closes that), and on a road no bridge carries (a deck ends where it ends).
/// The plan's start is the end's own distance along the curve and its end the depth the cap reaches past it.
/// </summary>
private void AddCapPlan(List<ChunkPlan> _Plans, RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, long _SurfaceSignature, bool _AtStart)
{
int nodeId = _AtStart ? _Segment.StartNodeId : _Segment.EndNodeId;
if (_Segment.Bridge is not null || GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)
return;
float distance = _AtStart ? 0.0f : _Table.Length;
float depth = RoadCapBuilder.Depth(_Profile, new RoadWidth(_Segment, _Table.Length), distance);
if (depth <= 0.01f)
return;
var hash = StableHash.Create();
hash.Add(_SurfaceSignature);
hash.Add(_AtStart ? 1 : 0);
hash.Add(depth);
_Profile.AddBorderToHash(ref hash);
_Plans.Add(new ChunkPlan(_AtStart ? 0 : 1, 2, distance, depth, RoadChunkKind.Cap, BorderCaps.None, default, hash.Value));
}
private static long CombineSignatures(List<ChunkPlan> _Plans)
{
var hash = StableHash.Create();
hash.Add(_Plans.Count);
foreach (var plan in _Plans)
hash.Add(plan.Signature);
return hash.Value;
}
private bool IsSegmentUpToDate(List<RoadChunk> _Existing, List<ChunkPlan> _Plans)
{
int have = _Existing?.Count ?? 0;
if (have != _Plans.Count)
return false;
foreach (var plan in _Plans)
{
var chunk = _Existing.FirstOrDefault(candidate => candidate.ChunkIndex == plan.Index && candidate.Kind == plan.Kind);
if (chunk is null)
return false;
// A locked chunk is exempt: it is deliberately stale.
if (!chunk.Locked && (chunk.Signature != plan.Signature || LacksDebugRecord(chunk)))
return false;
}
return true;
}
/// <summary>
/// While the border debug overlay is on, a border piece without its record (built before the flag went on, or
/// before a hot reload, which drops the records) is rebuilt to get one.
/// </summary>
private bool LacksDebugRecord(RoadChunk _Chunk)
{
return DebugBorders && _Chunk.Kind == RoadChunkKind.Border && (_Chunk.Debug is null || _Chunk.Debug.Text.Length == 0);
}
/// <summary>
/// Throws away every unlocked piece's signature so the next reconcile rebuilds all of them, junction layouts
/// included. Toggling the GameObject does not do this: pieces whose inputs have not changed are kept as they
/// are, so after a generator change this is the way to see it everywhere.
/// </summary>
[Button("Regenerate All", "refresh")]
public void RegenerateAll()
{
foreach (var chunk in GetChunks())
{
if (!chunk.Locked)
chunk.Signature = 0;
}
m_JunctionCache.Clear();
MarkDirty();
}
private long ComputeSurfaceSignature(RoadSegment _Segment, in RoadCurve _Curve, RoadProfile _Profile)
{
var hash = StableHash.Create();
hash.Add(GeneratorVersion);
hash.Add(_Curve.P0);
hash.Add(_Curve.P1);
hash.Add(_Curve.P2);
hash.Add(_Curve.P3);
new RoadWidth(_Segment, 1.0f).AddToHash(ref hash);
hash.Add(SampleSpacing);
hash.Add(ChunkLength);
hash.Add(SurfaceOffset);
_Profile.AddSurfaceToHash(ref hash);
return hash.Value;
}
/// <summary>
/// Everything a node's junction layout depends on: the node itself and the curve and width of each road at it.
/// </summary>
private long ComputeJunctionKey(RoadNode _Node, float _Radius)
{
var hash = StableHash.Create();
hash.Add(GeneratorVersion);
hash.Add(_Node.Position);
hash.Add(_Radius);
hash.Add(SurfaceOffset);
foreach (var segment in GetSegmentsAt(_Node.Id).OrderBy(segment => segment.Id))
{
hash.Add(segment.Id);
new RoadWidth(segment, 1.0f).AddToHash(ref hash);
hash.Add(segment.Ramp ? 1 : 0);
hash.Add(segment.StartNodeId == _Node.Id ? 1 : 0);
if (!TryGetCurve(segment, out var curve))
continue;
hash.Add(curve.P0);
hash.Add(curve.P1);
hash.Add(curve.P2);
hash.Add(curve.P3);
}
return hash.Value;
}
private static void AddPolygonToHash(RoadJunctionLayout _Layout, ref StableHash _Hash)
{
_Hash.Add(GeneratorVersion);
_Hash.Add(_Layout.Centre);
_Hash.Add(_Layout.Polygon.Count);
foreach (var vertex in _Layout.Polygon)
_Hash.Add(vertex);
}
private long ComputeJunctionSurfaceSignature(RoadJunctionLayout _Layout)
{
var hash = StableHash.Create();
AddPolygonToHash(_Layout, ref hash);
JunctionSurfaceProfile(_Layout).AddSurfaceToHash(ref hash);
return hash.Value;
}
private long ComputeJunctionBorderSignature(RoadJunctionLayout _Layout, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers)
{
var hash = StableHash.Create();
AddPolygonToHash(_Layout, ref hash);
hash.Add(DebugBorders ? 1 : 0);
foreach (var arm in _Layout.Arms)
{
var profile = ProfileOf(arm.Segment);
hash.Add(arm.Segment.Id);
hash.Add(arm.AtStart ? 1 : 0);
hash.Add(arm.Length);
profile.AddSurfaceToHash(ref hash);
profile.AddBorderToHash(ref hash);
_ArmBlends.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);
_ArmTapers.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);
}
return hash.Value;
}
/// <summary>
/// A junction's surface takes after its main road: the widest arm.
/// </summary>
private RoadProfile JunctionSurfaceProfile(RoadJunctionLayout _Layout)
{
var main = _Layout.Arms.OrderByDescending(arm => arm.Widths.Base).ThenBy(arm => arm.Segment.Id).FirstOrDefault();
return ProfileOf(main?.Segment);
}
private void EnqueueSegment(RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, List<ChunkPlan> _Plans, long _GroupSignature, List<RoadChunk> _Existing)
{
// Chunks with no plan (swallowed by a junction, past a shorter road's end, or a border the road no longer
// has) have no job coming to replace them.
if (_Existing is not null)
{
foreach (var chunk in _Existing)
{
if (!chunk.Locked && _Plans.All(plan => plan.Index != chunk.ChunkIndex || plan.Kind != chunk.Kind))
chunk.GameObject.Destroy();
}
}
if (_Plans.Count == 0)
{
m_PendingSegments.Remove(_Segment.Id);
return;
}
foreach (var plan in _Plans)
m_Jobs.Enqueue(new RebuildJob(_Segment.Id, -1, plan.Kind, _GroupSignature, plan, _Table, new RoadWidth(_Segment, _Table.Length), _Profile, null, null, null, null));
m_PendingSegments[_Segment.Id] = new PendingGroup(_GroupSignature, _Plans.Count);
}
private void ProcessJobs()
{
int budget = Math.Max(1, RebuildBudget);
while (budget > 0 && m_Jobs.Count > 0)
{
var job = m_Jobs.Dequeue();
if (job.Lot is not null)
{
var lotKey = (job.Lot.Id, job.Kind);
// Superseded by a newer version of the same lot, or the lot is gone.
if (!m_PendingLots.TryGetValue(lotKey, out var pendingLot) || pendingLot.Signature != job.GroupSignature)
continue;
BuildLotPiece(job);
budget--;
m_PendingLots.Remove(lotKey);
continue;
}
if (job.NodeId >= 0)
{
var key = (job.NodeId, job.Kind);
// Superseded by a newer layout of the same junction, or the junction is gone.
if (!m_PendingJunctions.TryGetValue(key, out var pending) || pending.Signature != job.GroupSignature)
continue;
BuildJunctionPiece(job);
budget--;
m_PendingJunctions.Remove(key);
continue;
}
// Superseded by a newer rebuild of the same segment, or the segment is gone.
if (!m_PendingSegments.TryGetValue(job.SegmentId, out var pendingSegment) || pendingSegment.Signature != job.GroupSignature)
continue;
if (BuildChunk(job))
budget--;
if (pendingSegment.Remaining <= 1)
m_PendingSegments.Remove(job.SegmentId);
else
m_PendingSegments[job.SegmentId] = pendingSegment with { Remaining = pendingSegment.Remaining - 1 };
}
}
/// <summary>
/// Returns false when there was nothing to do: the chunk in place was already built from these inputs (a
/// junction change at the far end of the road requeues the whole road, but only the chunk next to it differs).
/// </summary>
private bool BuildChunk(in RebuildJob _Job)
{
var plan = _Job.Plan;
var existing = FindChunk(_Job.SegmentId, plan.Index, plan.Kind);
if (existing is not null && (existing.Locked || (existing.Signature == plan.Signature && !LacksDebugRecord(existing))))
return false;
PolygonMesh mesh;
string name;
BorderDebug debug = null;
var placements = new List<(GameObject Prefab, Transform Transform)>();
if (plan.Kind == RoadChunkKind.Border)
{
debug = DebugBorders ? new BorderDebug() : null;
mesh = RoadBorderBuilder.BuildSegment(_Job.Table, plan.Start, plan.End, _Job.Width, _Job.Profile, SampleSpacing, SurfaceOffset, plan.Caps, plan.Blends, plan.Taper, debug);
name = $"Road {_Job.SegmentId} border [{plan.Index + 1}/{plan.Count}]";
}
else if (plan.Kind == RoadChunkKind.Bridge)
{
var segment = FindSegment(_Job.SegmentId);
if (segment?.Bridge is null)
return false;
var parts = new List<RoadBridgeBuilder.Placement>();
mesh = RoadBridgeBuilder.Build(_Job.Table, plan.Start, plan.End, plan.Bridge, _Job.Width, _Job.Profile, segment.Bridge, SampleSpacing, SurfaceOffset, GroundBelow, parts);
name = $"Road {_Job.SegmentId} bridge [{plan.Index + 1}/{plan.Count}]";
foreach (var part in parts)
placements.Add((part.Prefab, new Transform(part.Position, part.Rotation)));
}
else if (plan.Kind == RoadChunkKind.Props)
{
var segment = FindSegment(_Job.SegmentId);
if (segment?.Props is null)
return false;
// Nothing to mesh: the piece is only the parent of its prefabs.
var props = new List<RoadPropBuilder.Placement>();
var context = new RoadPropBuilder.Context(_Job.Table, plan.Start, plan.End, plan.Props, _Job.SegmentId, _Job.Width, _Job.Profile, segment.Bridge is not null, SurfaceOffset, GroundAround);
RoadPropBuilder.Build(context, segment.Props, props);
mesh = null;
name = $"Road {_Job.SegmentId} props [{plan.Index + 1}/{plan.Count}]";
foreach (var prop in props)
placements.Add((prop.Prefab, new Transform(prop.Position, prop.Rotation, prop.Scale)));
}
else if (plan.Kind == RoadChunkKind.Cap)
{
// Surface and border in one piece: the whole end is small, and nothing else shares it.
mesh = RoadCapBuilder.Build(_Job.Table, plan.Start, plan.Index == 0, _Job.Width, _Job.Profile, SurfaceOffset, plan.End);
name = $"Road {_Job.SegmentId} {(plan.Index == 0 ? "start" : "end")} cap";
}
else if (plan.Kind == RoadChunkKind.Marking)
{
var segment = FindSegment(_Job.SegmentId);
if (segment is null)
return false;
// Lines only where the chunk overlaps the stretches left clear by the crossings, laid on the rows the
// road surface itself was built on.
mesh = new PolygonMesh();
var roadRows = RoadRows.Build(_Job.Table, plan.Start, plan.End, SampleSpacing, _Job.Width, _Job.Profile.Simplification);
if (plan.Lines.HasGap)
{
RoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.GapStart), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);
RoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.GapEnd), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);
}
else
{
RoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);
}
// The crossings this piece owns, laid on the same rows as the lines and the road itself.
if (plan.Crossings.AtStart)
RoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.StartCentre, _Job.Width, _Job.Profile, SurfaceOffset);
if (plan.Crossings.AtEnd)
RoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.EndCentre, _Job.Width, _Job.Profile, SurfaceOffset);
name = $"Road {_Job.SegmentId} markings [{plan.Index + 1}/{plan.Count}]";
}
else
{
mesh = RoadSurfaceBuilder.Build(_Job.Table, plan.Start, plan.End, _Job.Width, SampleSpacing, _Job.Profile.Simplification, _Job.Profile.RoadTextureRepeat, SurfaceOffset, _Job.Profile.ResolvedRoadMaterial);
name = $"Road {_Job.SegmentId} [{plan.Index + 1}/{plan.Count}]";
}
var piece = CreateChunkObject(name, _Job.SegmentId, -1, plan.Kind, plan.Index, plan.Count, plan.Signature, mesh, debug);
foreach (var (prefab, transform) in placements)
prefab.Clone(transform, piece);
// The new piece is in place before the old one goes, so the road never flickers.
existing?.GameObject.Destroy();
return true;
}
/// <summary>
/// The height of whatever lies under a point of the network (its own pieces excepted), in network space, for
/// bridge supports to land on: the search starts just below the point, so the deck itself is never it. Null
/// over nothing.
/// </summary>
private float? GroundBelow(Vector3 _Local)
{
return TraceGround(_Local, -1.0f);
}
/// <summary>
/// The height of the ground at a point of the network, for props to stand on: the search starts well above
/// the point, so ground rising beside a road is found as well as ground falling away from it.
/// </summary>
private float? GroundAround(Vector3 _Local)
{
return TraceGround(_Local, 3000.0f);
}
private float? TraceGround(Vector3 _Local, float _StartAbove)
{
const float reach = 50000.0f;
var transform = WorldTransform;
Vector3 from = transform.PointToWorld(_Local) + Vector3.Up * _StartAbove;
var trace = Scene.Trace.Ray(from, from + Vector3.Down * reach).WithoutTags(ChunkTag).Run();
if (!trace.Hit)
return null;
return transform.PointToLocal(trace.EndPosition).z;
}
private void BuildJunctionPiece(in RebuildJob _Job)
{
var existing = FindJunctionChunk(_Job.NodeId, _Job.Kind);
if (existing is not null && existing.Locked)
return;
PolygonMesh mesh;
string name;
BorderDebug debug = null;
if (_Job.Kind == RoadChunkKind.Border)
{
var armBlends = _Job.ArmBlends;
var armTapers = _Job.ArmTapers;
debug = DebugBorders ? new BorderDebug() : null;
mesh = RoadBorderBuilder.BuildJunction(_Job.Junction, ProfileOf,
segment => armBlends is not null ? armBlends.GetValueOrDefault(segment.Id) : default,
segment => armTapers is not null ? armTapers.GetValueOrDefault(segment.Id) : default,
debug);
name = $"Junction {_Job.NodeId} border";
}
else
{
var profile = JunctionSurfaceProfile(_Job.Junction);
mesh = RoadJunctionBuilder.Build(_Job.Junction, profile.RoadTextureRepeat, profile.ResolvedRoadMaterial);
name = $"Junction {_Job.NodeId}";
}
CreateChunkObject(name, -1, _Job.NodeId, _Job.Kind, 0, 1, _Job.GroupSignature, mesh, debug);
existing?.GameObject.Destroy();
}
private GameObject CreateChunkObject(string _Name, int _SegmentId, int _NodeId, RoadChunkKind _Kind, int _Index, int _Count, long _Signature, PolygonMesh _Mesh, BorderDebug _Debug, int _LotId = -1)
{
var child = new GameObject(GameObject, true, _Name);
child.Tags.Add(ChunkTag);
var chunk = child.AddComponent<RoadChunk>();
chunk.SegmentId = _SegmentId;
chunk.NodeId = _NodeId;
chunk.LotId = _LotId;
chunk.Kind = _Kind;
chunk.ChunkIndex = _Index;
chunk.ChunkCount = _Count;
chunk.Signature = _Signature;
chunk.Debug = _Debug;
if (_Debug is not null)
DebugRevision++;
// A piece of props has no geometry of its own; it is the parent of its prefabs.
if (_Mesh is null)
return child;
var meshComponent = child.AddComponent<MeshComponent>();
meshComponent.Mesh = _Mesh;
meshComponent.SmoothingAngle = 40.0f;
// Paint lies flat on the road: nothing to collide with, and its shadow would be a sliver hidden under it,
// at the cost of a shadow pass.
if (_Kind == RoadChunkKind.Marking)
{
meshComponent.Collision = MeshComponent.CollisionType.None;
meshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;
}
return child;
}
}
Editor
library
using Sandbox;
namespace RedSnail.RoadTool2.Editor;
/// <summary>
/// Gizmo drawing shared by the road tools. Curves are in network-local space and drawn through the network's
/// transform, in whatever colour and thickness the caller set up.
/// </summary>
internal static class RoadGizmos
{
public static void DrawCurve(in RoadCurve _Curve, in Transform _Transform, int _Steps = 24)
{
Vector3 previous = _Transform.PointToWorld(_Curve.P0);
for (int i = 1; i <= _Steps; i++)
{
Vector3 point = _Transform.PointToWorld(_Curve.Evaluate((float)i / _Steps));
Gizmo.Draw.Line(previous, point);
previous = point;
}
}
/// <summary>
/// Draws the two edges of a road of the given width along the curve, capped at both ends.
/// </summary>
public static void DrawRoadOutline(in RoadCurve _Curve, float _Width, in Transform _Transform, int _Steps = 24)
{
DrawRoadOutline(_Curve, RoadWidth.Uniform(_Width), _Transform, _Steps);
}
/// <summary>
/// Draws the two edges of a road along the curve, each its own distance out (the curve's parameter standing in
/// for its distance), capped at both ends.
/// </summary>
public static void DrawRoadOutline(in RoadCurve _Curve, in RoadWidth _Width, in Transform _Transform, int _Steps = 24)
{
Vector3 previousLeft = default;
Vector3 previousRight = default;
for (int i = 0; i <= _Steps; i++)
{
float t = (float)i / _Steps;
float distance = t * _Width.Length;
Vector3 point = _Curve.Evaluate(t);
Vector3 right = RoadCurve.RightOf(_Curve.Tangent(t));
Vector3 left = _Transform.PointToWorld(point - right * _Width.LeftAt(distance));
Vector3 rightEdge = _Transform.PointToWorld(point + right * _Width.RightAt(distance));
if (i == 0 || i == _Steps)
Gizmo.Draw.Line(left, rightEdge);
if (i > 0)
{
Gizmo.Draw.Line(previousLeft, left);
Gizmo.Draw.Line(previousRight, rightEdge);
}
previousLeft = left;
previousRight = rightEdge;
}
}
/// <summary>
/// A node marker: a flat ring on the ground, filled when it is the one being worked on.
/// </summary>
public static void DrawNode(Vector3 _WorldPosition, bool _Filled)
{
float radius = RoadPicker.NodeRadius(_WorldPosition);
if (_Filled)
Gizmo.Draw.SolidSphere(_WorldPosition, radius * 0.6f);
Gizmo.Draw.LineCircle(_WorldPosition, Vector3.Up, radius);
}
}
Game
library
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// The cubic Bezier of one road segment, in network-local space. P1 and P2 are the tangent handles hanging off the
/// two end nodes.
/// </summary>
public readonly struct RoadCurve : IEquatable<RoadCurve>
{
public readonly Vector3 P0;
public readonly Vector3 P1;
public readonly Vector3 P2;
public readonly Vector3 P3;
public RoadCurve(Vector3 _P0, Vector3 _P1, Vector3 _P2, Vector3 _P3)
{
P0 = _P0;
P1 = _P1;
P2 = _P2;
P3 = _P3;
}
public Vector3 Evaluate(float _T)
{
float u = 1.0f - _T;
float uu = u * u;
float tt = _T * _T;
return P0 * (uu * u) + P1 * (3.0f * uu * _T) + P2 * (3.0f * u * tt) + P3 * (tt * _T);
}
/// <summary>
/// Unit direction of travel at <paramref name="_T"/>. Falls back to the chord when the derivative vanishes (a
/// handle sitting on its node) so callers always get a usable frame.
/// </summary>
public Vector3 Tangent(float _T)
{
float u = 1.0f - _T;
Vector3 derivative = (P1 - P0) * (3.0f * u * u) + (P2 - P1) * (6.0f * u * _T) + (P3 - P2) * (3.0f * _T * _T);
if (derivative.IsNearZeroLength)
derivative = P3 - P0;
if (derivative.IsNearZeroLength)
return Vector3.Forward;
return derivative.Normal;
}
/// <summary>
/// Chord-summed length, good enough to size sampling tables and count chunks.
/// </summary>
public float ApproximateLength(int _Steps = 32)
{
float length = 0.0f;
Vector3 previous = P0;
for (int i = 1; i <= _Steps; i++)
{
Vector3 point = Evaluate((float)i / _Steps);
length += Vector3.DistanceBetween(previous, point);
previous = point;
}
return length;
}
/// <summary>
/// De Casteljau subdivision: the two halves trace exactly the same path as the whole, so a road can be split
/// without moving.
/// </summary>
public void Split(float _T, out RoadCurve _Left, out RoadCurve _Right)
{
Vector3 q0 = Vector3.Lerp(P0, P1, _T);
Vector3 q1 = Vector3.Lerp(P1, P2, _T);
Vector3 q2 = Vector3.Lerp(P2, P3, _T);
Vector3 r0 = Vector3.Lerp(q0, q1, _T);
Vector3 r1 = Vector3.Lerp(q1, q2, _T);
Vector3 s = Vector3.Lerp(r0, r1, _T);
_Left = new RoadCurve(P0, q0, r0, s);
_Right = new RoadCurve(s, r1, q2, P3);
}
/// <summary>
/// The road's right-hand direction for a direction of travel: flat, perpendicular to the tangent, and
/// consistent with <see cref="Rotation.Right"/> for a rotation looking down the tangent with world up.
/// </summary>
public static Vector3 RightOf(Vector3 _Tangent)
{
Vector3 right = Vector3.Cross(_Tangent, Vector3.Up);
// Vertical tangent (a ramp seen from nowhere useful): any horizontal axis will do.
if (right.IsNearZeroLength)
return Vector3.Right;
return right.Normal;
}
public bool Equals(RoadCurve _Other)
{
return P0 == _Other.P0 && P1 == _Other.P1 && P2 == _Other.P2 && P3 == _Other.P3;
}
public override bool Equals(object _Obj)
{
return _Obj is RoadCurve other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(P0, P1, P2, P3);
}
}
Game
library
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// Generates the rounded end of a road that stops: a half disc of surface carried on past the last row, with the
/// border swept round it, so a dead end reads as a turning head rather than a road sliced through. The piece is
/// built beyond the road's own end, on the end row itself, so its seam with the last chunk is exact and nothing
/// about the road's length changes.
/// </summary>
internal static class RoadCapBuilder
{
/// <summary>
/// The arc is drawn in steps of about this many degrees.
/// </summary>
private const float ArcStepDegrees = 9.0f;
/// <summary>
/// How deep a road's rounded end reaches past it, as a share of the road's half width. 1 is a half circle.
/// </summary>
public const float DepthFactor = 1.0f;
/// <summary>
/// How far the rounded end of a road reaches past the node, or 0 where the road type does not round its ends.
/// </summary>
public static float Depth(RoadProfile _Profile, in RoadWidth _Width, float _Distance)
{
if (_Profile is null || !_Profile.RoundedEnds)
return 0.0f;
return (_Width.LeftAt(_Distance) + _Width.RightAt(_Distance)) * 0.5f * DepthFactor;
}
/// <summary>
/// The rounded end at one end of a road. <paramref name="_Distance"/> is the end's own distance along the curve
/// (0 or the table's length) and <paramref name="_AtStart"/> which end it is; the cap reaches out from there.
/// </summary>
public static PolygonMesh Build(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, float _Depth)
{
var mesh = new PolygonMesh();
if (_Depth <= 0.01f)
return mesh;
_Table.Frame(_Distance, out Vector3 centre, out Vector3 tangent);
centre += Vector3.Up * _Lift;
// Out of the road, and the road's own right at that end. The arc keeps the road's grade because the
// direction it reaches along carries the curve's climb with it.
Vector3 forward = (_AtStart ? -tangent : tangent).Normal;
Vector3 right = RoadCurve.RightOf(tangent);
float leftHalf = _Width.LeftAt(_Distance);
float rightHalf = _Width.RightAt(_Distance);
if (leftHalf + rightHalf <= 0.01f)
return mesh;
int steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));
var outline = new List<Vector3>(steps + 1);
var outwards = new List<Vector3>(steps + 1);
var offsets = new List<float>(steps + 1);
var aheads = new List<float>(steps + 1);
Arc(centre, forward, right, leftHalf, rightHalf, _Depth, steps, outline, outwards, offsets, aheads);
AddSurface(mesh, centre, outline, offsets, aheads, _Distance, _AtStart, _Profile);
AddBorder(mesh, outline, outwards, _Profile);
return mesh;
}
/// <summary>
/// The outline of a road's rounded end in network space, lifted onto the road surface, for the tools that need
/// to know where road is (the ground under it is held down and brought up to its kerb like a junction's).
/// Empty where the road does not round that end.
/// </summary>
public static List<Vector3> Outline(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, out Vector3 _Centre)
{
var outline = new List<Vector3>();
float depth = Depth(_Profile, _Width, _Distance);
_Table.Frame(_Distance, out _Centre, out Vector3 tangent);
_Centre += Vector3.Up * _Lift;
float leftHalf = _Width.LeftAt(_Distance);
float rightHalf = _Width.RightAt(_Distance);
if (depth <= 0.01f || leftHalf + rightHalf <= 0.01f)
return outline;
int steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));
Arc(_Centre, (_AtStart ? -tangent : tangent).Normal, RoadCurve.RightOf(tangent), leftHalf, rightHalf, depth, steps, outline, null, null, null);
return outline;
}
/// <summary>
/// The half ellipse from the road's left edge, round the tip, to its right edge: where each point is, which way
/// is out of the road there, and how far across and ahead of the end row it sits.
/// </summary>
private static void Arc(Vector3 _Centre, Vector3 _Forward, Vector3 _Right, float _LeftHalf, float _RightHalf, float _Depth, int _Steps,
List<Vector3> _Points, List<Vector3> _Outwards, List<float> _Offsets, List<float> _Aheads)
{
for (int i = 0; i <= _Steps; i++)
{
float angle = MathF.PI * (i / (float)_Steps - 0.5f);
float sin = MathF.Sin(angle);
float cos = MathF.Cos(angle);
float offset = sin * (sin >= 0.0f ? _RightHalf : _LeftHalf);
float ahead = _Depth * cos;
_Points.Add(_Centre + _Right * offset + _Forward * ahead);
_Outwards?.Add((_Right * sin + _Forward * cos).Normal);
_Offsets?.Add(offset);
_Aheads?.Add(ahead);
}
}
/// <summary>
/// The half disc of road surface: a fan from the middle of the end row out to the arc, so every triangle is
/// planar and the row itself is the fan's own base.
/// </summary>
private static void AddSurface(PolygonMesh _Mesh, Vector3 _Centre, List<Vector3> _Outline, List<float> _Offsets, List<float> _Aheads,
float _Distance, bool _AtStart, RoadProfile _Profile)
{
float repeat = MathF.Max(1.0f, _Profile.RoadTextureRepeat);
var material = _Profile.ResolvedRoadMaterial;
var centre = _Mesh.AddVertices(_Centre)[0];
var ring = new HalfEdgeMesh.VertexHandle[_Outline.Count];
for (int i = 0; i < _Outline.Count; i++)
ring[i] = _Mesh.AddVertices(_Outline[i])[0];
// U runs across the road from its centreline and V along it, the same as the road's own surface, so the
// asphalt carries on through the seam instead of restarting at it.
Vector2 centreUv = new(0.0f, _Distance / repeat);
// Which way round the fan closes, so it faces up like the road it carries on from.
Vector3 first = _Outline[0] - _Centre;
Vector3 second = _Outline[1] - _Centre;
bool flip = Vector3.Cross(first, second).z < 0.0f;
for (int i = 0; i < _Outline.Count - 1; i++)
{
var uvA = new Vector2(_Offsets[i] / repeat, (_Distance + (_AtStart ? -_Aheads[i] : _Aheads[i])) / repeat);
var uvB = new Vector2(_Offsets[i + 1] / repeat, (_Distance + (_AtStart ? -_Aheads[i + 1] : _Aheads[i + 1])) / repeat);
if (flip)
MeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i + 1], ring[i], centreUv, uvB, uvA);
else
MeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i], ring[i + 1], centreUv, uvA, uvB);
}
}
/// <summary>
/// The border round the arc, open at both ends: what closes it off is the road's own border, which runs into
/// it at the end row.
/// </summary>
private static void AddBorder(PolygonMesh _Mesh, List<Vector3> _Outline, List<Vector3> _Outwards, RoadProfile _Profile)
{
if (!_Profile.HasBorder)
return;
var path = new List<RoadBorderBuilder.PathPoint>(_Outline.Count);
float along = 0.0f;
for (int i = 0; i < _Outline.Count; i++)
{
if (i > 0)
along += _Outline[i].Distance(_Outline[i - 1]);
path.Add(new RoadBorderBuilder.PathPoint(_Outline[i], _Outwards[i], along));
}
RoadBorderBuilder.BuildPath(_Mesh, _Profile, path, false, false);
}
}
Game
library
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the
/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.
/// </summary>
internal static class RoadJunctionBuilder
{
public static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)
{
var mesh = new PolygonMesh();
var polygon = _Layout.Polygon;
if (polygon.Count < 3)
return mesh;
var centre = mesh.AddVertices(_Layout.Centre)[0];
var ring = mesh.AddVertices(polygon.ToArray());
Vector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);
for (int i = 0; i < polygon.Count; i++)
{
int next = (i + 1) % polygon.Count;
MeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],
centreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));
}
return mesh;
}
/// <summary>
/// Junctions have no single direction to run a texture along, so they use a top-down projection.
/// </summary>
private static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)
{
return new Vector2(_Position.x, _Position.y) / _TextureRepeat;
}
}
Game
library
using System.Text.Json.Serialization;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// How a parking lot is laid out inside: the bays round its edges and, on a lot with room for them, the rows down
/// its middle. Lots reference a style, so every car park built from it changes together when the asset is edited.
/// </summary>
[AssetType(Name = "Parking Style", Extension = "park", Category = "Road Tool 2")]
public sealed class ParkingStyle : GameResource
{
/// <summary>
/// How wide one bay is, measured along the edge it stands on.
/// </summary>
[Group("Bays"), Range(80.0f, 400.0f)] public float BayWidth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 150.0f;
/// <summary>
/// How far a bay reaches in from the edge.
/// </summary>
[Group("Bays"), Range(100.0f, 800.0f)] public float BayDepth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 260.0f;
/// <summary>
/// Bays turned this far from square to the edge they stand on, so cars drive into them at an angle. The bays
/// on the far side of an aisle lean the other way, as they do in a real car park.
/// </summary>
[Group("Bays"), Range(0.0f, 60.0f)] public float BayAngle { get; set { field = value.Clamp(0.0f, 80.0f); } } = 0.0f;
/// <summary>
/// Bays round the inside of the lot's edges.
/// </summary>
[Group("Bays")] public bool EdgeBays { get; set; } = true;
/// <summary>
/// Back to back rows of bays down the middle of the lot, as many as fit with an aisle between each of them.
/// </summary>
[Group("Rows")] public bool CentreRows { get; set; } = true;
/// <summary>
/// How much room is left for driving between one row of bays and the next.
/// </summary>
[Group("Rows"), Range(300.0f, 1500.0f)] public float AisleWidth { get; set { field = value.Clamp(50.0f, 100000.0f); } } = 600.0f;
/// <summary>
/// How the bay lines are painted. Empty paints them like a road's lane lines.
/// </summary>
[Group("Markings")] public RoadLineStyle Line { get; set; } = new() { Width = 8.0f };
/// <summary>
/// A line along the back of each row, closing its bays off.
/// </summary>
[Group("Markings")] public bool BackLine { get; set; } = false;
[Hide, JsonIgnore] public Material ResolvedLineMaterial => Line?.Material ?? Material.Load("roadtool2/materials/linewhite.vmat");
/// <summary>
/// How far apart two bay lines sit along the edge, which is wider than a bay once the bays lean.
/// </summary>
[Hide, JsonIgnore] public float Pitch => BayWidth / float.Cos(BayAngle.DegreeToRadian());
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(BayWidth);
_Hash.Add(BayDepth);
_Hash.Add(BayAngle);
_Hash.Add(EdgeBays ? 1 : 0);
_Hash.Add(CentreRows ? 1 : 0);
_Hash.Add(AisleWidth);
_Hash.Add(BackLine ? 1 : 0);
_Hash.Add(Line is null ? 0 : 1);
Line?.AddToHash(ref _Hash);
}
/// <summary>
/// The asset was saved: every network with a lot in this style has stale pieces now.
/// </summary>
protected override void PostReload()
{
base.PostReload();
RoadNetwork.Editor_MarkDirty();
}
protected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)
{
return CreateSimpleAssetTypeIcon("local_parking", _Width, _Height, "#8fd3ff", "black");
}
}
Game
library
using System.Collections.Generic;
using System.Text;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// What a border piece was built from, kept on the piece while the network's debug overlay is on: one line per row
/// from the kerb to the outer edge, coloured by the run it belongs to, plus a plain-text record of every number
/// that went into the rows, corners and patches (the editor writes those out to a file). Diagnostic only.
/// </summary>
public sealed class BorderDebug
{
public readonly record struct DebugLine(Vector3 From, Vector3 To, Color Color);
public readonly record struct DebugMarker(Vector3 Position, Color Color);
public static readonly Color ArmColor = Color.Green;
public static readonly Color FirstHalfColor = Color.Red;
public static readonly Color SecondHalfColor = Color.Cyan;
public static readonly Color PatchColor = Color.Yellow;
public static readonly Color CapColor = Color.Magenta;
public List<DebugLine> Lines { get; } = new();
public List<DebugMarker> Markers { get; } = new();
/// <summary>
/// The record, one entry per line.
/// </summary>
public StringBuilder Text { get; } = new();
public void Line(Vector3 _From, Vector3 _To, Color _Color)
{
Lines.Add(new DebugLine(_From, _To, _Color));
}
public void Marker(Vector3 _Position, Color _Color)
{
Markers.Add(new DebugMarker(_Position, _Color));
}
public void Note(string _Text)
{
Text.AppendLine(_Text);
}
public static string Format(Vector3 _Vector)
{
return $"({_Vector.x:0.###} {_Vector.y:0.###} {_Vector.z:0.###})";
}
public static string Format(Vector2 _Vector)
{
return $"({_Vector.x:0.###} {_Vector.y:0.###})";
}
}
Game
library
using System;
using System.Linq;
using System.Text;
using Sandbox;
namespace RedSnail.RoadTool2;
public sealed partial class RoadNetwork
{
/// <summary>
/// The whole border debug record as text: the graph and generation settings, then what every border piece was
/// built from. Only pieces built while <see cref="DebugBorders"/> was on carry a record.
/// </summary>
public string BuildDebugRecord()
{
var text = new StringBuilder();
text.AppendLine($"ROADTOOL2 BORDER DEBUG generator={GeneratorVersion} revision={DebugRevision} time={DateTime.Now:HH:mm:ss}");
text.AppendLine($"NETWORK name=\"{GameObject.Name}\" surfaceOffset={SurfaceOffset} spacing={SampleSpacing} chunkLength={ChunkLength} defaultProfile={DefaultProfile?.ResourceName ?? "(none)"}");
foreach (var node in Nodes)
text.AppendLine($"NODE id={node.Id} position={BorderDebug.Format(node.Position)} cornerRadius={node.CornerRadius} autoTangents={(node.AutoTangents ? 1 : 0)} degree={GetDegree(node.Id)}");
foreach (var segment in Segments)
{
text.AppendLine($"SEGMENT id={segment.Id} start={segment.StartNodeId} end={segment.EndNodeId} startTangent={BorderDebug.Format(segment.StartTangent)} endTangent={BorderDebug.Format(segment.EndTangent)} width={segment.Width} profile={ProfileOf(segment).ResourceName}");
if (TryGetCurve(segment, out var curve))
text.AppendLine($"CURVE segment={segment.Id} p0={BorderDebug.Format(curve.P0)} p1={BorderDebug.Format(curve.P1)} p2={BorderDebug.Format(curve.P2)} p3={BorderDebug.Format(curve.P3)} length={GetCurveTable(segment)?.Length ?? 0.0f:0.###}");
}
foreach (var chunk in GetChunks().OrderBy(chunk => chunk.IsJunction ? 1 : 0).ThenBy(chunk => chunk.IsJunction ? chunk.NodeId : chunk.SegmentId).ThenBy(chunk => chunk.ChunkIndex))
{
if (chunk.Kind != RoadChunkKind.Border)
continue;
text.AppendLine();
text.AppendLine($"PIECE name=\"{chunk.GameObject.Name}\" segment={chunk.SegmentId} node={chunk.NodeId} index={chunk.ChunkIndex} count={chunk.ChunkCount} record={(chunk.Debug is not null ? 1 : 0)}");
if (chunk.Debug is not null)
text.Append(chunk.Debug.Text);
}
return text.ToString();
}
}
Debug: View Raw JSON Response
{
"TotalCount": 71,
"Files": [
{
"Ident": "redsnail.roadtool2",
"Path": "Network/RoadBorderTaper.cs",
"FileName": "RoadBorderTaper.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// How one side's border tapers at either end of a segment: over the given length from the node its relief (kerb\r\n/// height, verge drop) runs to the share of itself given for that end \u2014 nothing when the other border is of another\r\n/// kind, the height the two meet at when it is of the same kind \u2014 and its width runs to the width the two meet at,\r\n/// so the two borders meet flush at the node. A zero length is no taper. An end marked as a spill has a country road\r\n/// on the other side, whose gravel is blended onto the border there. A gore end is the nose where this road and\r\n/// another diverge from one edge: the border there grows from nothing as the square of the distance, the way the gap\r\n/// between two roads parting from a common tangent opens up, so the two borders fill the gore between them without\r\n/// crossing.\r\n/// </summary>\r\n/// <param name=\"StartRelief\">\r\n/// The share of the profile's own relief the border has at the start node: 0 flattens it into the road, 1 leaves\r\n/// it as it is, and anything else brings it to a height it shares with the border on the other side (above 1 for\r\n/// the lower of the two).\r\n/// </param>\r\n/// <param name=\"StartOffset\">\r\n/// How far from the start node the taper ends: 0 at a plain pass-through, the arm's trim at a junction, where the\r\n/// corner starts. Everything nearer the node than that \u2014 the corner itself \u2014 is built in the shape the taper runs\r\n/// to, so the two roads round a corner meet in one cross-section instead of turning into each other on the arc.\r\n/// </param>\r\npublic readonly record struct RoadBorderTaper(float StartLength, float StartWidth, float StartRelief, bool StartSpill, float EndLength, float EndWidth, float EndRelief, bool EndSpill, bool StartGore = false, bool EndGore = false, float StartOffset = 0.0f, float EndOffset = 0.0f)\r\n{\r\n\t/// <summary>\r\n\t/// The share of the profile's relief in force at a curve distance (1 away from any taper, that end's relief at\r\n\t/// its node, and beyond its offset), how far along a taper the point is (0 outside one), the width the border is\r\n\t/// that far towards, and whether that taper's neighbour spills onto it. The two ends' tapers, offsets included,\r\n\t/// are each held to half the segment, so no point is ever inside both.\r\n\t/// </summary>\r\n\tpublic float ReliefAt(float _Along, float _Length, out float _Towards, out float _TargetWidth, out bool _Spill)\r\n\t{\r\n\t\tfloat relief = 1.0f;\r\n\r\n\t\t_Towards = 0.0f;\r\n\t\t_TargetWidth = 0.0f;\r\n\t\t_Spill = false;\r\n\r\n\t\tif (StartLength > 0.0f && _Along < StartOffset + StartLength)\r\n\t\t{\r\n\t\t\tfloat progress = Math.Clamp((_Along - StartOffset) / StartLength, 0.0f, 1.0f);\r\n\r\n\t\t\t_Towards = Towards(progress, StartGore);\r\n\t\t\t_TargetWidth = StartWidth;\r\n\t\t\t_Spill = StartSpill;\r\n\t\t\trelief = MathX.Lerp(StartRelief, 1.0f, progress);\r\n\t\t}\r\n\r\n\t\tif (EndLength > 0.0f && _Length - _Along < EndOffset + EndLength)\r\n\t\t{\r\n\t\t\tfloat progress = Math.Clamp((_Length - _Along - EndOffset) / EndLength, 0.0f, 1.0f);\r\n\t\t\tfloat towards = Towards(progress, EndGore);\r\n\r\n\t\t\tif (towards > _Towards)\r\n\t\t\t{\r\n\t\t\t\t_Towards = towards;\r\n\t\t\t\t_TargetWidth = EndWidth;\r\n\t\t\t\t_Spill = EndSpill;\r\n\t\t\t\trelief = MathX.Lerp(EndRelief, 1.0f, progress);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn relief;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How far towards the node's width the border is at a progress along the taper (0 at the node, 1 at its far\r\n\t/// end): straight along a plain taper, and along a gore the square, so the width is the square of the progress.\r\n\t/// </summary>\r\n\tprivate static float Towards(float _Progress, bool _Gore)\r\n\t{\r\n\t\treturn _Gore ? 1.0f - _Progress * _Progress : 1.0f - _Progress;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The factor the border's nominal width is scaled by at a curve distance, on its way to the neighbour's.\r\n\t/// </summary>\r\n\tpublic float WidthScaleAt(float _Along, float _Length, float _NominalWidth)\r\n\t{\r\n\t\tReliefAt(_Along, _Length, out float towards, out float targetWidth, out _);\r\n\r\n\t\tif (towards <= 0.0f || _NominalWidth <= 0.001f)\r\n\t\t\treturn 1.0f;\r\n\r\n\t\treturn (_NominalWidth + (targetWidth - _NominalWidth) * towards) / _NominalWidth;\r\n\t}\r\n\r\n\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(StartLength);\r\n\t\t_Hash.Add(StartWidth);\r\n\t\t_Hash.Add(StartRelief);\r\n\t\t_Hash.Add(StartSpill ? 1 : 0);\r\n\t\t_Hash.Add(StartGore ? 1 : 0);\r\n\t\t_Hash.Add(StartOffset);\r\n\t\t_Hash.Add(EndLength);\r\n\t\t_Hash.Add(EndWidth);\r\n\t\t_Hash.Add(EndRelief);\r\n\t\t_Hash.Add(EndSpill ? 1 : 0);\r\n\t\t_Hash.Add(EndGore ? 1 : 0);\r\n\t\t_Hash.Add(EndOffset);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Whether the border tapers at the end a node is at.\r\n\t/// </summary>\r\n\tpublic bool TapersAt(bool _AtStart)\r\n\t{\r\n\t\treturn _AtStart ? StartLength > 0.0f : EndLength > 0.0f;\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// The tapers of a segment's two borders, looking along the segment.\r\n/// </summary>\r\npublic readonly record struct SegmentBorderTapers(RoadBorderTaper Left, RoadBorderTaper Right)\r\n{\r\n\tpublic RoadBorderTaper Side(bool _Left)\r\n\t{\r\n\t\treturn _Left ? Left : Right;\r\n\t}\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\tLeft.AddToHash(ref _Hash);\r\n\t\tRight.AddToHash(ref _Hash);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Network/RoadSurfaceBuilder.cs",
"FileName": "RoadSurfaceBuilder.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the road surface of one chunk: a strip of quads between cross-section frames at the rows the chunk is\r\n/// built on (even distances along the segment's curve, thinned where the road type allows it).\r\n/// </summary>\r\ninternal static class RoadSurfaceBuilder\r\n{\r\n\t/// <summary>\r\n\t/// The two edge points of the road at a distance along the curve, each its own distance out from the centreline\r\n\t/// there. Junctions use this for the rows they share with the arm chunks, so both sides of the seam are computed\r\n\t/// the same way.\r\n\t/// </summary>\r\n\tpublic static void CrossSection(RoadCurveTable _Table, float _Distance, in RoadWidth _Width, float _Lift, out Vector3 _Left, out Vector3 _Right)\r\n\t{\r\n\t\t_Table.Frame(_Distance, out Vector3 position, out Vector3 tangent);\r\n\r\n\t\tposition += Vector3.Up * _Lift;\r\n\r\n\t\tVector3 right = RoadCurve.RightOf(tangent);\r\n\r\n\t\t_Left = position - right * _Width.LeftAt(_Distance);\r\n\t\t_Right = position + right * _Width.RightAt(_Distance);\r\n\t}\r\n\r\n\r\n\r\n\tpublic static PolygonMesh Build(RoadCurveTable _Table, float _StartDistance, float _EndDistance, in RoadWidth _Width, float _Spacing, in RoadSimplify _Simplify, float _TextureRepeat, float _SurfaceOffset, Material _Material)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\r\n\t\t// The rows the borders and the bridge are built on too, so the pieces meet exactly.\r\n\t\tvar rows = RoadRows.Build(_Table, _StartDistance, _EndDistance, _Spacing, _Width, _Simplify);\r\n\t\tfloat repeat = MathF.Max(1.0f, _TextureRepeat);\r\n\r\n\t\tvar left = new HalfEdgeMesh.VertexHandle[rows.Count];\r\n\t\tvar right = new HalfEdgeMesh.VertexHandle[rows.Count];\r\n\t\tvar uvLeft = new Vector2[rows.Count];\r\n\t\tvar uvRight = new Vector2[rows.Count];\r\n\r\n\t\tfor (int i = 0; i < rows.Count; i++)\r\n\t\t{\r\n\t\t\tfloat distance = rows[i];\r\n\r\n\t\t\tCrossSection(_Table, distance, _Width, _SurfaceOffset, out Vector3 leftPos, out Vector3 rightPos);\r\n\r\n\t\t\tvar handles = mesh.AddVertices(leftPos, rightPos);\r\n\t\t\tleft[i] = handles[0];\r\n\t\t\tright[i] = handles[1];\r\n\r\n\t\t\t// U runs across the road from the centreline, V along it, so a tiling material follows the road instead\r\n\t\t\t// of the world, and an edge that moves out takes its texture with it.\r\n\t\t\tfloat v = distance / repeat;\r\n\t\t\tuvLeft[i] = new Vector2(-_Width.LeftAt(distance) / repeat, v);\r\n\t\t\tuvRight[i] = new Vector2(_Width.RightAt(distance) / repeat, v);\r\n\t\t}\r\n\r\n\t\t// Two triangles rather than a quad: a road that turns as it climbs has cells whose four corners are not\r\n\t\t// coplanar, and then it matters exactly how the cell is split, both here and for the paint laid on top.\r\n\t\tfor (int i = 0; i < rows.Count - 1; i++)\r\n\t\t{\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i], right[i + 1], uvLeft[i], uvRight[i], uvRight[i + 1]);\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i + 1], left[i + 1], uvLeft[i], uvRight[i + 1], uvLeft[i + 1]);\r\n\t\t}\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of one cell of road surface, as it is built, over a point in plan: the cell is split from its\r\n\t/// near left corner to its far right one, so which of the two triangles the point falls on is which side of\r\n\t/// that diagonal it lies. The triangle's plane is used even for a point just off the cell (paint hanging over\r\n\t/// the road's edge), which keeps that paint on the surface's own plane.\r\n\t/// </summary>\r\n\tpublic static float CellHeight(Vector3 _NearLeft, Vector3 _NearRight, Vector3 _FarRight, Vector3 _FarLeft, Vector2 _Point)\r\n\t{\r\n\t\tVector2 diagonal = Flat(_FarRight) - Flat(_NearLeft);\r\n\t\tfloat side = Cross(diagonal, _Point - Flat(_NearLeft));\r\n\t\tfloat nearRightSide = Cross(diagonal, Flat(_NearRight) - Flat(_NearLeft));\r\n\r\n\t\treturn side * nearRightSide >= 0.0f\r\n\t\t\t? PlaneHeight(_NearLeft, _NearRight, _FarRight, _Point)\r\n\t\t\t: PlaneHeight(_NearLeft, _FarRight, _FarLeft, _Point);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of the plane through three points over a point in plan.\r\n\t/// </summary>\r\n\tprivate static float PlaneHeight(Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _Point)\r\n\t{\r\n\t\tVector2 a = Flat(_A);\r\n\t\tVector2 b = Flat(_B);\r\n\t\tVector2 c = Flat(_C);\r\n\t\tfloat area = Cross(b - a, c - a);\r\n\r\n\t\tif (MathF.Abs(area) < 0.0001f)\r\n\t\t\treturn _A.z;\r\n\r\n\t\tfloat weightA = Cross(b - _Point, c - _Point) / area;\r\n\t\tfloat weightB = Cross(c - _Point, a - _Point) / area;\r\n\r\n\t\treturn weightA * _A.z + weightB * _B.z + (1.0f - weightA - weightB) * _C.z;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static Vector2 Flat(Vector3 _Vector)\r\n\t{\r\n\t\treturn new Vector2(_Vector.x, _Vector.y);\r\n\t}\r\n\r\n\r\n\r\n\tprivate static float Cross(Vector2 _A, Vector2 _B)\r\n\t{\r\n\t\treturn _A.x * _B.y - _A.y * _B.x;\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Editor/RoadBorderDebugDump.cs",
"FileName": "RoadBorderDebugDump.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2.Editor;\r\n\r\n/// <summary>\r\n/// Writes each network's border debug record to Libraries/RoadTool2/debug/borders.txt whenever its border pieces\r\n/// have been rebuilt with the overlay on, so the numbers can be read outside the editor.\r\n/// </summary>\r\npublic static class RoadBorderDebugDump\r\n{\r\n\tprivate static readonly Dictionary<Guid, int> s_Written = new();\r\n\r\n\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic static void OnFrame()\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\r\n\t\tif (scene is null)\r\n\t\t\treturn;\r\n\r\n\t\tvar network = scene.Get<RoadNetwork>();\r\n\r\n\t\tif (!network.IsValid() || !network.DebugBorders || network.IsRebuilding)\r\n\t\t\treturn;\r\n\r\n\t\tif (s_Written.TryGetValue(network.Id, out int revision) && revision == network.DebugRevision)\r\n\t\t\treturn;\r\n\r\n\t\ts_Written[network.Id] = network.DebugRevision;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tstring path = GetPath(network);\r\n\r\n\t\t\tDirectory.CreateDirectory(Path.GetDirectoryName(path));\r\n\t\t\tFile.WriteAllText(path, network.BuildDebugRecord());\r\n\r\n\t\t\tLog.Info($\"Road Tool 2: border debug written to {path}\");\r\n\t\t}\r\n\t\tcatch (Exception exception)\r\n\t\t{\r\n\t\t\tLog.Warning($\"Road Tool 2: could not write the border debug record: {exception.Message}\");\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate static string GetPath(RoadNetwork _Network)\r\n\t{\r\n\t\tstring root = Project.Current.GetRootPath();\r\n\t\tstring library = Path.Combine(root, \"Libraries\", \"RoadTool2\");\r\n\r\n\t\t// The library folder may have been renamed; find it by its project file.\r\n\t\tif (!Directory.Exists(library))\r\n\t\t{\r\n\t\t\tforeach (string directory in Directory.GetDirectories(Path.Combine(root, \"Libraries\")))\r\n\t\t\t{\r\n\t\t\t\tif (File.Exists(Path.Combine(directory, \"roadtool2.sbproj\")))\r\n\t\t\t\t{\r\n\t\t\t\t\tlibrary = directory;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tstring name = string.Concat(_Network.GameObject.Name.Split(Path.GetInvalidFileNameChars())).Replace(' ', '_');\r\n\r\n\t\treturn Path.Combine(library, \"debug\", $\"borders_{name}.txt\");\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Profiles/BridgeStyle.cs",
"FileName": "BridgeStyle.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System.Text.Json.Serialization;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// What stands along the two edges of a bridge deck.\r\n/// </summary>\r\npublic enum BridgeSideStyle\r\n{\r\n\t/// <summary>\r\n\t/// Nothing above the deck: its side face is all there is.\r\n\t/// </summary>\r\n\tNone,\r\n\r\n\t/// <summary>\r\n\t/// A solid parapet wall.\r\n\t/// </summary>\r\n\tWall,\r\n\r\n\t/// <summary>\r\n\t/// Posts at intervals with horizontal rails running between them.\r\n\t/// </summary>\r\n\tRailing,\r\n\r\n\t/// <summary>\r\n\t/// A prefab of your own repeated along the edge (a fence panel, a lamp, a cable anchor...).\r\n\t/// </summary>\r\n\tPrefab\r\n}\r\n\r\n/// <summary>\r\n/// What holds a bridge deck up.\r\n/// </summary>\r\npublic enum BridgeSupportStyle\r\n{\r\n\tNone,\r\n\r\n\t/// <summary>\r\n\t/// Pillars at intervals, from the underside of the deck down into the ground.\r\n\t/// </summary>\r\n\tPillars,\r\n\r\n\t/// <summary>\r\n\t/// A prefab of your own at intervals under the deck (a pier, a tower...).\r\n\t/// </summary>\r\n\tPrefab\r\n}\r\n\r\npublic enum BridgePillarShape\r\n{\r\n\tSquare,\r\n\tRound\r\n}\r\n\r\n/// <summary>\r\n/// One pillar under the middle of the deck, or one under each edge.\r\n/// </summary>\r\npublic enum BridgePillarLayout\r\n{\r\n\tSingle,\r\n\tPair\r\n}\r\n\r\n/// <summary>\r\n/// How a stretch of road becomes a bridge: the deck under it, what stands along the deck's edges, and what holds it\r\n/// up. Each part is picked from a set of built-in designs or handed to a prefab of your own, so new kinds of bridge\r\n/// (cable-stayed, trusses...) are new designs of a part rather than a new kind of road. Roads reference a style, so\r\n/// every bridge built from it changes together when the asset is edited.\r\n/// </summary>\r\n[AssetType(Name = \"Bridge Style\", Extension = \"bridg\", Category = \"Road Tool 2\")]\r\npublic sealed class BridgeStyle : GameResource\r\n{\r\n\tprivate const string DefaultMaterial = \"roadtool2/materials/concrete.vmat\";\r\n\r\n\t/// <summary>\r\n\t/// Material of the deck's sides and underside (and of the walls and pillars when they have none of their own).\r\n\t/// </summary>\r\n\t[Group(\"Deck\")] public Material DeckMaterial { get; set; }\r\n\t[Group(\"Deck\"), Range(10.0f, 10000.0f)] public float DeckTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 500.0f;\r\n\r\n\t/// <summary>\r\n\t/// The deck's thickness below the road surface.\r\n\t/// </summary>\r\n\t[Group(\"Deck\"), Range(5.0f, 500.0f)] public float DeckDepth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 60.0f;\r\n\r\n\t/// <summary>\r\n\t/// A bare ledge of deck outside the side (wall, railing) on each edge. 0 ends the deck at the side's outer face.\r\n\t/// </summary>\r\n\t[Group(\"Deck\"), Range(0.0f, 300.0f)] public float DeckOverhang { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 0.0f;\r\n\r\n\t[Group(\"Side\")] public BridgeSideStyle Side { get; set; } = BridgeSideStyle.Wall;\r\n\r\n\t/// <summary>\r\n\t/// Material of the wall or the railing. Empty uses the deck's.\r\n\t/// </summary>\r\n\t[Group(\"Side\"), ShowIf(nameof(HasBuiltSide), true)] public Material SideMaterial { get; set; }\r\n\t[Group(\"Side\"), ShowIf(nameof(HasBuiltSide), true), Range(10.0f, 10000.0f)] public float SideTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 100.0f;\r\n\r\n\t/// <summary>\r\n\t/// The wall's thickness, or the railing's post size; also the width of the strip a prefab side stands on.\r\n\t/// </summary>\r\n\t[Group(\"Side\"), HideIf(nameof(Side), BridgeSideStyle.None), Range(1.0f, 200.0f)] public float SideWidth { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 20.0f;\r\n\r\n\t/// <summary>\r\n\t/// Height of the wall or railing above the road's border (the pavement top when there is one).\r\n\t/// </summary>\r\n\t[Group(\"Side\"), ShowIf(nameof(HasBuiltSide), true), Range(5.0f, 300.0f)] public float SideHeight { get; set { field = value.Clamp(0.1f, 10000.0f); } } = 45.0f;\r\n\r\n\t[Group(\"Railing\"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(50.0f, 1000.0f)] public float PostSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;\r\n\t[Group(\"Railing\"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1, 6)] public int RailCount { get; set { field = value.Clamp(1, 32); } } = 2;\r\n\t[Group(\"Railing\"), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1.0f, 50.0f)] public float RailSize { get; set { field = value.Clamp(0.1f, 1000.0f); } } = 6.0f;\r\n\r\n\t/// <summary>\r\n\t/// Placed on the deck's edge strip with its forward along the road and its right pointing away from the road.\r\n\t/// </summary>\r\n\t[Group(\"Side\"), ShowIf(nameof(Side), BridgeSideStyle.Prefab)] public GameObject SidePrefab { get; set; }\r\n\t[Group(\"Side\"), ShowIf(nameof(Side), BridgeSideStyle.Prefab), Range(10.0f, 2000.0f)] public float SideSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;\r\n\r\n\t[Group(\"Supports\")] public BridgeSupportStyle Supports { get; set; } = BridgeSupportStyle.Pillars;\r\n\t[Group(\"Supports\"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(200.0f, 10000.0f)] public float SupportSpacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1500.0f;\r\n\r\n\t/// <summary>\r\n\t/// The pillars' material. Empty uses the deck's.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public Material SupportMaterial { get; set; }\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarShape PillarShape { get; set; } = BridgePillarShape.Square;\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarLayout PillarLayout { get; set; } = BridgePillarLayout.Single;\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(10.0f, 1000.0f)] public float PillarSize { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 100.0f;\r\n\r\n\t/// <summary>\r\n\t/// For a pair of pillars: how far in from the deck's edges they stand.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(0.0f, 500.0f)] public float PillarInset { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 20.0f;\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(3, 32)] public int PillarSegments { get; set { field = value.Clamp(3, 64); } } = 12;\r\n\r\n\t/// <summary>\r\n\t/// How far a pillar carries on below the ground it lands on, so it never floats off a slope.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(0.0f, 500.0f)] public float SupportSink { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 50.0f;\r\n\r\n\t/// <summary>\r\n\t/// How far below the deck a support reaches when there is no ground under it to land on.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), HideIf(nameof(Supports), BridgeSupportStyle.None), Range(100.0f, 10000.0f)] public float SupportFallbackHeight { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1000.0f;\r\n\r\n\t/// <summary>\r\n\t/// Placed on the ground under the middle of the deck (or the fallback height below it), with its forward along\r\n\t/// the road. It is up to the prefab to reach the deck.\r\n\t/// </summary>\r\n\t[Group(\"Supports\"), ShowIf(nameof(Supports), BridgeSupportStyle.Prefab)] public GameObject SupportPrefab { get; set; }\r\n\r\n\t[Hide, JsonIgnore] public bool HasBuiltSide => Side is BridgeSideStyle.Wall or BridgeSideStyle.Railing;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedDeckMaterial => DeckMaterial ?? Material.Load(DefaultMaterial);\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedSideMaterial => SideMaterial ?? ResolvedDeckMaterial;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedSupportMaterial => SupportMaterial ?? ResolvedDeckMaterial;\r\n\r\n\t/// <summary>\r\n\t/// How wide the strip along each deck edge is that the side occupies (nothing for a bare deck).\r\n\t/// </summary>\r\n\t[Hide, JsonIgnore] public float SideStripWidth => Side == BridgeSideStyle.None ? 0.0f : SideWidth;\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Folds everything the bridge geometry is built from into a hash, so pieces built with an older version of\r\n\t/// the style are noticed.\r\n\t/// </summary>\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(DeckMaterial?.ResourcePath);\r\n\t\t_Hash.Add(DeckTextureRepeat);\r\n\t\t_Hash.Add(DeckDepth);\r\n\t\t_Hash.Add(DeckOverhang);\r\n\t\t_Hash.Add((int)Side);\r\n\t\t_Hash.Add(SideMaterial?.ResourcePath);\r\n\t\t_Hash.Add(SideTextureRepeat);\r\n\t\t_Hash.Add(SideWidth);\r\n\t\t_Hash.Add(SideHeight);\r\n\t\t_Hash.Add(PostSpacing);\r\n\t\t_Hash.Add(RailCount);\r\n\t\t_Hash.Add(RailSize);\r\n\t\t_Hash.Add(RoadPropSet.PrefabKey(SidePrefab));\r\n\t\t_Hash.Add(SideSpacing);\r\n\t\t_Hash.Add((int)Supports);\r\n\t\t_Hash.Add(SupportSpacing);\r\n\t\t_Hash.Add(SupportMaterial?.ResourcePath);\r\n\t\t_Hash.Add((int)PillarShape);\r\n\t\t_Hash.Add((int)PillarLayout);\r\n\t\t_Hash.Add(PillarSize);\r\n\t\t_Hash.Add(PillarInset);\r\n\t\t_Hash.Add(PillarSegments);\r\n\t\t_Hash.Add(SupportSink);\r\n\t\t_Hash.Add(SupportFallbackHeight);\r\n\t\t_Hash.Add(RoadPropSet.PrefabKey(SupportPrefab));\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The asset was saved: every network with a bridge in this style has stale pieces now.\r\n\t/// </summary>\r\n\tprotected override void PostReload()\r\n\t{\r\n\t\tbase.PostReload();\r\n\r\n\t\tRoadNetwork.Editor_MarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\r\n\t{\r\n\t\treturn CreateSimpleAssetTypeIcon(\"architecture\", _Width, _Height, \"#7fb2ff\", \"black\");\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Utility/MeshUtility.cs",
"FileName": "MeshUtility.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\npublic static class MeshUtility\r\n{\r\n\tpublic static HalfEdgeMesh.VertexHandle GetOrAddVertex(PolygonMesh _Mesh, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _Pos)\r\n\t{\r\n\t\tif (!_Cache.TryGetValue(_Pos, out var handle))\r\n\t\t{\r\n\t\t\thandle = _Mesh.AddVertices(_Pos)[0];\r\n\t\t\t_Cache[_Pos] = handle;\r\n\t\t}\r\n\r\n\t\treturn handle;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Adds a quad face; returns an invalid handle when the mesh refused it (a duplicate vertex, a topology clash).\r\n\t/// </summary>\r\n\tpublic static HalfEdgeMesh.FaceHandle AddTexturedQuad(PolygonMesh _Mesh, Material _Material,\r\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\r\n\t\tHalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D,\r\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)\r\n\t{\r\n\t\tif (HasDuplicateVertex(_A, _B, _C, _D))\r\n\t\t\treturn default;\r\n\r\n\t\tvar face = _Mesh.AddFace(_A, _B, _C, _D);\r\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn default;\r\n\r\n\t\t_Mesh.SetFaceMaterial(face, _Material);\r\n\t\t_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC, _UvD });\r\n\r\n\t\treturn face;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Adds a triangle face; returns an invalid handle when the mesh refused it.\r\n\t/// </summary>\r\n\tpublic static HalfEdgeMesh.FaceHandle AddTexturedTriangle(PolygonMesh _Mesh, Material _Material,\r\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\r\n\t\tHalfEdgeMesh.VertexHandle _C,\r\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC)\r\n\t{\r\n\t\tif (HasDuplicateVertex(_A, _B, _C))\r\n\t\t\treturn default;\r\n\r\n\t\tvar face = _Mesh.AddFace(_A, _B, _C);\r\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn default;\r\n\r\n\t\t_Mesh.SetFaceMaterial(face, _Material);\r\n\t\t_Mesh.SetFaceTextureCoords(face, new List<Vector2> { _UvA, _UvB, _UvC });\r\n\r\n\t\treturn face;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Paints a face's vertices with blend weights (what a blendable material mixes its layers by), in the order\r\n\t/// the face's vertices were given.\r\n\t/// </summary>\r\n\tpublic static void SetFaceBlends(PolygonMesh _Mesh, HalfEdgeMesh.FaceHandle _Face, IReadOnlyList<Color32> _Blends)\r\n\t{\r\n\t\tif (!_Face.IsValid || !_Mesh.GetFaceVerticesConnectedToFace(_Face, out var edges))\r\n\t\t\treturn;\r\n\r\n\t\tint count = System.Math.Min(edges.Length, _Blends.Count);\r\n\r\n\t\tfor (int i = 0; i < count; i++)\r\n\t\t\t_Mesh.SetVertexBlend(edges[i], _Blends[i]);\r\n\t}\r\n\r\n\r\n\r\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C)\r\n\t{\r\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _B.Equals(_C);\r\n\t}\r\n\r\n\r\n\r\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D)\r\n\t{\r\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _A.Equals(_D) ||\r\n\t\t\t_B.Equals(_C) || _B.Equals(_D) ||\r\n\t\t\t_C.Equals(_D);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Geometry/RoadCurveTable.cs",
"FileName": "RoadCurveTable.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Arc-length table over a <see cref=\"RoadCurve\"/>, so the road can be sampled at even distances (mesh rows, chunk\r\n/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a\r\n/// polyline for picking.\r\n/// </summary>\r\npublic sealed class RoadCurveTable\r\n{\r\n\tprivate readonly Vector3[] m_Positions;\r\n\tprivate readonly float[] m_Distances;\r\n\r\n\tpublic RoadCurve Curve { get; }\r\n\r\n\tpublic float Length => m_Distances[^1];\r\n\r\n\tpublic int SampleCount => m_Positions.Length;\r\n\r\n\t/// <summary>\r\n\t/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road\r\n\t/// altogether when the cursor's ray misses it, which is most of them in a city.\r\n\t/// </summary>\r\n\tpublic BBox Bounds { get; }\r\n\r\n\r\n\r\n\tpublic RoadCurveTable(RoadCurve _Curve, int _Samples)\r\n\t{\r\n\t\tCurve = _Curve;\r\n\r\n\t\tint count = Math.Max(1, _Samples);\r\n\t\tm_Positions = new Vector3[count + 1];\r\n\t\tm_Distances = new float[count + 1];\r\n\t\tm_Positions[0] = _Curve.P0;\r\n\r\n\t\tvar bounds = new BBox(_Curve.P0, _Curve.P0);\r\n\r\n\t\tfor (int i = 1; i <= count; i++)\r\n\t\t{\r\n\t\t\tm_Positions[i] = _Curve.Evaluate((float)i / count);\r\n\t\t\tm_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);\r\n\t\t\tbounds = bounds.AddPoint(m_Positions[i]);\r\n\t\t}\r\n\r\n\t\tBounds = bounds;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Builds a table with a sample density that suits the curve's length.\r\n\t/// </summary>\r\n\tpublic static RoadCurveTable Create(RoadCurve _Curve)\r\n\t{\r\n\t\tint samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);\r\n\r\n\t\treturn new RoadCurveTable(_Curve, samples);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 SamplePosition(int _Index)\r\n\t{\r\n\t\treturn m_Positions[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float SampleDistance(int _Index)\r\n\t{\r\n\t\treturn m_Distances[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float DistanceToT(float _Distance)\r\n\t{\r\n\t\tif (_Distance <= 0.0f)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tif (_Distance >= Length)\r\n\t\t\treturn 1.0f;\r\n\r\n\t\tint low = 0;\r\n\t\tint high = m_Distances.Length - 1;\r\n\r\n\t\twhile (high - low > 1)\r\n\t\t{\r\n\t\t\tint mid = (low + high) / 2;\r\n\r\n\t\t\tif (m_Distances[mid] <= _Distance)\r\n\t\t\t\tlow = mid;\r\n\t\t\telse\r\n\t\t\t\thigh = mid;\r\n\t\t}\r\n\r\n\t\tfloat span = m_Distances[high] - m_Distances[low];\r\n\t\tfloat fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;\r\n\r\n\t\treturn (low + fraction) / (m_Distances.Length - 1);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 PositionAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Evaluate(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 TangentAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Tangent(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where the road is at a distance and which way it runs there, in one go: everything that lays something\r\n\t/// across the road wants both, and asking for them separately searches the table twice.\r\n\t/// </summary>\r\n\tpublic void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)\r\n\t{\r\n\t\tfloat t = DistanceToT(_Distance);\r\n\r\n\t\t_Position = Curve.Evaluate(t);\r\n\t\t_Tangent = Curve.Tangent(t);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Network/RoadJunctionBuilder.cs",
"FileName": "RoadJunctionBuilder.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the\r\n/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.\r\n/// </summary>\r\ninternal static class RoadJunctionBuilder\r\n{\r\n\tpublic static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\t\tvar polygon = _Layout.Polygon;\r\n\r\n\t\tif (polygon.Count < 3)\r\n\t\t\treturn mesh;\r\n\r\n\t\tvar centre = mesh.AddVertices(_Layout.Centre)[0];\r\n\t\tvar ring = mesh.AddVertices(polygon.ToArray());\r\n\t\tVector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);\r\n\r\n\t\tfor (int i = 0; i < polygon.Count; i++)\r\n\t\t{\r\n\t\t\tint next = (i + 1) % polygon.Count;\r\n\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],\r\n\t\t\t\tcentreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));\r\n\t\t}\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Junctions have no single direction to run a texture along, so they use a top-down projection.\r\n\t/// </summary>\r\n\tprivate static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)\r\n\t{\r\n\t\treturn new Vector2(_Position.x, _Position.y) / _TextureRepeat;\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": ".obj/__compiler_extra.cs",
"FileName": "__compiler_extra.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Road Tool\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"roadtool2\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"redsnail\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"redsnail.roadtool2\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"30\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-10-18T00:54:30.0847204Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.166.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.166.0\")]"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Geometry/RoadCurveTable.cs",
"FileName": "RoadCurveTable.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Arc-length table over a <see cref=\"RoadCurve\"/>, so the road can be sampled at even distances (mesh rows, chunk\r\n/// boundaries) instead of at even parameter values, which bunch up around tight bends. The samples double as a\r\n/// polyline for picking.\r\n/// </summary>\r\npublic sealed class RoadCurveTable\r\n{\r\n\tprivate readonly Vector3[] m_Positions;\r\n\tprivate readonly float[] m_Distances;\r\n\r\n\tpublic RoadCurve Curve { get; }\r\n\r\n\tpublic float Length => m_Distances[^1];\r\n\r\n\tpublic int SampleCount => m_Positions.Length;\r\n\r\n\t/// <summary>\r\n\t/// The box the curve itself runs through. Picking widens it by the road's own reach and skips the road\r\n\t/// altogether when the cursor's ray misses it, which is most of them in a city.\r\n\t/// </summary>\r\n\tpublic BBox Bounds { get; }\r\n\r\n\r\n\r\n\tpublic RoadCurveTable(RoadCurve _Curve, int _Samples)\r\n\t{\r\n\t\tCurve = _Curve;\r\n\r\n\t\tint count = Math.Max(1, _Samples);\r\n\t\tm_Positions = new Vector3[count + 1];\r\n\t\tm_Distances = new float[count + 1];\r\n\t\tm_Positions[0] = _Curve.P0;\r\n\r\n\t\tvar bounds = new BBox(_Curve.P0, _Curve.P0);\r\n\r\n\t\tfor (int i = 1; i <= count; i++)\r\n\t\t{\r\n\t\t\tm_Positions[i] = _Curve.Evaluate((float)i / count);\r\n\t\t\tm_Distances[i] = m_Distances[i - 1] + Vector3.DistanceBetween(m_Positions[i - 1], m_Positions[i]);\r\n\t\t\tbounds = bounds.AddPoint(m_Positions[i]);\r\n\t\t}\r\n\r\n\t\tBounds = bounds;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Builds a table with a sample density that suits the curve's length.\r\n\t/// </summary>\r\n\tpublic static RoadCurveTable Create(RoadCurve _Curve)\r\n\t{\r\n\t\tint samples = Math.Clamp((int)(_Curve.ApproximateLength() / 20.0f), 16, 1024);\r\n\r\n\t\treturn new RoadCurveTable(_Curve, samples);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 SamplePosition(int _Index)\r\n\t{\r\n\t\treturn m_Positions[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float SampleDistance(int _Index)\r\n\t{\r\n\t\treturn m_Distances[_Index];\r\n\t}\r\n\r\n\r\n\r\n\tpublic float DistanceToT(float _Distance)\r\n\t{\r\n\t\tif (_Distance <= 0.0f)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tif (_Distance >= Length)\r\n\t\t\treturn 1.0f;\r\n\r\n\t\tint low = 0;\r\n\t\tint high = m_Distances.Length - 1;\r\n\r\n\t\twhile (high - low > 1)\r\n\t\t{\r\n\t\t\tint mid = (low + high) / 2;\r\n\r\n\t\t\tif (m_Distances[mid] <= _Distance)\r\n\t\t\t\tlow = mid;\r\n\t\t\telse\r\n\t\t\t\thigh = mid;\r\n\t\t}\r\n\r\n\t\tfloat span = m_Distances[high] - m_Distances[low];\r\n\t\tfloat fraction = span > 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;\r\n\r\n\t\treturn (low + fraction) / (m_Distances.Length - 1);\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 PositionAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Evaluate(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 TangentAt(float _Distance)\r\n\t{\r\n\t\treturn Curve.Tangent(DistanceToT(_Distance));\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where the road is at a distance and which way it runs there, in one go: everything that lays something\r\n\t/// across the road wants both, and asking for them separately searches the table twice.\r\n\t/// </summary>\r\n\tpublic void Frame(float _Distance, out Vector3 _Position, out Vector3 _Tangent)\r\n\t{\r\n\t\tfloat t = DistanceToT(_Distance);\r\n\r\n\t\t_Position = Curve.Evaluate(t);\r\n\t\t_Tangent = Curve.Tangent(t);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Geometry/RoadWidth.cs",
"FileName": "RoadWidth.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// How far a road's two edges sit from its centreline along a segment: half the road's width on each side, plus\r\n/// whatever extra each side has at the segment's start and end, blended smoothly between the two. That is what lets\r\n/// a lane appear on one side over a taper (a highway exit or entry), a ramp start as a point and grow, or a road\r\n/// flare out to a wider one.\r\n/// </summary>\r\npublic readonly struct RoadWidth\r\n{\r\n\t/// <summary>\r\n\t/// No road is built narrower than this: two edges on one point make faces with no area.\r\n\t/// </summary>\r\n\tprivate const float MinWidth = 2.0f;\r\n\r\n\tpublic readonly float Base;\r\n\tpublic readonly float LeftStart;\r\n\tpublic readonly float LeftEnd;\r\n\tpublic readonly float RightStart;\r\n\tpublic readonly float RightEnd;\r\n\tpublic readonly float Length;\r\n\r\n\tpublic RoadWidth(float _Base, float _LeftStart, float _LeftEnd, float _RightStart, float _RightEnd, float _Length)\r\n\t{\r\n\t\tBase = _Base;\r\n\t\tLeftStart = _LeftStart;\r\n\t\tLeftEnd = _LeftEnd;\r\n\t\tRightStart = _RightStart;\r\n\t\tRightEnd = _RightEnd;\r\n\t\tLength = MathF.Max(0.001f, _Length);\r\n\t}\r\n\r\n\tpublic RoadWidth(RoadSegment _Segment, float _Length)\r\n\t\t: this(_Segment.Width, _Segment.LeftExtraStart, _Segment.LeftExtraEnd, _Segment.RightExtraStart, _Segment.RightExtraEnd, _Length)\r\n\t{\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// A plain road of one width all along.\r\n\t/// </summary>\r\n\tpublic static RoadWidth Uniform(float _Width)\r\n\t{\r\n\t\treturn new RoadWidth(_Width, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// True when both edges sit at half the width all along: the road every piece of the tool was first built for.\r\n\t/// </summary>\r\n\tpublic bool IsUniform => LeftStart == 0.0f && LeftEnd == 0.0f && RightStart == 0.0f && RightEnd == 0.0f;\r\n\r\n\t/// <summary>\r\n\t/// The furthest either edge gets from the centreline anywhere along the segment.\r\n\t/// </summary>\r\n\tpublic float MaxHalf => MathF.Max(MathF.Max(Base * 0.5f + LeftStart, Base * 0.5f + LeftEnd), MathF.Max(Base * 0.5f + RightStart, Base * 0.5f + RightEnd));\r\n\r\n\t/// <summary>\r\n\t/// Distance of the left edge from the centreline at a curve distance, looking along the segment.\r\n\t/// </summary>\r\n\tpublic float LeftAt(float _Distance)\r\n\t{\r\n\t\tfloat left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);\r\n\t\tfloat right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);\r\n\r\n\t\treturn left + right < MinWidth ? left + (MinWidth - left - right) * 0.5f : left;\r\n\t}\r\n\r\n\tpublic float RightAt(float _Distance)\r\n\t{\r\n\t\tfloat left = Base * 0.5f + Blend(LeftStart, LeftEnd, _Distance);\r\n\t\tfloat right = Base * 0.5f + Blend(RightStart, RightEnd, _Distance);\r\n\r\n\t\treturn left + right < MinWidth ? right + (MinWidth - left - right) * 0.5f : right;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// The road's full width at a curve distance.\r\n\t/// </summary>\r\n\tpublic float WidthAt(float _Distance)\r\n\t{\r\n\t\treturn LeftAt(_Distance) + RightAt(_Distance);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// How much wider than the base half width the left edge is at a curve distance (negative when narrower).\r\n\t/// </summary>\r\n\tpublic float LeftExtraAt(float _Distance)\r\n\t{\r\n\t\treturn LeftAt(_Distance) - Base * 0.5f;\r\n\t}\r\n\r\n\tpublic float RightExtraAt(float _Distance)\r\n\t{\r\n\t\treturn RightAt(_Distance) - Base * 0.5f;\r\n\t}\r\n\r\n\r\n\r\n\tprivate float Blend(float _Start, float _End, float _Distance)\r\n\t{\r\n\t\tif (_Start == _End)\r\n\t\t\treturn _Start;\r\n\r\n\t\tfloat t = Math.Clamp(_Distance / Length, 0.0f, 1.0f);\r\n\r\n\t\t// Eased, so the edge leaves and arrives without a kink.\r\n\t\tt = t * t * (3.0f - 2.0f * t);\r\n\r\n\t\treturn _Start + (_End - _Start) * t;\r\n\t}\r\n\r\n\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(Base);\r\n\t\t_Hash.Add(LeftStart);\r\n\t\t_Hash.Add(LeftEnd);\r\n\t\t_Hash.Add(RightStart);\r\n\t\t_Hash.Add(RightEnd);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Utility/StableHash.cs",
"FileName": "StableHash.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// 64-bit FNV-1a hash that is stable across editor sessions, unlike <see cref=\"HashCode\"/> which is seeded per\r\n/// process. Used to stamp generated meshes with the inputs they were built from, so a scene reopened days later\r\n/// still knows which chunks are current.\r\n/// </summary>\r\npublic struct StableHash\r\n{\r\n\tprivate const ulong OffsetBasis = 14695981039346656037UL;\r\n\tprivate const ulong Prime = 1099511628211UL;\r\n\r\n\tprivate ulong m_Hash;\r\n\r\n\tpublic long Value => unchecked((long)m_Hash);\r\n\r\n\r\n\r\n\tpublic static StableHash Create()\r\n\t{\r\n\t\treturn new StableHash { m_Hash = OffsetBasis };\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(int _Value)\r\n\t{\r\n\t\tunchecked\r\n\t\t{\r\n\t\t\tfor (int i = 0; i < 4; i++)\r\n\t\t\t{\r\n\t\t\t\tm_Hash ^= (byte)(_Value >> (i * 8));\r\n\t\t\t\tm_Hash *= Prime;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(long _Value)\r\n\t{\r\n\t\tAdd(unchecked((int)_Value));\r\n\t\tAdd(unchecked((int)(_Value >> 32)));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(float _Value)\r\n\t{\r\n\t\tAdd(BitConverter.SingleToInt32Bits(_Value));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(Vector3 _Value)\r\n\t{\r\n\t\tAdd(_Value.x);\r\n\t\tAdd(_Value.y);\r\n\t\tAdd(_Value.z);\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Add(string _Value)\r\n\t{\r\n\t\tif (_Value is not null)\r\n\t\t{\r\n\t\t\tforeach (char c in _Value)\r\n\t\t\t\tAdd((int)c);\r\n\t\t}\r\n\r\n\t\t// Terminator, so \"ab\" + \"c\" never collides with \"a\" + \"bc\".\r\n\t\tAdd(-1);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Network/RoadChunk.cs",
"FileName": "RoadChunk.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\npublic enum RoadChunkKind\r\n{\r\n\t/// <summary>\r\n\t/// The road surface itself.\r\n\t/// </summary>\r\n\tSurface,\r\n\r\n\t/// <summary>\r\n\t/// What runs along the road's sides: sidewalks, verges.\r\n\t/// </summary>\r\n\tBorder,\r\n\r\n\t/// <summary>\r\n\t/// The painted lines on the surface: lane lines, centre line, edge lines.\r\n\t/// </summary>\r\n\tMarking,\r\n\r\n\t/// <summary>\r\n\t/// The bridge under a road: deck, sides, supports.\r\n\t/// </summary>\r\n\tBridge,\r\n\r\n\t/// <summary>\r\n\t/// The prefabs standing along a road and before its junctions.\r\n\t/// </summary>\r\n\tProps,\r\n\r\n\t/// <summary>\r\n\t/// The rounded end of a road that stops: surface and border together, in one piece per end.\r\n\t/// </summary>\r\n\tCap\r\n}\r\n\r\n/// <summary>\r\n/// Marks a generated mesh child of a <see cref=\"RoadNetwork\"/> and remembers what it covers: a stretch of a segment,\r\n/// or the junction at a node, and whether it is the surface or the border of it. The network uses it to replace\r\n/// exactly the pieces that went stale and leave the rest alone.\r\n/// </summary>\r\n[Icon(\"view_week\")]\r\npublic sealed class RoadChunk : Component\r\n{\r\n\t/// <summary>\r\n\t/// The segment this chunk is a stretch of, or -1 for a junction piece.\r\n\t/// </summary>\r\n\t[Property, ReadOnly] public int SegmentId { get; set; } = -1;\r\n\r\n\t/// <summary>\r\n\t/// The node this chunk is the junction of, or -1 for a road stretch.\r\n\t/// </summary>\r\n\t[Property, ReadOnly] public int NodeId { get; set; } = -1;\r\n\r\n\t/// <summary>\r\n\t/// The parking lot this chunk is a piece of, or -1 for anything else.\r\n\t/// </summary>\r\n\t[Property, ReadOnly] public int LotId { get; set; } = -1;\r\n\r\n\t[Property, ReadOnly] public RoadChunkKind Kind { get; set; } = RoadChunkKind.Surface;\r\n\r\n\t[Property, ReadOnly] public int ChunkIndex { get; set; }\r\n\r\n\t[Property, ReadOnly] public int ChunkCount { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// A locked chunk is never regenerated, so its mesh can be edited by hand. It keeps its geometry even if the road\r\n\t/// under it changes; unlock it to have it rebuilt.\r\n\t/// </summary>\r\n\t[Property]\r\n\tpublic bool Locked\r\n\t{\r\n\t\tget;\r\n\t\tset\r\n\t\t{\r\n\t\t\tfield = value;\r\n\r\n\t\t\t// Unlocking is a request to catch up with the graph.\r\n\t\t\tGameObject?.Parent?.Components.Get<RoadNetwork>()?.MarkDirty();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Hash of everything this chunk was generated from. When it no longer matches what the graph says it should be\r\n\t/// the chunk is stale.\r\n\t/// </summary>\r\n\t[Property, Hide] public long Signature { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// The rows a border piece was built from, when the network's debug overlay is on. Not saved.\r\n\t/// </summary>\r\n\tpublic BorderDebug Debug { get; set; }\r\n\r\n\tpublic bool IsJunction => NodeId >= 0;\r\n\r\n\tpublic bool IsLot => LotId >= 0;\r\n\r\n\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif (Debug is null)\r\n\t\t\treturn;\r\n\r\n\t\tvar network = GameObject?.Parent?.Components.Get<RoadNetwork>();\r\n\r\n\t\tif (network is null || !network.DebugBorders)\r\n\t\t\treturn;\r\n\r\n\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\tGizmo.Draw.LineThickness = 2.0f;\r\n\r\n\t\tforeach (var line in Debug.Lines)\r\n\t\t{\r\n\t\t\tGizmo.Draw.Color = line.Color;\r\n\t\t\tGizmo.Draw.Line(line.From, line.To);\r\n\t\t}\r\n\r\n\t\tforeach (var marker in Debug.Markers)\r\n\t\t{\r\n\t\t\tGizmo.Draw.Color = marker.Color;\r\n\t\t\tGizmo.Draw.SolidSphere(marker.Position, 2.0f);\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Network/RoadNetwork.Rebuild.cs",
"FileName": "RoadNetwork.Rebuild.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\npublic sealed partial class RoadNetwork\r\n{\r\n\t/// <summary>\r\n\t/// Bump whenever the generated geometry changes (UV layout, sampling, profile...), so pieces built by an older\r\n\t/// generator are replaced on the next reconcile instead of lingering until their road is edited.\r\n\t/// </summary>\r\n\tprivate const int GeneratorVersion = 57;\r\n\r\n\t/// <summary>\r\n\t/// A stretch of road shorter than this is not worth a chunk (it happens when a junction swallows a whole chunk).\r\n\t/// </summary>\r\n\tprivate const float MinChunkLength = 1.0f;\r\n\r\n\t/// <summary>\r\n\t/// Where a segment's painted lines may run: between <see cref=\"Start\"/> and <see cref=\"End\"/>, skipping the\r\n\t/// gap between <see cref=\"GapStart\"/> and <see cref=\"GapEnd\"/> when there is one (a crossing in the middle of\r\n\t/// a short road between two junctions).\r\n\t/// </summary>\r\n\tprivate readonly record struct LineRange(float Start, float End, float GapStart, float GapEnd)\r\n\t{\r\n\t\tpublic bool HasGap => GapEnd > GapStart;\r\n\r\n\t\tpublic void AddToHash(ref StableHash _Hash)\r\n\t\t{\r\n\t\t\t_Hash.Add(Start);\r\n\t\t\t_Hash.Add(End);\r\n\t\t\t_Hash.Add(GapStart);\r\n\t\t\t_Hash.Add(GapEnd);\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// A segment's pedestrian crossings: at either end, or one shared crossing halfway along a short road between\r\n\t/// two junctions (recorded on the start end). Centres are curve distances from the segment's start.\r\n\t/// </summary>\r\n\tprivate readonly record struct SegmentCrossings(bool AtStart, float StartCentre, bool AtEnd, float EndCentre);\r\n\r\n\t/// <summary>\r\n\t/// One piece of a segment to build. <see cref=\"Lines\"/> bounds the painted lines; <see cref=\"Taper\"/> is the\r\n\t/// border's run-out into a different road type at either end; <see cref=\"Bridge\"/> is the whole built range\r\n\t/// and the ends of a bridge piece; the other kinds ignore them.\r\n\t/// </summary>\r\n\tprivate readonly record struct ChunkPlan(int Index, int Count, float Start, float End, RoadChunkKind Kind, BorderCaps Caps, RoadBorderBuilder.SegmentBorderBlends Blends, long Signature, LineRange Lines = default, SegmentBorderTapers Taper = default, RoadBridgeBuilder.BridgeSpan Bridge = default, RoadPropBuilder.PropSpan Props = default, RoadMarkingBuilder.DashPhase Dash = default, SegmentCrossings Crossings = default);\r\n\r\n\tprivate readonly record struct PendingGroup(long Signature, int Remaining);\r\n\r\n\t/// <summary>\r\n\t/// One piece to (re)build: a chunk of a segment, or, when <see cref=\"NodeId\"/> is set, a piece of a junction.\r\n\t/// <see cref=\"ArmBlends\"/> and <see cref=\"ArmTapers\"/> carry, for a junction's border piece, what each arm's\r\n\t/// chunks were built with, keyed by segment.\r\n\t/// </summary>\r\n\tprivate readonly record struct RebuildJob(int SegmentId, int NodeId, RoadChunkKind Kind, long GroupSignature, ChunkPlan Plan, RoadCurveTable Table, RoadWidth Width, RoadProfile Profile, RoadJunctionLayout Junction, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> ArmBlends, Dictionary<int, SegmentBorderTapers> ArmTapers, RoadLot Lot);\r\n\r\n\tprivate bool m_IsDirty;\r\n\tprivate readonly Queue<RebuildJob> m_Jobs = new();\r\n\tprivate readonly Dictionary<int, PendingGroup> m_PendingSegments = new();\r\n\tprivate readonly Dictionary<(int NodeId, RoadChunkKind Kind), PendingGroup> m_PendingJunctions = new();\r\n\r\n\t/// <summary>\r\n\t/// Junction layouts keyed by node, each with a hash of what it was computed from. Finding corners on curved\r\n\t/// edges is the one expensive step of a reconcile, and while a node is dragged only its own junction and its\r\n\t/// neighbours' change.\r\n\t/// </summary>\r\n\tprivate readonly Dictionary<int, (long Key, RoadJunctionLayout Layout)> m_JunctionCache = new();\r\n\r\n\t/// <summary>\r\n\t/// True while pieces are still queued for (re)generation.\r\n\t/// </summary>\r\n\tpublic bool IsRebuilding => m_Jobs.Count > 0;\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Asks for a reconcile pass on the next editor frame: the graph is compared against the pieces that exist and\r\n\t/// only what differs gets queued for regeneration.\r\n\t/// </summary>\r\n\tpublic void MarkDirty()\r\n\t{\r\n\t\tm_IsDirty = true;\r\n\r\n\t\t// Undo, reloads and the inspector all land here with a graph nobody told us about piece by piece.\r\n\t\tInvalidateIndex();\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Asks the network in the editor's scene for a reconcile. Editing a road type, a bridge, a prop set or a\r\n\t/// parking layout changes roads that hold no copy of it, so the asset tells the network rather than the other\r\n\t/// way round.\r\n\t/// </summary>\r\n\tpublic static void Editor_MarkDirty()\r\n\t{\r\n\t\tvar scene = Application.Editor?.Scene;\r\n\r\n\t\tif (!scene.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tvar network = scene.Get<RoadNetwork>();\r\n\r\n\t\tif (!network.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tnetwork.MarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// One reconcile pass, then as much of the queued work as this frame's budget allows. Driven from the\r\n\t/// component's own update in RoadNetwork.cs.\r\n\t/// </summary>\r\n\tprivate void Tick()\r\n\t{\r\n\t\tif (m_IsDirty)\r\n\t\t{\r\n\t\t\tm_IsDirty = false;\r\n\t\t\tReconcile();\r\n\t\t}\r\n\r\n\t\tProcessJobs();\r\n\t}\r\n\r\n\r\n\r\n\tpublic IEnumerable<RoadChunk> GetChunks()\r\n\t{\r\n\t\tforeach (var child in GameObject.Children)\r\n\t\t{\r\n\t\t\tif (!child.IsValid() || child.IsDestroyed)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar chunk = child.Components.Get<RoadChunk>();\r\n\r\n\t\t\tif (chunk is not null)\r\n\t\t\t\tyield return chunk;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate RoadChunk FindChunk(int _SegmentId, int _ChunkIndex, RoadChunkKind _Kind)\r\n\t{\r\n\t\treturn GetChunks().FirstOrDefault(chunk => !chunk.IsJunction && chunk.SegmentId == _SegmentId && chunk.ChunkIndex == _ChunkIndex && chunk.Kind == _Kind);\r\n\t}\r\n\r\n\r\n\r\n\tprivate RoadChunk FindJunctionChunk(int _NodeId, RoadChunkKind _Kind)\r\n\t{\r\n\t\treturn GetChunks().FirstOrDefault(chunk => chunk.IsJunction && chunk.NodeId == _NodeId && chunk.Kind == _Kind);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Compares the graph with the pieces under this GameObject and queues work for everything that is missing,\r\n\t/// stale or built from different settings. Stateless on purpose: it copes with undo, reloads, hand deleted\r\n\t/// pieces and hot reloads alike, because the pieces themselves carry what they were built from.\r\n\t/// </summary>\r\n\tprivate void Reconcile()\r\n\t{\r\n\t\tPruneCurveTables();\r\n\r\n\t\t// A lot's entrances belong to its edge, wherever the lot has been moved to since.\r\n\t\tSnapAllOpenings();\r\n\r\n\t\tvar segmentChunks = new Dictionary<int, List<RoadChunk>>();\r\n\t\tvar junctionChunks = new Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk>();\r\n\t\tvar lotChunks = new Dictionary<(int LotId, RoadChunkKind Kind), RoadChunk>();\r\n\r\n\t\tforeach (var chunk in GetChunks())\r\n\t\t{\r\n\t\t\tif (chunk.IsLot)\r\n\t\t\t{\r\n\t\t\t\tif (lotChunks.TryGetValue((chunk.LotId, chunk.Kind), out var duplicate))\r\n\t\t\t\t\tduplicate.GameObject.Destroy();\r\n\r\n\t\t\t\tlotChunks[(chunk.LotId, chunk.Kind)] = chunk;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (chunk.IsJunction)\r\n\t\t\t{\r\n\t\t\t\t// Two pieces for one junction can only be leftovers; keep one, the reconcile sorts out whether it is current.\r\n\t\t\t\tif (junctionChunks.TryGetValue((chunk.NodeId, chunk.Kind), out var duplicate))\r\n\t\t\t\t\tduplicate.GameObject.Destroy();\r\n\r\n\t\t\t\tjunctionChunks[(chunk.NodeId, chunk.Kind)] = chunk;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (!segmentChunks.TryGetValue(chunk.SegmentId, out var list))\r\n\t\t\t{\r\n\t\t\t\tlist = new List<RoadChunk>();\r\n\t\t\t\tsegmentChunks[chunk.SegmentId] = list;\r\n\t\t\t}\r\n\r\n\t\t\tlist.Add(chunk);\r\n\t\t}\r\n\r\n\t\tvar layouts = ComputeJunctionLayouts();\r\n\t\tvar dashPhases = ComputeDashPhases();\r\n\t\tvar liveSegments = new HashSet<int>();\r\n\t\tvar segmentBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();\r\n\t\tvar segmentTapers = new Dictionary<int, SegmentBorderTapers>();\r\n\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tliveSegments.Add(segment.Id);\r\n\r\n\t\t\tvar profile = ProfileOf(segment);\r\n\t\t\t// A road opening into a parking lot starts where the lot's surface stops covering it.\r\n\t\t\tfloat trimStart = MathF.Max(layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.StartNodeId));\r\n\t\t\tfloat trimEnd = MathF.Max(layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id)?.Trim ?? 0.0f, LotTrimAt(segment.EndNodeId));\r\n\t\t\tvar caps = ComputeBorderCaps(segment, layouts);\r\n\t\t\tvar blends = ComputeBorderBlends(segment, table, layouts);\r\n\t\t\tvar taper = ComputeBorderTaper(segment, table, layouts, trimStart, trimEnd);\r\n\t\t\tvar crossings = ComputeCrossings(segment, table, layouts, trimStart, trimEnd);\r\n\t\t\tvar lines = ComputeLineRange(table, profile, trimStart, trimEnd, crossings);\r\n\t\t\tvar dash = dashPhases.GetValueOrDefault(segment.Id);\r\n\r\n\t\t\tvar plans = PlanChunks(segment, table, trimStart, trimEnd, profile, caps, blends, taper, ComputeSurfaceSignature(segment, table.Curve, profile), lines, dash, crossings);\r\n\t\t\tlong groupSignature = CombineSignatures(plans);\r\n\r\n\t\t\tsegmentBlends[segment.Id] = blends;\r\n\t\t\tsegmentTapers[segment.Id] = taper;\r\n\t\t\tsegmentChunks.TryGetValue(segment.Id, out var existing);\r\n\r\n\t\t\tif (IsSegmentUpToDate(existing, plans))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Already queued with these exact inputs: let those jobs finish rather than doubling them.\r\n\t\t\tif (m_PendingSegments.TryGetValue(segment.Id, out var pending) && pending.Signature == groupSignature)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tEnqueueSegment(segment, table, profile, plans, groupSignature, existing);\r\n\t\t}\r\n\r\n\t\tforeach (var (nodeId, layout) in layouts)\r\n\t\t{\r\n\t\t\tReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Surface, ComputeJunctionSurfaceSignature(layout), junctionChunks, null);\r\n\r\n\t\t\tbool anyBorder = layout.Arms.Any(arm => ProfileOf(arm.Segment).HasBorder);\r\n\r\n\t\t\tif (anyBorder)\r\n\t\t\t{\r\n\t\t\t\t// The junction's border runs continue the arms' chunks, so they take the same lifts and tapers those\r\n\t\t\t\t// were built with.\r\n\t\t\t\tvar armBlends = new Dictionary<int, RoadBorderBuilder.SegmentBorderBlends>();\r\n\t\t\t\tvar armTapers = new Dictionary<int, SegmentBorderTapers>();\r\n\r\n\t\t\t\tforeach (var arm in layout.Arms)\r\n\t\t\t\t{\r\n\t\t\t\t\tarmBlends[arm.Segment.Id] = segmentBlends.GetValueOrDefault(arm.Segment.Id);\r\n\t\t\t\t\tarmTapers[arm.Segment.Id] = segmentTapers.GetValueOrDefault(arm.Segment.Id);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tReconcileJunctionPiece(nodeId, layout, RoadChunkKind.Border, ComputeJunctionBorderSignature(layout, armBlends, armTapers), junctionChunks, armBlends, armTapers);\r\n\t\t\t}\r\n\t\t\telse if (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Border), out var border))\r\n\t\t\t{\r\n\t\t\t\tborder.GameObject.Destroy();\r\n\t\t\t}\r\n\r\n\t\t\t// Crossings used to be decals hung on the junction; the roads paint their own now, so anything left\r\n\t\t\t// here from then goes.\r\n\t\t\tif (junctionChunks.TryGetValue((nodeId, RoadChunkKind.Marking), out var marking))\r\n\t\t\t\tmarking.GameObject.Destroy();\r\n\t\t}\r\n\r\n\t\tReconcileLots(lotChunks);\r\n\r\n\t\t// Whatever no longer belongs to anything in the graph goes, locked or not.\r\n\t\tforeach (var (segmentId, chunks) in segmentChunks)\r\n\t\t{\r\n\t\t\tif (liveSegments.Contains(segmentId))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tforeach (var chunk in chunks)\r\n\t\t\t\tchunk.GameObject.Destroy();\r\n\r\n\t\t\tm_PendingSegments.Remove(segmentId);\r\n\t\t}\r\n\r\n\t\tforeach (var (key, chunk) in junctionChunks)\r\n\t\t{\r\n\t\t\tif (layouts.ContainsKey(key.NodeId))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tchunk.GameObject.Destroy();\r\n\t\t\tm_PendingJunctions.Remove(key);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void ReconcileJunctionPiece(int _NodeId, RoadJunctionLayout _Layout, RoadChunkKind _Kind, long _Signature, Dictionary<(int NodeId, RoadChunkKind Kind), RoadChunk> _Existing, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers = null)\r\n\t{\r\n\t\tvar key = (_NodeId, _Kind);\r\n\r\n\t\tif (_Existing.TryGetValue(key, out var existing) && (existing.Locked || (existing.Signature == _Signature && !LacksDebugRecord(existing))))\r\n\t\t\treturn;\r\n\r\n\t\tif (m_PendingJunctions.TryGetValue(key, out var pending) && pending.Signature == _Signature)\r\n\t\t\treturn;\r\n\r\n\t\tm_Jobs.Enqueue(new RebuildJob(-1, _NodeId, _Kind, _Signature, default, null, RoadWidth.Uniform(0.0f), null, _Layout, _ArmBlends, _ArmTapers, null));\r\n\t\tm_PendingJunctions[key] = new PendingGroup(_Signature, 1);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where every segment sits in its run: the stretch of road carried on through pass-through nodes until a\r\n\t/// junction, a road end or a loop. Each run is walked once, from the lowest-numbered segment in it backwards\r\n\t/// to its head and then forwards to its tail, in that segment's direction, so every segment of the run agrees\r\n\t/// on where the run's distance starts and the dashes carry across the nodes.\r\n\t/// </summary>\r\n\tprivate Dictionary<int, RoadMarkingBuilder.DashPhase> ComputeDashPhases()\r\n\t{\r\n\t\tvar phases = new Dictionary<int, RoadMarkingBuilder.DashPhase>();\r\n\r\n\t\tforeach (var seed in Segments.OrderBy(segment => segment.Id))\r\n\t\t{\r\n\t\t\tif (phases.ContainsKey(seed.Id))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar seen = new HashSet<int> { seed.Id };\r\n\t\t\tvar run = new List<(RoadSegment Segment, bool Forward)>();\r\n\r\n\t\t\t// Back to the head: a road carrying on through the node runs with the run when it ends at that node.\r\n\t\t\tvar segment = seed;\r\n\t\t\tint nodeId = seed.StartNodeId;\r\n\r\n\t\t\twhile (RunContinuation(segment, nodeId) is { } previous && seen.Add(previous.Id))\r\n\t\t\t{\r\n\t\t\t\tbool forward = previous.EndNodeId == nodeId;\r\n\r\n\t\t\t\trun.Add((previous, forward));\r\n\t\t\t\tsegment = previous;\r\n\t\t\t\tnodeId = forward ? previous.StartNodeId : previous.EndNodeId;\r\n\t\t\t}\r\n\r\n\t\t\trun.Reverse();\r\n\t\t\trun.Add((seed, true));\r\n\r\n\t\t\t// On to the tail.\r\n\t\t\tsegment = seed;\r\n\t\t\tnodeId = seed.EndNodeId;\r\n\r\n\t\t\twhile (RunContinuation(segment, nodeId) is { } next && seen.Add(next.Id))\r\n\t\t\t{\r\n\t\t\t\tbool forward = next.StartNodeId == nodeId;\r\n\r\n\t\t\t\trun.Add((next, forward));\r\n\t\t\t\tsegment = next;\r\n\t\t\t\tnodeId = forward ? next.EndNodeId : next.StartNodeId;\r\n\t\t\t}\r\n\r\n\t\t\tfloat offset = 0.0f;\r\n\r\n\t\t\tforeach (var (member, forward) in run)\r\n\t\t\t{\r\n\t\t\t\tphases[member.Id] = new RoadMarkingBuilder.DashPhase(offset, !forward);\r\n\t\t\t\toffset += GetCurveTable(member)?.Length ?? 0.0f;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn phases;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The road a segment carries straight on into at a pass-through node, or null at anything else.\r\n\t/// </summary>\r\n\tprivate RoadSegment RunContinuation(RoadSegment _Segment, int _NodeId)\r\n\t{\r\n\t\tif (GetDegree(_NodeId) != 2)\r\n\t\t\treturn null;\r\n\r\n\t\treturn GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How a segment's border runs out at each end where it meets another road type's: over the profile's transition\r\n\t/// length, towards the cross-section the two share. Without the junction layouts only pass-through nodes are\r\n\t/// known; the reconcile passes them, so junction corners taper too.\r\n\t/// </summary>\r\n\tpublic SegmentBorderTapers GetBorderTaper(RoadSegment _Segment)\r\n\t{\r\n\t\tvar table = GetCurveTable(_Segment);\r\n\r\n\t\treturn table is null ? default : ComputeBorderTaper(_Segment, table);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// One end of a border's taper: how long it is, the width and share of relief it runs to, whether a country road's\r\n\t/// gravel spills over it, and how far from the node it ends.\r\n\t/// </summary>\r\n\tprivate readonly record struct EndTaper(float Length, float Width, float Relief, bool Spill, float Offset);\r\n\r\n\r\n\r\n\tprivate SegmentBorderTapers ComputeBorderTaper(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts = null, float _TrimStart = 0.0f, float _TrimEnd = 0.0f)\r\n\t{\r\n\t\tvar profile = ProfileOf(_Segment);\r\n\r\n\t\tif (!profile.HasBorder)\r\n\t\t\treturn default;\r\n\r\n\t\tfloat limit = _Table.Length * 0.5f;\r\n\t\tRoadBorderTaper left = default;\r\n\t\tRoadBorderTaper right = default;\r\n\r\n\t\t// A transition ends at its trim, so the two ends share the stretch BETWEEN the trims and each gets half of\r\n\t\t// it; however far a junction reaches along the road (a ramp meeting a highway can trim most of it), the two\r\n\t\t// never run into each other and neither is squeezed out.\r\n\t\tfloat transitionLimit = MathF.Max(0.0f, _Table.Length - _TrimStart - _TrimEnd) * 0.5f;\r\n\r\n\t\tif (profile.TransitionLength > 0.0f)\r\n\t\t{\r\n\t\t\tSideTapers(_Segment, profile, _Segment.StartNodeId, true, transitionLimit, _Layouts, ref left, ref right);\r\n\t\t\tSideTapers(_Segment, profile, _Segment.EndNodeId, false, transitionLimit, _Layouts, ref left, ref right);\r\n\t\t}\r\n\r\n\t\t// At a nose, the border on the side facing the road this one parts from grows out of nothing instead.\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.StartNodeId, true, limit) is { } startLeft)\r\n\t\t\tleft = left with { StartLength = startLeft, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };\r\n\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.StartNodeId, false, limit) is { } startRight)\r\n\t\t\tright = right with { StartLength = startRight, StartWidth = 0.0f, StartRelief = 0.0f, StartSpill = false, StartGore = true, StartOffset = 0.0f };\r\n\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.EndNodeId, true, limit) is { } endLeft)\r\n\t\t\tleft = left with { EndLength = endLeft, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };\r\n\r\n\t\tif (GoreLength(_Segment, _Table, _Segment.EndNodeId, false, limit) is { } endRight)\r\n\t\t\tright = right with { EndLength = endRight, EndWidth = 0.0f, EndRelief = 0.0f, EndSpill = false, EndGore = true, EndOffset = 0.0f };\r\n\r\n\t\treturn new SegmentBorderTapers(left, right);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The tapers at one end of a segment, one per side, each towards the border across from it there. At a plain\r\n\t/// pass-through node that is the other road on both sides. At a junction it is the road on the far side of each\r\n\t/// corner, and the taper ends at the arm's trim, where the corner starts: the corner itself is then built in the\r\n\t/// one cross-section both its roads have run to, so it is a clean curve rather than one border turning into\r\n\t/// the other round the arc.\r\n\t/// </summary>\r\n\tprivate void SideTapers(RoadSegment _Segment, RoadProfile _Profile, int _NodeId, bool _AtStart, float _Limit, Dictionary<int, RoadJunctionLayout> _Layouts, ref RoadBorderTaper _Left, ref RoadBorderTaper _Right)\r\n\t{\r\n\t\tint degree = GetDegree(_NodeId);\r\n\r\n\t\tif (degree < 2)\r\n\t\t\treturn;\r\n\r\n\t\tRoadSegment leftNeighbour = null;\r\n\t\tRoadSegment rightNeighbour = null;\r\n\t\tfloat trim = 0.0f;\r\n\r\n\t\tif (_Layouts is not null && _Layouts.TryGetValue(_NodeId, out var layout) && layout.FindArm(_Segment.Id) is { } arm)\r\n\t\t{\r\n\t\t\ttrim = arm.Trim;\r\n\r\n\t\t\t// A corner starting on this arm runs on its left looking out of the node, one ending on it on its right;\r\n\t\t\t// looking along a segment that ends at the node swaps the two.\r\n\t\t\tforeach (var path in layout.CornerPaths)\r\n\t\t\t{\r\n\t\t\t\tif (path.ArmA.Segment.Id == _Segment.Id)\r\n\t\t\t\t{\r\n\t\t\t\t\tif (_AtStart)\r\n\t\t\t\t\t\tleftNeighbour = path.ArmB.Segment;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\trightNeighbour = path.ArmB.Segment;\r\n\t\t\t\t}\r\n\t\t\t\telse if (path.ArmB.Segment.Id == _Segment.Id)\r\n\t\t\t\t{\r\n\t\t\t\t\tif (_AtStart)\r\n\t\t\t\t\t\trightNeighbour = path.ArmA.Segment;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tleftNeighbour = path.ArmA.Segment;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if (degree == 2)\r\n\t\t{\r\n\t\t\tleftNeighbour = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\t\t\trightNeighbour = leftNeighbour;\r\n\t\t}\r\n\r\n\t\tif (SideTaper(_Profile, leftNeighbour, degree, trim, _Limit, out var leftEnd))\r\n\t\t\t_Left = WithEnd(_Left, _AtStart, leftEnd);\r\n\r\n\t\tif (SideTaper(_Profile, rightNeighbour, degree, trim, _Limit, out var rightEnd))\r\n\t\t\t_Right = WithEnd(_Right, _AtStart, rightEnd);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The taper one side of a border takes towards the border across from it at a node, or false when it takes none:\r\n\t/// nothing across, a border that is already the same size, or \u2014 at a junction \u2014 a border of another kind, which\r\n\t/// a junction still closes off against this one.\r\n\t/// </summary>\r\n\tprivate bool SideTaper(RoadProfile _Profile, RoadSegment _Neighbour, int _Degree, float _Trim, float _Limit, out EndTaper _Taper)\r\n\t{\r\n\t\t_Taper = default;\r\n\r\n\t\tif (_Neighbour is null)\r\n\t\t\treturn false;\r\n\r\n\t\tvar other = ProfileOf(_Neighbour);\r\n\t\tbool sameKind = other.Border == _Profile.Border;\r\n\r\n\t\tif (_Degree > 2 && !sameKind)\r\n\t\t\treturn false;\r\n\r\n\t\tif (sameKind && MathF.Abs(other.FlatBorderWidth - _Profile.FlatBorderWidth) < 0.01f && MathF.Abs(other.ReliefHeight - _Profile.ReliefHeight) < 0.01f)\r\n\t\t\treturn false;\r\n\r\n\t\t// Held to its half of the stretch between the trims, so it never runs into the taper at the far end.\r\n\t\tfloat length = MathF.Min(_Profile.TransitionLength, _Limit);\r\n\r\n\t\tif (length <= 0.01f)\r\n\t\t\treturn false;\r\n\r\n\t\t_Taper = new EndTaper(length, RoadProfile.MeetingWidth(_Profile, other), RoadProfile.MeetingRelief(_Profile, other), !sameKind && other.Border == RoadBorderStyle.CountrySide, _Trim);\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static RoadBorderTaper WithEnd(in RoadBorderTaper _Taper, bool _AtStart, in EndTaper _End)\r\n\t{\r\n\t\treturn _AtStart\r\n\t\t\t? _Taper with { StartLength = _End.Length, StartWidth = _End.Width, StartRelief = _End.Relief, StartSpill = _End.Spill, StartOffset = _End.Offset }\r\n\t\t\t: _Taper with { EndLength = _End.Length, EndWidth = _End.Width, EndRelief = _End.Relief, EndSpill = _End.Spill, EndOffset = _End.Offset };\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How far from a node the gap between this segment's edge on one side (its own left or right) and the edge of\r\n\t/// another road parting from the same point has opened enough for both roads' borders to fit side by side; null\r\n\t/// when no road parts from that edge there. Sampled along both roads at equal distances from the node, which\r\n\t/// is how two roads leaving a common tangent separate.\r\n\t/// </summary>\r\n\tprivate float? GoreLength(RoadSegment _Segment, RoadCurveTable _Table, int _NodeId, bool _Left, float _Limit)\r\n\t{\r\n\t\tconst float step = 25.0f;\r\n\r\n\t\tif (GetDegree(_NodeId) < 3)\r\n\t\t\treturn null;\r\n\r\n\t\tbool atStart = _Segment.StartNodeId == _NodeId;\r\n\t\tfloat own = ProfileOf(_Segment).FlatBorderWidth;\r\n\t\tvar width = new RoadWidth(_Segment, _Table.Length);\r\n\r\n\t\t// This segment's edge point at a distance from the node, on the side asked for.\r\n\t\tVector3 OwnEdge(float _FromNode)\r\n\t\t{\r\n\t\t\tfloat distance = atStart ? _FromNode : _Table.Length - _FromNode;\r\n\r\n\t\t\tRoadSurfaceBuilder.CrossSection(_Table, distance, width, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);\r\n\r\n\t\t\treturn _Left ? leftEdge : rightEdge;\r\n\t\t}\r\n\r\n\t\tVector3 start = OwnEdge(0.0f);\r\n\r\n\t\tforeach (var other in GetSegmentsAt(_NodeId))\r\n\t\t{\r\n\t\t\tif (other.Id == _Segment.Id)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar otherTable = GetCurveTable(other);\r\n\r\n\t\t\tif (otherTable is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tbool otherAtStart = other.StartNodeId == _NodeId;\r\n\t\t\tvar otherWidth = new RoadWidth(other, otherTable.Length);\r\n\r\n\t\t\tVector3 OtherEdge(float _FromNode, bool _OtherLeft)\r\n\t\t\t{\r\n\t\t\t\tfloat distance = otherAtStart ? _FromNode : otherTable.Length - _FromNode;\r\n\r\n\t\t\t\tRoadSurfaceBuilder.CrossSection(otherTable, distance, otherWidth, 0.0f, out Vector3 leftEdge, out Vector3 rightEdge);\r\n\r\n\t\t\t\treturn _OtherLeft ? leftEdge : rightEdge;\r\n\t\t\t}\r\n\r\n\t\t\t// The other road's edge that starts on ours, if either does.\r\n\t\t\tbool otherLeft;\r\n\r\n\t\t\tif (OtherEdge(0.0f, true).Distance(start) < 1.0f)\r\n\t\t\t\totherLeft = true;\r\n\t\t\telse if (OtherEdge(0.0f, false).Distance(start) < 1.0f)\r\n\t\t\t\totherLeft = false;\r\n\t\t\telse\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Parting means leaving the node the same way; a road turning off at an angle is a corner, not a gore.\r\n\t\t\tVector3 ownWay = OwnEdge(step) - start;\r\n\t\t\tVector3 otherWay = OtherEdge(step, otherLeft) - start;\r\n\r\n\t\t\tif (ownWay.IsNearZeroLength || otherWay.IsNearZeroLength || Vector3.Dot(ownWay.Normal, otherWay.Normal) < 0.7f)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat needed = own + ProfileOf(other).FlatBorderWidth;\r\n\t\t\tfloat reach = MathF.Min(_Limit, MathF.Min(_Table.Length, otherTable.Length));\r\n\r\n\t\t\tfor (float fromNode = step; fromNode <= reach; fromNode += step)\r\n\t\t\t{\r\n\t\t\t\tif (OwnEdge(fromNode).Distance(OtherEdge(fromNode, otherLeft)) >= needed)\r\n\t\t\t\t\treturn MathF.Max(step, fromNode);\r\n\t\t\t}\r\n\r\n\t\t\treturn MathF.Max(step, reach);\r\n\t\t}\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Whether two road types' borders come to one cross-section where they meet, so nothing needs closing off\r\n\t/// between them: of a kind, and both tapering into the width and height they share.\r\n\t/// </summary>\r\n\tprivate static bool MeetsFlush(RoadProfile _A, RoadProfile _B)\r\n\t{\r\n\t\treturn _A.HasBorder && _A.Border == _B.Border && _A.TransitionLength > 0.0f && _B.TransitionLength > 0.0f;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The filled area of every junction, in network space (lifted to the road surface): its centre and its\r\n\t/// outline, which the junction surface fans between, plus how far a pavement reaches out from the outline\r\n\t/// round it and how far above the outline that pavement's top sits (nothing when no arm has a sidewalk). For\r\n\t/// tools that need to know where road is.\r\n\t/// </summary>\r\n\tpublic IEnumerable<(Vector3 Centre, IReadOnlyList<Vector3> Polygon, float PavementReach, float PavementTop)> GetJunctionPolygons()\r\n\t{\r\n\t\t// A parking lot is a junction as far as the ground under it is concerned.\r\n\t\tforeach (var lot in Lots)\r\n\t\t{\r\n\t\t\tvar profile = ProfileOf(lot);\r\n\t\t\tbool pavement = profile.Border == RoadBorderStyle.Sidewalk;\r\n\r\n\t\t\tyield return (lot.Centre, RoadLotOutline.Build(lot, OpeningsOf(lot)).Points, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);\r\n\t\t}\r\n\r\n\t\t// A rounded dead end reaches past its node, where no road piece covers the ground.\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null || segment.Bridge is not null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar profile = ProfileOf(segment);\r\n\t\t\tbool pavement = profile.Border == RoadBorderStyle.Sidewalk;\r\n\t\t\tvar width = new RoadWidth(segment, table.Length);\r\n\r\n\t\t\tforeach (bool atStart in new[] { true, false })\r\n\t\t\t{\r\n\t\t\t\tint nodeId = atStart ? segment.StartNodeId : segment.EndNodeId;\r\n\r\n\t\t\t\tif (GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar outline = RoadCapBuilder.Outline(table, atStart ? 0.0f : table.Length, atStart, width, profile, SurfaceOffset, out Vector3 centre);\r\n\r\n\t\t\t\tif (outline.Count >= 3)\r\n\t\t\t\t\tyield return (centre, outline, pavement ? profile.BorderWidth : 0.0f, pavement ? profile.CurbHeight : 0.0f);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach (var layout in ComputeJunctionLayouts().Values)\r\n\t\t{\r\n\t\t\tif (layout.Polygon.Count < 3)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat reach = 0.0f;\r\n\t\t\tfloat top = 0.0f;\r\n\r\n\t\t\tforeach (var arm in layout.Arms)\r\n\t\t\t{\r\n\t\t\t\tvar profile = ProfileOf(arm.Segment);\r\n\r\n\t\t\t\tif (profile.Border != RoadBorderStyle.Sidewalk)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\treach = MathF.Max(reach, profile.BorderWidth);\r\n\t\t\t\ttop = MathF.Max(top, profile.CurbHeight);\r\n\t\t\t}\r\n\r\n\t\t\tyield return (layout.Centre, layout.Polygon, reach, top);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Lays out every junction. A road too short for the fillets at both its ends gets those fillets shrunk until\r\n\t/// their tangent points fit on it (a trim past the tangent point would send the corner geometry back over the\r\n\t/// road); only corners that cannot fit even sharp are clamped. Then the junction polygons are built.\r\n\t/// </summary>\r\n\tprivate Dictionary<int, RoadJunctionLayout> ComputeJunctionLayouts()\r\n\t{\r\n\t\tvar radii = Nodes.ToDictionary(node => node.Id, node => MathF.Max(0.0f, node.CornerRadius));\r\n\t\tvar layouts = BuildJunctionLayouts(radii);\r\n\t\tvar shrink = new Dictionary<int, float>();\r\n\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);\r\n\t\t\tvar endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);\r\n\t\t\tfloat sharp = (startArm?.SharpTrim ?? 0.0f) + (endArm?.SharpTrim ?? 0.0f);\r\n\t\t\tfloat fillet = (startArm is null ? 0.0f : startArm.RawTrim - startArm.SharpTrim) + (endArm is null ? 0.0f : endArm.RawTrim - endArm.SharpTrim);\r\n\t\t\tfloat available = MathF.Max(0.0f, table.Length - MinChunkLength);\r\n\r\n\t\t\tif (fillet <= 0.0f || sharp + fillet <= available)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// The fillet's share of a trim is proportional to the radius, so this factor makes it fit exactly.\r\n\t\t\tfloat factor = Math.Clamp((available - sharp) / fillet, 0.0f, 1.0f);\r\n\r\n\t\t\tforeach (int nodeId in new[] { segment.StartNodeId, segment.EndNodeId })\r\n\t\t\t{\r\n\t\t\t\tif (layouts.ContainsKey(nodeId))\r\n\t\t\t\t\tshrink[nodeId] = MathF.Min(shrink.GetValueOrDefault(nodeId, 1.0f), factor);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (shrink.Count > 0)\r\n\t\t{\r\n\t\t\tforeach (var (nodeId, factor) in shrink)\r\n\t\t\t\tradii[nodeId] *= factor;\r\n\r\n\t\t\tlayouts = BuildJunctionLayouts(radii);\r\n\t\t}\r\n\r\n\t\t// Last resort, for corners that do not fit even when sharp: share what road there is.\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\tvar table = GetCurveTable(segment);\r\n\r\n\t\t\tif (table is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar startArm = layouts.GetValueOrDefault(segment.StartNodeId)?.FindArm(segment.Id);\r\n\t\t\tvar endArm = layouts.GetValueOrDefault(segment.EndNodeId)?.FindArm(segment.Id);\r\n\t\t\tfloat total = (startArm?.Trim ?? 0.0f) + (endArm?.Trim ?? 0.0f);\r\n\t\t\tfloat available = MathF.Max(0.0f, table.Length - MinChunkLength);\r\n\r\n\t\t\tif (total <= available || total <= 0.0f)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat scale = available / total;\r\n\r\n\t\t\tif (startArm is not null)\r\n\t\t\t\tstartArm.Trim *= scale;\r\n\r\n\t\t\tif (endArm is not null)\r\n\t\t\t\tendArm.Trim *= scale;\r\n\t\t}\r\n\r\n\t\tforeach (var layout in layouts.Values)\r\n\t\t\tlayout.BuildPolygon();\r\n\r\n\t\treturn layouts;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The corner fold on one side of a segment at one of its nodes, and whether the segment is that fold's first\r\n\t/// arm (its left edge) or second (its right edge). Looking out of the node, a segment starting there has its\r\n\t/// left on the arm's left; one ending there has it on the arm's right.\r\n\t/// </summary>\r\n\tprivate static RoadJunctionLayout.CornerFold FindFold(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _SideA)\r\n\t{\r\n\t\t_SideA = _AtStart == _Left;\r\n\r\n\t\tif (!_Layouts.TryGetValue(_NodeId, out var layout))\r\n\t\t\treturn null;\r\n\r\n\t\tvar path = _SideA\r\n\t\t\t? layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmA.Segment.Id == _Segment.Id)\r\n\t\t\t: layout.CornerPaths.FirstOrDefault(candidate => candidate.ArmB.Segment.Id == _Segment.Id);\r\n\r\n\t\treturn path?.Fold;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The junction layout of every node for the given fillet radii, from the cache where the inputs match.\r\n\t/// </summary>\r\n\tprivate Dictionary<int, RoadJunctionLayout> BuildJunctionLayouts(Dictionary<int, float> _Radii)\r\n\t{\r\n\t\tvar layouts = new Dictionary<int, RoadJunctionLayout>();\r\n\t\tvar liveNodes = new HashSet<int>();\r\n\r\n\t\tforeach (var node in Nodes)\r\n\t\t{\r\n\t\t\tliveNodes.Add(node.Id);\r\n\r\n\t\t\tfloat radius = _Radii[node.Id];\r\n\t\t\tlong key = ComputeJunctionKey(node, radius);\r\n\t\t\tRoadJunctionLayout layout;\r\n\r\n\t\t\t// A cached layout is only as good as its references: undo and reloads swap the graph's objects.\r\n\t\t\tif (m_JunctionCache.TryGetValue(node.Id, out var cached) && cached.Key == key && (cached.Layout is null || cached.Layout.Refresh(this)))\r\n\t\t\t{\r\n\t\t\t\tlayout = cached.Layout;\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tlayout = RoadJunctionLayout.Create(this, node, SurfaceOffset, radius);\r\n\t\t\t\tm_JunctionCache[node.Id] = (key, layout);\r\n\t\t\t}\r\n\r\n\t\t\tif (layout is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tlayout.ResetTrims();\r\n\t\t\tlayouts[node.Id] = layout;\r\n\t\t}\r\n\r\n\t\tforeach (int nodeId in m_JunctionCache.Keys.Where(id => !liveNodes.Contains(id)).ToList())\r\n\t\t\tm_JunctionCache.Remove(nodeId);\r\n\r\n\t\treturn layouts;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Whether an end of a road asks for a pedestrian crossing: the road type has them, the road has not turned\r\n\t/// this end's off, and the end is a real junction (three roads or more; a bend has a layout too, but nobody\r\n\t/// crosses at a bend) that trims the road.\r\n\t/// </summary>\r\n\tprivate bool WantsCrossing(RoadSegment _Segment, int _NodeId, Dictionary<int, RoadJunctionLayout> _Layouts, float _Trim)\r\n\t{\r\n\t\tif (!ProfileOf(_Segment).Crossings || !_Segment.CrossingAt(_NodeId))\r\n\t\t\treturn false;\r\n\r\n\t\treturn _Layouts.TryGetValue(_NodeId, out var layout) && layout.Arms.Count >= 3 && _Trim > 0.01f;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Places a road's crossings on the stretch left between its junctions. Two crossings need room for both and\r\n\t/// a gap between; a shorter road between two junctions gets one crossing shared by both, halfway along; a road\r\n\t/// shorter than a crossing gets none.\r\n\t/// </summary>\r\n\tprivate SegmentCrossings ComputeCrossings(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts, float _TrimStart, float _TrimEnd)\r\n\t{\r\n\t\tvar profile = ProfileOf(_Segment);\r\n\t\tfloat depth = profile.CrossingDepth;\r\n\t\tfloat clear = _Table.Length - _TrimStart - _TrimEnd;\r\n\t\tbool wantStart = WantsCrossing(_Segment, _Segment.StartNodeId, _Layouts, _TrimStart);\r\n\t\tbool wantEnd = WantsCrossing(_Segment, _Segment.EndNodeId, _Layouts, _TrimEnd);\r\n\r\n\t\tif (clear < depth || (!wantStart && !wantEnd))\r\n\t\t\treturn default;\r\n\r\n\t\t// A ladder of stripes is laid on the road outside the junction; two lines mark the walk across the\r\n\t\t// junction's own mouth, so that kind is pulled in far enough to put its outer line on the pavement's\r\n\t\t// corner instead of a crossing's depth short of it.\r\n\t\tbool inside = profile.CrossingStyle == RoadCrossingStyle.Lines;\r\n\t\tfloat startCentre = inside\r\n\t\t\t? MathF.Max(depth * 0.5f, _TrimStart - depth * 0.5f)\r\n\t\t\t: _TrimStart + depth * 0.5f;\r\n\r\n\t\tfloat endCentre = inside\r\n\t\t\t? MathF.Min(_Table.Length - depth * 0.5f, _Table.Length - _TrimEnd + depth * 0.5f)\r\n\t\t\t: _Table.Length - _TrimEnd - depth * 0.5f;\r\n\r\n\t\tif (wantStart && wantEnd)\r\n\t\t{\r\n\t\t\tif (clear >= 2.0f * depth + profile.CrossingClearance)\r\n\t\t\t\treturn new SegmentCrossings(true, startCentre, true, endCentre);\r\n\r\n\t\t\treturn new SegmentCrossings(true, _TrimStart + clear * 0.5f, false, 0.0f);\r\n\t\t}\r\n\r\n\t\treturn wantStart\r\n\t\t\t? new SegmentCrossings(true, startCentre, false, 0.0f)\r\n\t\t\t: new SegmentCrossings(false, 0.0f, true, endCentre);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where the painted lines run: the stretch between the junctions, kept clear of the crossings.\r\n\t/// </summary>\r\n\tprivate static LineRange ComputeLineRange(RoadCurveTable _Table, RoadProfile _Profile, float _TrimStart, float _TrimEnd, in SegmentCrossings _Crossings)\r\n\t{\r\n\t\tfloat reach = _Profile.CrossingDepth * 0.5f + _Profile.CrossingClearance;\r\n\t\tfloat start = _TrimStart;\r\n\t\tfloat end = _Table.Length - _TrimEnd;\r\n\t\tfloat gapStart = 0.0f;\r\n\t\tfloat gapEnd = 0.0f;\r\n\r\n\t\tif (_Crossings.AtEnd)\r\n\t\t\tend = MathF.Min(end, _Crossings.EndCentre - reach);\r\n\r\n\t\tif (_Crossings.AtStart)\r\n\t\t{\r\n\t\t\tfloat before = _Crossings.StartCentre - reach;\r\n\t\t\tfloat after = _Crossings.StartCentre + reach;\r\n\r\n\t\t\t// A crossing at the very start just pushes the lines back; one further along cuts a gap in them.\r\n\t\t\tif (before <= start + 1.0f)\r\n\t\t\t{\r\n\t\t\t\tstart = MathF.Max(start, after);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tgapStart = before;\r\n\t\t\t\tgapEnd = after;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn new LineRange(start, end, gapStart, gapEnd);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Which ends of a segment's borders nothing continues: a road end, a side of a junction with no corner on it,\r\n\t/// or a pass-through node where the road changes type or width.\r\n\t/// </summary>\r\n\tprivate BorderCaps ComputeBorderCaps(RoadSegment _Segment, Dictionary<int, RoadJunctionLayout> _Layouts)\r\n\t{\r\n\t\tvar caps = BorderCaps.None;\r\n\r\n\t\tif (IsBorderOpen(_Segment, _Segment.StartNodeId, true, _Layouts, out bool startLeft, out bool startRight))\r\n\t\t{\r\n\t\t\tcaps |= startLeft ? BorderCaps.StartLeft : BorderCaps.None;\r\n\t\t\tcaps |= startRight ? BorderCaps.StartRight : BorderCaps.None;\r\n\t\t}\r\n\r\n\t\tif (IsBorderOpen(_Segment, _Segment.EndNodeId, false, _Layouts, out bool endLeft, out bool endRight))\r\n\t\t{\r\n\t\t\tcaps |= endLeft ? BorderCaps.EndLeft : BorderCaps.None;\r\n\t\t\tcaps |= endRight ? BorderCaps.EndRight : BorderCaps.None;\r\n\t\t}\r\n\r\n\t\treturn caps;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// A bridge segment's built range and ends. An end is closed off unless it is a pass-through node onto\r\n\t/// another bridge of the same style, width and border, where the two decks meet flush; there, only one of the\r\n\t/// two segments provides the node's railing post: the one leaving the node, or the lower id when both leave\r\n\t/// or both arrive.\r\n\t/// </summary>\r\n\tprivate RoadBridgeBuilder.BridgeSpan ComputeBridgeSpan(RoadSegment _Segment, float _RangeStart, float _RangeEnd)\r\n\t{\r\n\t\tvar startNeighbour = DeckContinuation(_Segment, _Segment.StartNodeId);\r\n\t\tvar endNeighbour = DeckContinuation(_Segment, _Segment.EndNodeId);\r\n\r\n\t\tbool postStart = startNeighbour is null || startNeighbour.EndNodeId == _Segment.StartNodeId || _Segment.Id < startNeighbour.Id;\r\n\t\tbool postEnd = endNeighbour is null || (endNeighbour.EndNodeId == _Segment.EndNodeId && _Segment.Id < endNeighbour.Id);\r\n\r\n\t\treturn new RoadBridgeBuilder.BridgeSpan(_RangeStart, _RangeEnd, startNeighbour is null, postStart, endNeighbour is null, postEnd);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The bridge segment the deck carries straight on to at a node, or null where the deck ends.\r\n\t/// </summary>\r\n\tprivate RoadSegment DeckContinuation(RoadSegment _Segment, int _NodeId)\r\n\t{\r\n\t\tif (GetDegree(_NodeId) != 2)\r\n\t\t\treturn null;\r\n\r\n\t\tvar other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\r\n\t\tif (other?.Bridge is null || other.Bridge.ResourcePath != _Segment.Bridge.ResourcePath || !SameEdgesAt(_Segment, other, _NodeId))\r\n\t\t\treturn null;\r\n\r\n\t\tbool sameExtent = MathF.Abs(RoadBridgeBuilder.DeckExtent(ProfileOf(other), other.Bridge) - RoadBridgeBuilder.DeckExtent(ProfileOf(_Segment), _Segment.Bridge)) < 0.01f;\r\n\r\n\t\treturn sameExtent ? other : null;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The lift lines of a segment's two borders: at each end, the corner's height difference on that side (or\r\n\t/// nothing), run linearly along the road between the two.\r\n\t/// </summary>\r\n\tprivate RoadBorderBuilder.SegmentBorderBlends ComputeBorderBlends(RoadSegment _Segment, RoadCurveTable _Table, Dictionary<int, RoadJunctionLayout> _Layouts)\r\n\t{\r\n\t\treturn new RoadBorderBuilder.SegmentBorderBlends(SideBlendFor(_Segment, _Table, true, _Layouts), SideBlendFor(_Segment, _Table, false, _Layouts));\r\n\t}\r\n\r\n\r\n\r\n\tprivate RoadBorderBuilder.SideBlend SideBlendFor(RoadSegment _Segment, RoadCurveTable _Table, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts)\r\n\t{\r\n\t\tbool hasStart = CornerLift(_Segment, _Segment.StartNodeId, true, _Left, _Layouts, out float startDistance, out float startDelta);\r\n\t\tbool hasEnd = CornerLift(_Segment, _Segment.EndNodeId, false, _Left, _Layouts, out float endDistance, out float endDelta);\r\n\r\n\t\tif (!hasStart && !hasEnd)\r\n\t\t\treturn default;\r\n\r\n\t\t// An end with no corner on this side holds the border at road height from where the border actually starts,\r\n\t\t// which is past the trim: a road opening into a lot stops at the lot's flares, and a blend still running\r\n\t\t// there hands the lot a pavement tilted by whatever it had left to shed while the lot's own is flat.\r\n\t\tif (!hasStart)\r\n\t\t{\r\n\t\t\tstartDistance = LotTrimAt(_Segment.StartNodeId);\r\n\t\t\tstartDelta = 0.0f;\r\n\t\t}\r\n\r\n\t\tif (!hasEnd)\r\n\t\t{\r\n\t\t\tendDistance = _Table.Length - LotTrimAt(_Segment.EndNodeId);\r\n\t\t\tendDelta = 0.0f;\r\n\t\t}\r\n\r\n\t\treturn new RoadBorderBuilder.SideBlend(true, startDistance, startDelta, endDistance, endDelta);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Where along the segment the corner's tangent point on this side is, and how much higher the corner sits\r\n\t/// than the road edge there; false when there is no inside corner on that side at that end.\r\n\t/// </summary>\r\n\tprivate static bool CornerLift(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary<int, RoadJunctionLayout> _Layouts, out float _Distance, out float _Delta)\r\n\t{\r\n\t\t_Distance = 0.0f;\r\n\t\t_Delta = 0.0f;\r\n\r\n\t\tvar fold = FindFold(_Segment, _NodeId, _AtStart, _Left, _Layouts, out bool sideA);\r\n\r\n\t\tif (fold is null || !_Layouts.TryGetValue(_NodeId, out var layout))\r\n\t\t\treturn false;\r\n\r\n\t\tvar arm = layout.FindArm(_Segment.Id);\r\n\r\n\t\tif (arm is null)\r\n\t\t\treturn false;\r\n\r\n\t\t_Distance = arm.CurveDistance(sideA ? fold.TangentParamA : fold.TangentParamB);\r\n\t\t_Delta = fold.ApexHeight - (sideA ? fold.TangentHeightA : fold.TangentHeightB);\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsBorderOpen(RoadSegment _Segment, int _NodeId, bool _AtStart, Dictionary<int, RoadJunctionLayout> _Layouts, out bool _Left, out bool _Right)\r\n\t{\r\n\t\tint degree = GetDegree(_NodeId);\r\n\r\n\t\t// A road opening into a lot runs into the lot's own kerb, which closes it off; two caps in one place\r\n\t\t// would only fight each other.\r\n\t\tif (LotAtOpening(_NodeId) is { } lot && ProfileOf(lot).Border == ProfileOf(_Segment).Border)\r\n\t\t{\r\n\t\t\t_Left = false;\r\n\t\t\t_Right = false;\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif (degree <= 1)\r\n\t\t{\r\n\t\t\t// A road rounded off where it stops carries its border round the end; a cap across the road as well\r\n\t\t\t// would sit inside that.\r\n\t\t\tbool rounded = _Segment.Bridge is null && ProfileOf(_Segment).RoundedEnds;\r\n\r\n\t\t\t_Left = !rounded;\r\n\t\t\t_Right = !rounded;\r\n\t\t\treturn !rounded;\r\n\t\t}\r\n\r\n\t\tif (_Layouts.TryGetValue(_NodeId, out var layout))\r\n\t\t{\r\n\t\t\t// A corner path starting on this arm continues its left border (looking out of the node); one ending on\r\n\t\t\t// it continues its right. Looking out of the node along a segment that ends there swaps the sides.\r\n\t\t\tbool armLeftContinued = layout.CornerPaths.Any(path => path.ArmA.Segment.Id == _Segment.Id);\r\n\t\t\tbool armRightContinued = layout.CornerPaths.Any(path => path.ArmB.Segment.Id == _Segment.Id);\r\n\r\n\t\t\t_Left = !(_AtStart ? armLeftContinued : armRightContinued);\r\n\t\t\t_Right = !(_AtStart ? armRightContinued : armLeftContinued);\r\n\t\t\treturn _Left || _Right;\r\n\t\t}\r\n\r\n\t\tif (degree == 2)\r\n\t\t{\r\n\t\t\tvar other = GetSegmentsAt(_NodeId).FirstOrDefault(candidate => candidate.Id != _Segment.Id);\r\n\t\t\tbool continuous = other is not null && SameEdgesAt(_Segment, other, _NodeId)\r\n\t\t\t\t&& (RoadBorderBuilder.SameProfile(ProfileOf(other), ProfileOf(_Segment)) || MeetsFlush(ProfileOf(other), ProfileOf(_Segment)));\r\n\r\n\t\t\t_Left = !continuous;\r\n\t\t\t_Right = !continuous;\r\n\t\t\treturn !continuous;\r\n\t\t}\r\n\r\n\t\t_Left = true;\r\n\t\t_Right = true;\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Splits a segment into chunks at fixed distances from its start, clipped by the junction trims at each end,\r\n\t/// with a border chunk over each stretch when the road type has borders. Anchoring the boundaries to the\r\n\t/// segment rather than to the trimmed length means a junction change at one end only touches the chunk next\r\n\t/// to it.\r\n\t/// </summary>\r\n\tprivate List<ChunkPlan> PlanChunks(RoadSegment _Segment, RoadCurveTable _Table, float _TrimStart, float _TrimEnd, RoadProfile _Profile, BorderCaps _Caps, in RoadBorderBuilder.SegmentBorderBlends _Blends, in SegmentBorderTapers _Taper, long _SurfaceSignature, in LineRange _Lines, in RoadMarkingBuilder.DashPhase _Dash, in SegmentCrossings _Crossings)\r\n\t{\r\n\t\tfloat length = _Table.Length;\r\n\t\tint count = Math.Max(1, (int)MathF.Ceiling(length / ChunkLength));\r\n\t\tfloat usableEnd = length - _TrimEnd;\r\n\t\tvar ranges = new List<(int Index, float Start, float End)>(count);\r\n\r\n\t\tfor (int i = 0; i < count; i++)\r\n\t\t{\r\n\t\t\tfloat start = MathF.Max(length * i / count, _TrimStart);\r\n\t\t\tfloat end = MathF.Min(length * (i + 1) / count, usableEnd);\r\n\r\n\t\t\tif (end - start >= MinChunkLength)\r\n\t\t\t\tranges.Add((i, start, end));\r\n\t\t}\r\n\r\n\t\tvar plans = new List<ChunkPlan>(ranges.Count * 4);\r\n\t\tvar borderHash = StableHash.Create();\r\n\t\tborderHash.Add(_SurfaceSignature);\r\n\t\t_Profile.AddBorderToHash(ref borderHash);\r\n\t\t_Blends.AddToHash(ref borderHash);\r\n\t\t_Taper.AddToHash(ref borderHash);\r\n\t\tborderHash.Add(DebugBorders ? 1 : 0);\r\n\t\tlong borderSignature = borderHash.Value;\r\n\r\n\t\t// The deck follows the road and stands off its border, so it is stale when either changes; its posts and\r\n\t\t// supports are spread over the whole built range, so every piece is stale when an end moves.\r\n\t\tvar bridge = _Segment.Bridge;\r\n\t\tvar bridgeSpan = bridge is null ? default : ComputeBridgeSpan(_Segment, _TrimStart, usableEnd);\r\n\t\tlong bridgeSignature = 0;\r\n\r\n\t\tif (bridge is not null)\r\n\t\t{\r\n\t\t\tvar bridgeHash = StableHash.Create();\r\n\t\t\tbridgeHash.Add(_SurfaceSignature);\r\n\t\t\tbridgeHash.Add(bridge.ResourcePath);\r\n\t\t\t_Profile.AddBorderToHash(ref bridgeHash);\r\n\t\t\tbridge.AddToHash(ref bridgeHash);\r\n\t\t\tbridgeSpan.AddToHash(ref bridgeHash);\r\n\t\t\tbridgeSignature = bridgeHash.Value;\r\n\t\t}\r\n\r\n\t\t// Props are spread over the whole built range too, and stand on the border, so they follow its shape.\r\n\t\tvar props = _Segment.Props;\r\n\t\tvar propSpan = props is null ? default : new RoadPropBuilder.PropSpan(_TrimStart, usableEnd, GetDegree(_Segment.StartNodeId) >= 3, GetDegree(_Segment.EndNodeId) >= 3);\r\n\t\tlong propSignature = 0;\r\n\r\n\t\tif (props is not null && !props.IsEmpty)\r\n\t\t{\r\n\t\t\tvar propHash = StableHash.Create();\r\n\t\t\tpropHash.Add(_SurfaceSignature);\r\n\t\t\tpropHash.Add(props.ResourcePath);\r\n\t\t\tpropHash.Add(bridge is null ? 0 : 1);\r\n\t\t\t_Profile.AddBorderToHash(ref propHash);\r\n\t\t\tprops.AddToHash(ref propHash);\r\n\t\t\tpropSpan.AddToHash(ref propHash);\r\n\t\t\tpropSignature = propHash.Value;\r\n\t\t}\r\n\r\n\t\t// A road with no lines of its own still needs the piece when it has a crossing to paint.\r\n\t\tbool markings = RoadMarkingBuilder.HasMarkings(_Segment, _Profile) || _Crossings.AtStart || _Crossings.AtEnd;\r\n\t\tvar markingHash = StableHash.Create();\r\n\t\tmarkingHash.Add(_SurfaceSignature);\r\n\t\tmarkingHash.Add(_Segment.LaneCount);\r\n\t\tmarkingHash.Add(_Segment.ForwardLanes);\r\n\t\t_Lines.AddToHash(ref markingHash);\r\n\t\t_Dash.AddToHash(ref markingHash);\r\n\t\t_Profile.AddMarkingsToHash(ref markingHash);\r\n\t\t_Profile.AddCrossingsToHash(ref markingHash);\r\n\t\tmarkingHash.Add(_Crossings.AtStart ? _Crossings.StartCentre : -1.0f);\r\n\t\tmarkingHash.Add(_Crossings.AtEnd ? _Crossings.EndCentre : -1.0f);\r\n\t\tlong markingSignature = markingHash.Value;\r\n\r\n\t\tfor (int r = 0; r < ranges.Count; r++)\r\n\t\t{\r\n\t\t\tvar (index, start, end) = ranges[r];\r\n\r\n\t\t\tvar hash = StableHash.Create();\r\n\t\t\thash.Add(_SurfaceSignature);\r\n\t\t\thash.Add(index);\r\n\t\t\thash.Add(count);\r\n\t\t\thash.Add(start);\r\n\t\t\thash.Add(end);\r\n\r\n\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Surface, BorderCaps.None, default, hash.Value));\r\n\r\n\t\t\tif (markings)\r\n\t\t\t{\r\n\t\t\t\thash = StableHash.Create();\r\n\t\t\t\t// A crossing belongs to one piece: the outermost, since its band may lie clear of every piece's\r\n\t\t\t\t// own stretch of road.\r\n\t\t\t\tvar owned = new SegmentCrossings(_Crossings.AtStart && r == 0, _Crossings.StartCentre,\r\n\t\t\t\t\t_Crossings.AtEnd && r == ranges.Count - 1, _Crossings.EndCentre);\r\n\r\n\t\t\t\thash.Add(markingSignature);\r\n\t\t\t\thash.Add(index);\r\n\t\t\t\thash.Add(count);\r\n\t\t\t\thash.Add(start);\r\n\t\t\t\thash.Add(end);\r\n\t\t\t\thash.Add(owned.AtStart ? 1 : 0);\r\n\t\t\t\thash.Add(owned.AtEnd ? 1 : 0);\r\n\r\n\t\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Marking, BorderCaps.None, default, hash.Value, _Lines, Dash: _Dash, Crossings: owned));\r\n\t\t\t}\r\n\r\n\t\t\tif (bridge is not null)\r\n\t\t\t{\r\n\t\t\t\t// The piece works out for itself whether it is an outermost one, from its stretch against the span.\r\n\t\t\t\thash = StableHash.Create();\r\n\t\t\t\thash.Add(bridgeSignature);\r\n\t\t\t\thash.Add(index);\r\n\t\t\t\thash.Add(count);\r\n\t\t\t\thash.Add(start);\r\n\t\t\t\thash.Add(end);\r\n\r\n\t\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Bridge, BorderCaps.None, default, hash.Value, Bridge: bridgeSpan));\r\n\t\t\t}\r\n\r\n\t\t\tif (propSignature != 0)\r\n\t\t\t{\r\n\t\t\t\thash = StableHash.Create();\r\n\t\t\t\thash.Add(propSignature);\r\n\t\t\t\thash.Add(index);\r\n\t\t\t\thash.Add(count);\r\n\t\t\t\thash.Add(start);\r\n\t\t\t\thash.Add(end);\r\n\r\n\t\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Props, BorderCaps.None, default, hash.Value, Props: propSpan));\r\n\t\t\t}\r\n\r\n\t\t\tif (!_Profile.HasBorder)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Only the outermost stretches have open ends to close.\r\n\t\t\tvar caps = BorderCaps.None;\r\n\r\n\t\t\tif (r == 0)\r\n\t\t\t\tcaps |= _Caps & (BorderCaps.StartLeft | BorderCaps.StartRight);\r\n\r\n\t\t\tif (r == ranges.Count - 1)\r\n\t\t\t\tcaps |= _Caps & (BorderCaps.EndLeft | BorderCaps.EndRight);\r\n\r\n\t\t\thash = StableHash.Create();\r\n\t\t\thash.Add(borderSignature);\r\n\t\t\thash.Add(index);\r\n\t\t\thash.Add(count);\r\n\t\t\thash.Add(start);\r\n\t\t\thash.Add(end);\r\n\t\t\thash.Add((int)caps);\r\n\r\n\t\t\tplans.Add(new ChunkPlan(index, count, start, end, RoadChunkKind.Border, caps, _Blends, hash.Value, default, _Taper));\r\n\t\t}\r\n\r\n\t\t// A road that simply stops is rounded off there, in a piece of its own beyond the node.\r\n\t\tAddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, true);\r\n\t\tAddCapPlan(plans, _Segment, _Table, _Profile, _SurfaceSignature, false);\r\n\r\n\t\treturn plans;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The rounded end of a road, where it has one: at a node with nothing else at it, which is not a way into a\r\n\t/// parking lot (the lot's own kerb closes that), and on a road no bridge carries (a deck ends where it ends).\r\n\t/// The plan's start is the end's own distance along the curve and its end the depth the cap reaches past it.\r\n\t/// </summary>\r\n\tprivate void AddCapPlan(List<ChunkPlan> _Plans, RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, long _SurfaceSignature, bool _AtStart)\r\n\t{\r\n\t\tint nodeId = _AtStart ? _Segment.StartNodeId : _Segment.EndNodeId;\r\n\r\n\t\tif (_Segment.Bridge is not null || GetDegree(nodeId) != 1 || LotAtOpening(nodeId) is not null)\r\n\t\t\treturn;\r\n\r\n\t\tfloat distance = _AtStart ? 0.0f : _Table.Length;\r\n\t\tfloat depth = RoadCapBuilder.Depth(_Profile, new RoadWidth(_Segment, _Table.Length), distance);\r\n\r\n\t\tif (depth <= 0.01f)\r\n\t\t\treturn;\r\n\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\thash.Add(_SurfaceSignature);\r\n\t\thash.Add(_AtStart ? 1 : 0);\r\n\t\thash.Add(depth);\r\n\t\t_Profile.AddBorderToHash(ref hash);\r\n\r\n\t\t_Plans.Add(new ChunkPlan(_AtStart ? 0 : 1, 2, distance, depth, RoadChunkKind.Cap, BorderCaps.None, default, hash.Value));\r\n\t}\r\n\r\n\r\n\r\n\tprivate static long CombineSignatures(List<ChunkPlan> _Plans)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\t\thash.Add(_Plans.Count);\r\n\r\n\t\tforeach (var plan in _Plans)\r\n\t\t\thash.Add(plan.Signature);\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsSegmentUpToDate(List<RoadChunk> _Existing, List<ChunkPlan> _Plans)\r\n\t{\r\n\t\tint have = _Existing?.Count ?? 0;\r\n\r\n\t\tif (have != _Plans.Count)\r\n\t\t\treturn false;\r\n\r\n\t\tforeach (var plan in _Plans)\r\n\t\t{\r\n\t\t\tvar chunk = _Existing.FirstOrDefault(candidate => candidate.ChunkIndex == plan.Index && candidate.Kind == plan.Kind);\r\n\r\n\t\t\tif (chunk is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\t// A locked chunk is exempt: it is deliberately stale.\r\n\t\t\tif (!chunk.Locked && (chunk.Signature != plan.Signature || LacksDebugRecord(chunk)))\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// While the border debug overlay is on, a border piece without its record (built before the flag went on, or\r\n\t/// before a hot reload, which drops the records) is rebuilt to get one.\r\n\t/// </summary>\r\n\tprivate bool LacksDebugRecord(RoadChunk _Chunk)\r\n\t{\r\n\t\treturn DebugBorders && _Chunk.Kind == RoadChunkKind.Border && (_Chunk.Debug is null || _Chunk.Debug.Text.Length == 0);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Throws away every unlocked piece's signature so the next reconcile rebuilds all of them, junction layouts\r\n\t/// included. Toggling the GameObject does not do this: pieces whose inputs have not changed are kept as they\r\n\t/// are, so after a generator change this is the way to see it everywhere.\r\n\t/// </summary>\r\n\t[Button(\"Regenerate All\", \"refresh\")]\r\n\tpublic void RegenerateAll()\r\n\t{\r\n\t\tforeach (var chunk in GetChunks())\r\n\t\t{\r\n\t\t\tif (!chunk.Locked)\r\n\t\t\t\tchunk.Signature = 0;\r\n\t\t}\r\n\r\n\t\tm_JunctionCache.Clear();\r\n\t\tMarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\tprivate long ComputeSurfaceSignature(RoadSegment _Segment, in RoadCurve _Curve, RoadProfile _Profile)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\thash.Add(GeneratorVersion);\r\n\t\thash.Add(_Curve.P0);\r\n\t\thash.Add(_Curve.P1);\r\n\t\thash.Add(_Curve.P2);\r\n\t\thash.Add(_Curve.P3);\r\n\t\tnew RoadWidth(_Segment, 1.0f).AddToHash(ref hash);\r\n\t\thash.Add(SampleSpacing);\r\n\t\thash.Add(ChunkLength);\r\n\t\thash.Add(SurfaceOffset);\r\n\t\t_Profile.AddSurfaceToHash(ref hash);\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Everything a node's junction layout depends on: the node itself and the curve and width of each road at it.\r\n\t/// </summary>\r\n\tprivate long ComputeJunctionKey(RoadNode _Node, float _Radius)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\thash.Add(GeneratorVersion);\r\n\t\thash.Add(_Node.Position);\r\n\t\thash.Add(_Radius);\r\n\t\thash.Add(SurfaceOffset);\r\n\r\n\t\tforeach (var segment in GetSegmentsAt(_Node.Id).OrderBy(segment => segment.Id))\r\n\t\t{\r\n\t\t\thash.Add(segment.Id);\r\n\t\t\tnew RoadWidth(segment, 1.0f).AddToHash(ref hash);\r\n\t\t\thash.Add(segment.Ramp ? 1 : 0);\r\n\t\t\thash.Add(segment.StartNodeId == _Node.Id ? 1 : 0);\r\n\r\n\t\t\tif (!TryGetCurve(segment, out var curve))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\thash.Add(curve.P0);\r\n\t\t\thash.Add(curve.P1);\r\n\t\t\thash.Add(curve.P2);\r\n\t\t\thash.Add(curve.P3);\r\n\t\t}\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static void AddPolygonToHash(RoadJunctionLayout _Layout, ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(GeneratorVersion);\r\n\t\t_Hash.Add(_Layout.Centre);\r\n\t\t_Hash.Add(_Layout.Polygon.Count);\r\n\r\n\t\tforeach (var vertex in _Layout.Polygon)\r\n\t\t\t_Hash.Add(vertex);\r\n\t}\r\n\r\n\r\n\r\n\tprivate long ComputeJunctionSurfaceSignature(RoadJunctionLayout _Layout)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\tAddPolygonToHash(_Layout, ref hash);\r\n\t\tJunctionSurfaceProfile(_Layout).AddSurfaceToHash(ref hash);\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\tprivate long ComputeJunctionBorderSignature(RoadJunctionLayout _Layout, Dictionary<int, RoadBorderBuilder.SegmentBorderBlends> _ArmBlends, Dictionary<int, SegmentBorderTapers> _ArmTapers)\r\n\t{\r\n\t\tvar hash = StableHash.Create();\r\n\r\n\t\tAddPolygonToHash(_Layout, ref hash);\r\n\t\thash.Add(DebugBorders ? 1 : 0);\r\n\r\n\t\tforeach (var arm in _Layout.Arms)\r\n\t\t{\r\n\t\t\tvar profile = ProfileOf(arm.Segment);\r\n\r\n\t\t\thash.Add(arm.Segment.Id);\r\n\t\t\thash.Add(arm.AtStart ? 1 : 0);\r\n\t\t\thash.Add(arm.Length);\r\n\t\t\tprofile.AddSurfaceToHash(ref hash);\r\n\t\t\tprofile.AddBorderToHash(ref hash);\r\n\t\t\t_ArmBlends.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);\r\n\t\t\t_ArmTapers.GetValueOrDefault(arm.Segment.Id).AddToHash(ref hash);\r\n\t\t}\r\n\r\n\t\treturn hash.Value;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// A junction's surface takes after its main road: the widest arm.\r\n\t/// </summary>\r\n\tprivate RoadProfile JunctionSurfaceProfile(RoadJunctionLayout _Layout)\r\n\t{\r\n\t\tvar main = _Layout.Arms.OrderByDescending(arm => arm.Widths.Base).ThenBy(arm => arm.Segment.Id).FirstOrDefault();\r\n\r\n\t\treturn ProfileOf(main?.Segment);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnqueueSegment(RoadSegment _Segment, RoadCurveTable _Table, RoadProfile _Profile, List<ChunkPlan> _Plans, long _GroupSignature, List<RoadChunk> _Existing)\r\n\t{\r\n\t\t// Chunks with no plan (swallowed by a junction, past a shorter road's end, or a border the road no longer\r\n\t\t// has) have no job coming to replace them.\r\n\t\tif (_Existing is not null)\r\n\t\t{\r\n\t\t\tforeach (var chunk in _Existing)\r\n\t\t\t{\r\n\t\t\t\tif (!chunk.Locked && _Plans.All(plan => plan.Index != chunk.ChunkIndex || plan.Kind != chunk.Kind))\r\n\t\t\t\t\tchunk.GameObject.Destroy();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (_Plans.Count == 0)\r\n\t\t{\r\n\t\t\tm_PendingSegments.Remove(_Segment.Id);\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tforeach (var plan in _Plans)\r\n\t\t\tm_Jobs.Enqueue(new RebuildJob(_Segment.Id, -1, plan.Kind, _GroupSignature, plan, _Table, new RoadWidth(_Segment, _Table.Length), _Profile, null, null, null, null));\r\n\r\n\t\tm_PendingSegments[_Segment.Id] = new PendingGroup(_GroupSignature, _Plans.Count);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void ProcessJobs()\r\n\t{\r\n\t\tint budget = Math.Max(1, RebuildBudget);\r\n\r\n\t\twhile (budget > 0 && m_Jobs.Count > 0)\r\n\t\t{\r\n\t\t\tvar job = m_Jobs.Dequeue();\r\n\r\n\t\t\tif (job.Lot is not null)\r\n\t\t\t{\r\n\t\t\t\tvar lotKey = (job.Lot.Id, job.Kind);\r\n\r\n\t\t\t\t// Superseded by a newer version of the same lot, or the lot is gone.\r\n\t\t\t\tif (!m_PendingLots.TryGetValue(lotKey, out var pendingLot) || pendingLot.Signature != job.GroupSignature)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tBuildLotPiece(job);\r\n\t\t\t\tbudget--;\r\n\r\n\t\t\t\tm_PendingLots.Remove(lotKey);\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (job.NodeId >= 0)\r\n\t\t\t{\r\n\t\t\t\tvar key = (job.NodeId, job.Kind);\r\n\r\n\t\t\t\t// Superseded by a newer layout of the same junction, or the junction is gone.\r\n\t\t\t\tif (!m_PendingJunctions.TryGetValue(key, out var pending) || pending.Signature != job.GroupSignature)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tBuildJunctionPiece(job);\r\n\t\t\t\tbudget--;\r\n\r\n\t\t\t\tm_PendingJunctions.Remove(key);\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// Superseded by a newer rebuild of the same segment, or the segment is gone.\r\n\t\t\tif (!m_PendingSegments.TryGetValue(job.SegmentId, out var pendingSegment) || pendingSegment.Signature != job.GroupSignature)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif (BuildChunk(job))\r\n\t\t\t\tbudget--;\r\n\r\n\t\t\tif (pendingSegment.Remaining <= 1)\r\n\t\t\t\tm_PendingSegments.Remove(job.SegmentId);\r\n\t\t\telse\r\n\t\t\t\tm_PendingSegments[job.SegmentId] = pendingSegment with { Remaining = pendingSegment.Remaining - 1 };\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Returns false when there was nothing to do: the chunk in place was already built from these inputs (a\r\n\t/// junction change at the far end of the road requeues the whole road, but only the chunk next to it differs).\r\n\t/// </summary>\r\n\tprivate bool BuildChunk(in RebuildJob _Job)\r\n\t{\r\n\t\tvar plan = _Job.Plan;\r\n\t\tvar existing = FindChunk(_Job.SegmentId, plan.Index, plan.Kind);\r\n\r\n\t\tif (existing is not null && (existing.Locked || (existing.Signature == plan.Signature && !LacksDebugRecord(existing))))\r\n\t\t\treturn false;\r\n\r\n\t\tPolygonMesh mesh;\r\n\t\tstring name;\r\n\t\tBorderDebug debug = null;\r\n\t\tvar placements = new List<(GameObject Prefab, Transform Transform)>();\r\n\r\n\t\tif (plan.Kind == RoadChunkKind.Border)\r\n\t\t{\r\n\t\t\tdebug = DebugBorders ? new BorderDebug() : null;\r\n\t\t\tmesh = RoadBorderBuilder.BuildSegment(_Job.Table, plan.Start, plan.End, _Job.Width, _Job.Profile, SampleSpacing, SurfaceOffset, plan.Caps, plan.Blends, plan.Taper, debug);\r\n\t\t\tname = $\"Road {_Job.SegmentId} border [{plan.Index + 1}/{plan.Count}]\";\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Bridge)\r\n\t\t{\r\n\t\t\tvar segment = FindSegment(_Job.SegmentId);\r\n\r\n\t\t\tif (segment?.Bridge is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tvar parts = new List<RoadBridgeBuilder.Placement>();\r\n\t\t\tmesh = RoadBridgeBuilder.Build(_Job.Table, plan.Start, plan.End, plan.Bridge, _Job.Width, _Job.Profile, segment.Bridge, SampleSpacing, SurfaceOffset, GroundBelow, parts);\r\n\t\t\tname = $\"Road {_Job.SegmentId} bridge [{plan.Index + 1}/{plan.Count}]\";\r\n\r\n\t\t\tforeach (var part in parts)\r\n\t\t\t\tplacements.Add((part.Prefab, new Transform(part.Position, part.Rotation)));\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Props)\r\n\t\t{\r\n\t\t\tvar segment = FindSegment(_Job.SegmentId);\r\n\r\n\t\t\tif (segment?.Props is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\t// Nothing to mesh: the piece is only the parent of its prefabs.\r\n\t\t\tvar props = new List<RoadPropBuilder.Placement>();\r\n\t\t\tvar context = new RoadPropBuilder.Context(_Job.Table, plan.Start, plan.End, plan.Props, _Job.SegmentId, _Job.Width, _Job.Profile, segment.Bridge is not null, SurfaceOffset, GroundAround);\r\n\r\n\t\t\tRoadPropBuilder.Build(context, segment.Props, props);\r\n\t\t\tmesh = null;\r\n\t\t\tname = $\"Road {_Job.SegmentId} props [{plan.Index + 1}/{plan.Count}]\";\r\n\r\n\t\t\tforeach (var prop in props)\r\n\t\t\t\tplacements.Add((prop.Prefab, new Transform(prop.Position, prop.Rotation, prop.Scale)));\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Cap)\r\n\t\t{\r\n\t\t\t// Surface and border in one piece: the whole end is small, and nothing else shares it.\r\n\t\t\tmesh = RoadCapBuilder.Build(_Job.Table, plan.Start, plan.Index == 0, _Job.Width, _Job.Profile, SurfaceOffset, plan.End);\r\n\t\t\tname = $\"Road {_Job.SegmentId} {(plan.Index == 0 ? \"start\" : \"end\")} cap\";\r\n\t\t}\r\n\t\telse if (plan.Kind == RoadChunkKind.Marking)\r\n\t\t{\r\n\t\t\tvar segment = FindSegment(_Job.SegmentId);\r\n\r\n\t\t\tif (segment is null)\r\n\t\t\t\treturn false;\r\n\r\n\t\t\t// Lines only where the chunk overlaps the stretches left clear by the crossings, laid on the rows the\r\n\t\t\t// road surface itself was built on.\r\n\t\t\tmesh = new PolygonMesh();\r\n\r\n\t\t\tvar roadRows = RoadRows.Build(_Job.Table, plan.Start, plan.End, SampleSpacing, _Job.Width, _Job.Profile.Simplification);\r\n\r\n\t\t\tif (plan.Lines.HasGap)\r\n\t\t\t{\r\n\t\t\t\tRoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.GapStart), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);\r\n\t\t\t\tRoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.GapEnd), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tRoadMarkingBuilder.Build(mesh, _Job.Table, roadRows, MathF.Max(plan.Start, plan.Lines.Start), MathF.Min(plan.End, plan.Lines.End), segment, _Job.Width, _Job.Profile, SurfaceOffset, plan.Dash);\r\n\t\t\t}\r\n\r\n\t\t\t// The crossings this piece owns, laid on the same rows as the lines and the road itself.\r\n\t\t\tif (plan.Crossings.AtStart)\r\n\t\t\t\tRoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.StartCentre, _Job.Width, _Job.Profile, SurfaceOffset);\r\n\r\n\t\t\tif (plan.Crossings.AtEnd)\r\n\t\t\t\tRoadMarkingBuilder.BuildCrossing(mesh, _Job.Table, roadRows, plan.Crossings.EndCentre, _Job.Width, _Job.Profile, SurfaceOffset);\r\n\r\n\t\t\tname = $\"Road {_Job.SegmentId} markings [{plan.Index + 1}/{plan.Count}]\";\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tmesh = RoadSurfaceBuilder.Build(_Job.Table, plan.Start, plan.End, _Job.Width, SampleSpacing, _Job.Profile.Simplification, _Job.Profile.RoadTextureRepeat, SurfaceOffset, _Job.Profile.ResolvedRoadMaterial);\r\n\t\t\tname = $\"Road {_Job.SegmentId} [{plan.Index + 1}/{plan.Count}]\";\r\n\t\t}\r\n\r\n\t\tvar piece = CreateChunkObject(name, _Job.SegmentId, -1, plan.Kind, plan.Index, plan.Count, plan.Signature, mesh, debug);\r\n\r\n\t\tforeach (var (prefab, transform) in placements)\r\n\t\t\tprefab.Clone(transform, piece);\r\n\r\n\t\t// The new piece is in place before the old one goes, so the road never flickers.\r\n\t\texisting?.GameObject.Destroy();\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of whatever lies under a point of the network (its own pieces excepted), in network space, for\r\n\t/// bridge supports to land on: the search starts just below the point, so the deck itself is never it. Null\r\n\t/// over nothing.\r\n\t/// </summary>\r\n\tprivate float? GroundBelow(Vector3 _Local)\r\n\t{\r\n\t\treturn TraceGround(_Local, -1.0f);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The height of the ground at a point of the network, for props to stand on: the search starts well above\r\n\t/// the point, so ground rising beside a road is found as well as ground falling away from it.\r\n\t/// </summary>\r\n\tprivate float? GroundAround(Vector3 _Local)\r\n\t{\r\n\t\treturn TraceGround(_Local, 3000.0f);\r\n\t}\r\n\r\n\r\n\r\n\tprivate float? TraceGround(Vector3 _Local, float _StartAbove)\r\n\t{\r\n\t\tconst float reach = 50000.0f;\r\n\r\n\t\tvar transform = WorldTransform;\r\n\t\tVector3 from = transform.PointToWorld(_Local) + Vector3.Up * _StartAbove;\r\n\t\tvar trace = Scene.Trace.Ray(from, from + Vector3.Down * reach).WithoutTags(ChunkTag).Run();\r\n\r\n\t\tif (!trace.Hit)\r\n\t\t\treturn null;\r\n\r\n\t\treturn transform.PointToLocal(trace.EndPosition).z;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildJunctionPiece(in RebuildJob _Job)\r\n\t{\r\n\t\tvar existing = FindJunctionChunk(_Job.NodeId, _Job.Kind);\r\n\r\n\t\tif (existing is not null && existing.Locked)\r\n\t\t\treturn;\r\n\r\n\t\tPolygonMesh mesh;\r\n\t\tstring name;\r\n\t\tBorderDebug debug = null;\r\n\r\n\t\tif (_Job.Kind == RoadChunkKind.Border)\r\n\t\t{\r\n\t\t\tvar armBlends = _Job.ArmBlends;\r\n\t\t\tvar armTapers = _Job.ArmTapers;\r\n\r\n\t\t\tdebug = DebugBorders ? new BorderDebug() : null;\r\n\t\t\tmesh = RoadBorderBuilder.BuildJunction(_Job.Junction, ProfileOf,\r\n\t\t\t\tsegment => armBlends is not null ? armBlends.GetValueOrDefault(segment.Id) : default,\r\n\t\t\t\tsegment => armTapers is not null ? armTapers.GetValueOrDefault(segment.Id) : default,\r\n\t\t\t\tdebug);\r\n\t\t\tname = $\"Junction {_Job.NodeId} border\";\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar profile = JunctionSurfaceProfile(_Job.Junction);\r\n\r\n\t\t\tmesh = RoadJunctionBuilder.Build(_Job.Junction, profile.RoadTextureRepeat, profile.ResolvedRoadMaterial);\r\n\t\t\tname = $\"Junction {_Job.NodeId}\";\r\n\t\t}\r\n\r\n\t\tCreateChunkObject(name, -1, _Job.NodeId, _Job.Kind, 0, 1, _Job.GroupSignature, mesh, debug);\r\n\r\n\t\texisting?.GameObject.Destroy();\r\n\t}\r\n\r\n\r\n\r\n\tprivate GameObject CreateChunkObject(string _Name, int _SegmentId, int _NodeId, RoadChunkKind _Kind, int _Index, int _Count, long _Signature, PolygonMesh _Mesh, BorderDebug _Debug, int _LotId = -1)\r\n\t{\r\n\t\tvar child = new GameObject(GameObject, true, _Name);\r\n\t\tchild.Tags.Add(ChunkTag);\r\n\r\n\t\tvar chunk = child.AddComponent<RoadChunk>();\r\n\t\tchunk.SegmentId = _SegmentId;\r\n\t\tchunk.NodeId = _NodeId;\r\n\t\tchunk.LotId = _LotId;\r\n\t\tchunk.Kind = _Kind;\r\n\t\tchunk.ChunkIndex = _Index;\r\n\t\tchunk.ChunkCount = _Count;\r\n\t\tchunk.Signature = _Signature;\r\n\t\tchunk.Debug = _Debug;\r\n\r\n\t\tif (_Debug is not null)\r\n\t\t\tDebugRevision++;\r\n\r\n\t\t// A piece of props has no geometry of its own; it is the parent of its prefabs.\r\n\t\tif (_Mesh is null)\r\n\t\t\treturn child;\r\n\r\n\t\tvar meshComponent = child.AddComponent<MeshComponent>();\r\n\t\tmeshComponent.Mesh = _Mesh;\r\n\t\tmeshComponent.SmoothingAngle = 40.0f;\r\n\r\n\t\t// Paint lies flat on the road: nothing to collide with, and its shadow would be a sliver hidden under it,\r\n\t\t// at the cost of a shadow pass.\r\n\t\tif (_Kind == RoadChunkKind.Marking)\r\n\t\t{\r\n\t\t\tmeshComponent.Collision = MeshComponent.CollisionType.None;\r\n\t\t\tmeshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;\r\n\t\t}\r\n\r\n\t\treturn child;\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Editor/RoadGizmos.cs",
"FileName": "RoadGizmos.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2.Editor;\r\n\r\n/// <summary>\r\n/// Gizmo drawing shared by the road tools. Curves are in network-local space and drawn through the network's\r\n/// transform, in whatever colour and thickness the caller set up.\r\n/// </summary>\r\ninternal static class RoadGizmos\r\n{\r\n\tpublic static void DrawCurve(in RoadCurve _Curve, in Transform _Transform, int _Steps = 24)\r\n\t{\r\n\t\tVector3 previous = _Transform.PointToWorld(_Curve.P0);\r\n\r\n\t\tfor (int i = 1; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tVector3 point = _Transform.PointToWorld(_Curve.Evaluate((float)i / _Steps));\r\n\r\n\t\t\tGizmo.Draw.Line(previous, point);\r\n\t\t\tprevious = point;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Draws the two edges of a road of the given width along the curve, capped at both ends.\r\n\t/// </summary>\r\n\tpublic static void DrawRoadOutline(in RoadCurve _Curve, float _Width, in Transform _Transform, int _Steps = 24)\r\n\t{\r\n\t\tDrawRoadOutline(_Curve, RoadWidth.Uniform(_Width), _Transform, _Steps);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Draws the two edges of a road along the curve, each its own distance out (the curve's parameter standing in\r\n\t/// for its distance), capped at both ends.\r\n\t/// </summary>\r\n\tpublic static void DrawRoadOutline(in RoadCurve _Curve, in RoadWidth _Width, in Transform _Transform, int _Steps = 24)\r\n\t{\r\n\t\tVector3 previousLeft = default;\r\n\t\tVector3 previousRight = default;\r\n\r\n\t\tfor (int i = 0; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tfloat t = (float)i / _Steps;\r\n\t\t\tfloat distance = t * _Width.Length;\r\n\t\t\tVector3 point = _Curve.Evaluate(t);\r\n\t\t\tVector3 right = RoadCurve.RightOf(_Curve.Tangent(t));\r\n\r\n\t\t\tVector3 left = _Transform.PointToWorld(point - right * _Width.LeftAt(distance));\r\n\t\t\tVector3 rightEdge = _Transform.PointToWorld(point + right * _Width.RightAt(distance));\r\n\r\n\t\t\tif (i == 0 || i == _Steps)\r\n\t\t\t\tGizmo.Draw.Line(left, rightEdge);\r\n\r\n\t\t\tif (i > 0)\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Line(previousLeft, left);\r\n\t\t\t\tGizmo.Draw.Line(previousRight, rightEdge);\r\n\t\t\t}\r\n\r\n\t\t\tpreviousLeft = left;\r\n\t\t\tpreviousRight = rightEdge;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// A node marker: a flat ring on the ground, filled when it is the one being worked on.\r\n\t/// </summary>\r\n\tpublic static void DrawNode(Vector3 _WorldPosition, bool _Filled)\r\n\t{\r\n\t\tfloat radius = RoadPicker.NodeRadius(_WorldPosition);\r\n\r\n\t\tif (_Filled)\r\n\t\t\tGizmo.Draw.SolidSphere(_WorldPosition, radius * 0.6f);\r\n\r\n\t\tGizmo.Draw.LineCircle(_WorldPosition, Vector3.Up, radius);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Geometry/RoadCurve.cs",
"FileName": "RoadCurve.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// The cubic Bezier of one road segment, in network-local space. P1 and P2 are the tangent handles hanging off the\r\n/// two end nodes.\r\n/// </summary>\r\npublic readonly struct RoadCurve : IEquatable<RoadCurve>\r\n{\r\n\tpublic readonly Vector3 P0;\r\n\tpublic readonly Vector3 P1;\r\n\tpublic readonly Vector3 P2;\r\n\tpublic readonly Vector3 P3;\r\n\r\n\r\n\r\n\tpublic RoadCurve(Vector3 _P0, Vector3 _P1, Vector3 _P2, Vector3 _P3)\r\n\t{\r\n\t\tP0 = _P0;\r\n\t\tP1 = _P1;\r\n\t\tP2 = _P2;\r\n\t\tP3 = _P3;\r\n\t}\r\n\r\n\r\n\r\n\tpublic Vector3 Evaluate(float _T)\r\n\t{\r\n\t\tfloat u = 1.0f - _T;\r\n\t\tfloat uu = u * u;\r\n\t\tfloat tt = _T * _T;\r\n\r\n\t\treturn P0 * (uu * u) + P1 * (3.0f * uu * _T) + P2 * (3.0f * u * tt) + P3 * (tt * _T);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Unit direction of travel at <paramref name=\"_T\"/>. Falls back to the chord when the derivative vanishes (a\r\n\t/// handle sitting on its node) so callers always get a usable frame.\r\n\t/// </summary>\r\n\tpublic Vector3 Tangent(float _T)\r\n\t{\r\n\t\tfloat u = 1.0f - _T;\r\n\t\tVector3 derivative = (P1 - P0) * (3.0f * u * u) + (P2 - P1) * (6.0f * u * _T) + (P3 - P2) * (3.0f * _T * _T);\r\n\r\n\t\tif (derivative.IsNearZeroLength)\r\n\t\t\tderivative = P3 - P0;\r\n\r\n\t\tif (derivative.IsNearZeroLength)\r\n\t\t\treturn Vector3.Forward;\r\n\r\n\t\treturn derivative.Normal;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Chord-summed length, good enough to size sampling tables and count chunks.\r\n\t/// </summary>\r\n\tpublic float ApproximateLength(int _Steps = 32)\r\n\t{\r\n\t\tfloat length = 0.0f;\r\n\t\tVector3 previous = P0;\r\n\r\n\t\tfor (int i = 1; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tVector3 point = Evaluate((float)i / _Steps);\r\n\t\t\tlength += Vector3.DistanceBetween(previous, point);\r\n\t\t\tprevious = point;\r\n\t\t}\r\n\r\n\t\treturn length;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// De Casteljau subdivision: the two halves trace exactly the same path as the whole, so a road can be split\r\n\t/// without moving.\r\n\t/// </summary>\r\n\tpublic void Split(float _T, out RoadCurve _Left, out RoadCurve _Right)\r\n\t{\r\n\t\tVector3 q0 = Vector3.Lerp(P0, P1, _T);\r\n\t\tVector3 q1 = Vector3.Lerp(P1, P2, _T);\r\n\t\tVector3 q2 = Vector3.Lerp(P2, P3, _T);\r\n\t\tVector3 r0 = Vector3.Lerp(q0, q1, _T);\r\n\t\tVector3 r1 = Vector3.Lerp(q1, q2, _T);\r\n\t\tVector3 s = Vector3.Lerp(r0, r1, _T);\r\n\r\n\t\t_Left = new RoadCurve(P0, q0, r0, s);\r\n\t\t_Right = new RoadCurve(s, r1, q2, P3);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The road's right-hand direction for a direction of travel: flat, perpendicular to the tangent, and\r\n\t/// consistent with <see cref=\"Rotation.Right\"/> for a rotation looking down the tangent with world up.\r\n\t/// </summary>\r\n\tpublic static Vector3 RightOf(Vector3 _Tangent)\r\n\t{\r\n\t\tVector3 right = Vector3.Cross(_Tangent, Vector3.Up);\r\n\r\n\t\t// Vertical tangent (a ramp seen from nowhere useful): any horizontal axis will do.\r\n\t\tif (right.IsNearZeroLength)\r\n\t\t\treturn Vector3.Right;\r\n\r\n\t\treturn right.Normal;\r\n\t}\r\n\r\n\r\n\r\n\tpublic bool Equals(RoadCurve _Other)\r\n\t{\r\n\t\treturn P0 == _Other.P0 && P1 == _Other.P1 && P2 == _Other.P2 && P3 == _Other.P3;\r\n\t}\r\n\r\n\r\n\r\n\tpublic override bool Equals(object _Obj)\r\n\t{\r\n\t\treturn _Obj is RoadCurve other && Equals(other);\r\n\t}\r\n\r\n\r\n\r\n\tpublic override int GetHashCode()\r\n\t{\r\n\t\treturn HashCode.Combine(P0, P1, P2, P3);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Network/RoadCapBuilder.cs",
"FileName": "RoadCapBuilder.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the rounded end of a road that stops: a half disc of surface carried on past the last row, with the\r\n/// border swept round it, so a dead end reads as a turning head rather than a road sliced through. The piece is\r\n/// built beyond the road's own end, on the end row itself, so its seam with the last chunk is exact and nothing\r\n/// about the road's length changes.\r\n/// </summary>\r\ninternal static class RoadCapBuilder\r\n{\r\n\t/// <summary>\r\n\t/// The arc is drawn in steps of about this many degrees.\r\n\t/// </summary>\r\n\tprivate const float ArcStepDegrees = 9.0f;\r\n\r\n\t/// <summary>\r\n\t/// How deep a road's rounded end reaches past it, as a share of the road's half width. 1 is a half circle.\r\n\t/// </summary>\r\n\tpublic const float DepthFactor = 1.0f;\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// How far the rounded end of a road reaches past the node, or 0 where the road type does not round its ends.\r\n\t/// </summary>\r\n\tpublic static float Depth(RoadProfile _Profile, in RoadWidth _Width, float _Distance)\r\n\t{\r\n\t\tif (_Profile is null || !_Profile.RoundedEnds)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\treturn (_Width.LeftAt(_Distance) + _Width.RightAt(_Distance)) * 0.5f * DepthFactor;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The rounded end at one end of a road. <paramref name=\"_Distance\"/> is the end's own distance along the curve\r\n\t/// (0 or the table's length) and <paramref name=\"_AtStart\"/> which end it is; the cap reaches out from there.\r\n\t/// </summary>\r\n\tpublic static PolygonMesh Build(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, float _Depth)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\r\n\t\tif (_Depth <= 0.01f)\r\n\t\t\treturn mesh;\r\n\r\n\t\t_Table.Frame(_Distance, out Vector3 centre, out Vector3 tangent);\r\n\r\n\t\tcentre += Vector3.Up * _Lift;\r\n\r\n\t\t// Out of the road, and the road's own right at that end. The arc keeps the road's grade because the\r\n\t\t// direction it reaches along carries the curve's climb with it.\r\n\t\tVector3 forward = (_AtStart ? -tangent : tangent).Normal;\r\n\t\tVector3 right = RoadCurve.RightOf(tangent);\r\n\t\tfloat leftHalf = _Width.LeftAt(_Distance);\r\n\t\tfloat rightHalf = _Width.RightAt(_Distance);\r\n\r\n\t\tif (leftHalf + rightHalf <= 0.01f)\r\n\t\t\treturn mesh;\r\n\r\n\t\tint steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));\r\n\t\tvar outline = new List<Vector3>(steps + 1);\r\n\t\tvar outwards = new List<Vector3>(steps + 1);\r\n\t\tvar offsets = new List<float>(steps + 1);\r\n\t\tvar aheads = new List<float>(steps + 1);\r\n\r\n\t\tArc(centre, forward, right, leftHalf, rightHalf, _Depth, steps, outline, outwards, offsets, aheads);\r\n\r\n\t\tAddSurface(mesh, centre, outline, offsets, aheads, _Distance, _AtStart, _Profile);\r\n\t\tAddBorder(mesh, outline, outwards, _Profile);\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The outline of a road's rounded end in network space, lifted onto the road surface, for the tools that need\r\n\t/// to know where road is (the ground under it is held down and brought up to its kerb like a junction's).\r\n\t/// Empty where the road does not round that end.\r\n\t/// </summary>\r\n\tpublic static List<Vector3> Outline(RoadCurveTable _Table, float _Distance, bool _AtStart, in RoadWidth _Width, RoadProfile _Profile, float _Lift, out Vector3 _Centre)\r\n\t{\r\n\t\tvar outline = new List<Vector3>();\r\n\t\tfloat depth = Depth(_Profile, _Width, _Distance);\r\n\r\n\t\t_Table.Frame(_Distance, out _Centre, out Vector3 tangent);\r\n\r\n\t\t_Centre += Vector3.Up * _Lift;\r\n\r\n\t\tfloat leftHalf = _Width.LeftAt(_Distance);\r\n\t\tfloat rightHalf = _Width.RightAt(_Distance);\r\n\r\n\t\tif (depth <= 0.01f || leftHalf + rightHalf <= 0.01f)\r\n\t\t\treturn outline;\r\n\r\n\t\tint steps = Math.Max(4, (int)MathF.Ceiling(180.0f / ArcStepDegrees));\r\n\r\n\t\tArc(_Centre, (_AtStart ? -tangent : tangent).Normal, RoadCurve.RightOf(tangent), leftHalf, rightHalf, depth, steps, outline, null, null, null);\r\n\r\n\t\treturn outline;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The half ellipse from the road's left edge, round the tip, to its right edge: where each point is, which way\r\n\t/// is out of the road there, and how far across and ahead of the end row it sits.\r\n\t/// </summary>\r\n\tprivate static void Arc(Vector3 _Centre, Vector3 _Forward, Vector3 _Right, float _LeftHalf, float _RightHalf, float _Depth, int _Steps,\r\n\t\tList<Vector3> _Points, List<Vector3> _Outwards, List<float> _Offsets, List<float> _Aheads)\r\n\t{\r\n\t\tfor (int i = 0; i <= _Steps; i++)\r\n\t\t{\r\n\t\t\tfloat angle = MathF.PI * (i / (float)_Steps - 0.5f);\r\n\t\t\tfloat sin = MathF.Sin(angle);\r\n\t\t\tfloat cos = MathF.Cos(angle);\r\n\t\t\tfloat offset = sin * (sin >= 0.0f ? _RightHalf : _LeftHalf);\r\n\t\t\tfloat ahead = _Depth * cos;\r\n\r\n\t\t\t_Points.Add(_Centre + _Right * offset + _Forward * ahead);\r\n\t\t\t_Outwards?.Add((_Right * sin + _Forward * cos).Normal);\r\n\t\t\t_Offsets?.Add(offset);\r\n\t\t\t_Aheads?.Add(ahead);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The half disc of road surface: a fan from the middle of the end row out to the arc, so every triangle is\r\n\t/// planar and the row itself is the fan's own base.\r\n\t/// </summary>\r\n\tprivate static void AddSurface(PolygonMesh _Mesh, Vector3 _Centre, List<Vector3> _Outline, List<float> _Offsets, List<float> _Aheads,\r\n\t\tfloat _Distance, bool _AtStart, RoadProfile _Profile)\r\n\t{\r\n\t\tfloat repeat = MathF.Max(1.0f, _Profile.RoadTextureRepeat);\r\n\t\tvar material = _Profile.ResolvedRoadMaterial;\r\n\t\tvar centre = _Mesh.AddVertices(_Centre)[0];\r\n\t\tvar ring = new HalfEdgeMesh.VertexHandle[_Outline.Count];\r\n\r\n\t\tfor (int i = 0; i < _Outline.Count; i++)\r\n\t\t\tring[i] = _Mesh.AddVertices(_Outline[i])[0];\r\n\r\n\t\t// U runs across the road from its centreline and V along it, the same as the road's own surface, so the\r\n\t\t// asphalt carries on through the seam instead of restarting at it.\r\n\t\tVector2 centreUv = new(0.0f, _Distance / repeat);\r\n\r\n\t\t// Which way round the fan closes, so it faces up like the road it carries on from.\r\n\t\tVector3 first = _Outline[0] - _Centre;\r\n\t\tVector3 second = _Outline[1] - _Centre;\r\n\t\tbool flip = Vector3.Cross(first, second).z < 0.0f;\r\n\r\n\t\tfor (int i = 0; i < _Outline.Count - 1; i++)\r\n\t\t{\r\n\t\t\tvar uvA = new Vector2(_Offsets[i] / repeat, (_Distance + (_AtStart ? -_Aheads[i] : _Aheads[i])) / repeat);\r\n\t\t\tvar uvB = new Vector2(_Offsets[i + 1] / repeat, (_Distance + (_AtStart ? -_Aheads[i + 1] : _Aheads[i + 1])) / repeat);\r\n\r\n\t\t\tif (flip)\r\n\t\t\t\tMeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i + 1], ring[i], centreUv, uvB, uvA);\r\n\t\t\telse\r\n\t\t\t\tMeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i], ring[i + 1], centreUv, uvA, uvB);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The border round the arc, open at both ends: what closes it off is the road's own border, which runs into\r\n\t/// it at the end row.\r\n\t/// </summary>\r\n\tprivate static void AddBorder(PolygonMesh _Mesh, List<Vector3> _Outline, List<Vector3> _Outwards, RoadProfile _Profile)\r\n\t{\r\n\t\tif (!_Profile.HasBorder)\r\n\t\t\treturn;\r\n\r\n\t\tvar path = new List<RoadBorderBuilder.PathPoint>(_Outline.Count);\r\n\t\tfloat along = 0.0f;\r\n\r\n\t\tfor (int i = 0; i < _Outline.Count; i++)\r\n\t\t{\r\n\t\t\tif (i > 0)\r\n\t\t\t\talong += _Outline[i].Distance(_Outline[i - 1]);\r\n\r\n\t\t\tpath.Add(new RoadBorderBuilder.PathPoint(_Outline[i], _Outwards[i], along));\r\n\t\t}\r\n\r\n\t\tRoadBorderBuilder.BuildPath(_Mesh, _Profile, path, false, false);\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Network/RoadJunctionBuilder.cs",
"FileName": "RoadJunctionBuilder.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// Generates the surface of a junction: the layout's polygon fanned out from the node. Every triangle shares the\r\n/// centre, so each one is planar and none can be dropped by the mesh builder, whatever the arms' heights do.\r\n/// </summary>\r\ninternal static class RoadJunctionBuilder\r\n{\r\n\tpublic static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)\r\n\t{\r\n\t\tvar mesh = new PolygonMesh();\r\n\t\tvar polygon = _Layout.Polygon;\r\n\r\n\t\tif (polygon.Count < 3)\r\n\t\t\treturn mesh;\r\n\r\n\t\tvar centre = mesh.AddVertices(_Layout.Centre)[0];\r\n\t\tvar ring = mesh.AddVertices(polygon.ToArray());\r\n\t\tVector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);\r\n\r\n\t\tfor (int i = 0; i < polygon.Count; i++)\r\n\t\t{\r\n\t\t\tint next = (i + 1) % polygon.Count;\r\n\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],\r\n\t\t\t\tcentreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));\r\n\t\t}\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// Junctions have no single direction to run a texture along, so they use a top-down projection.\r\n\t/// </summary>\r\n\tprivate static Vector2 PlanarUv(Vector3 _Position, float _TextureRepeat)\r\n\t{\r\n\t\treturn new Vector2(_Position.x, _Position.y) / _TextureRepeat;\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Code/Profiles/ParkingStyle.cs",
"FileName": "ParkingStyle.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System.Text.Json.Serialization;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// How a parking lot is laid out inside: the bays round its edges and, on a lot with room for them, the rows down\r\n/// its middle. Lots reference a style, so every car park built from it changes together when the asset is edited.\r\n/// </summary>\r\n[AssetType(Name = \"Parking Style\", Extension = \"park\", Category = \"Road Tool 2\")]\r\npublic sealed class ParkingStyle : GameResource\r\n{\r\n\t/// <summary>\r\n\t/// How wide one bay is, measured along the edge it stands on.\r\n\t/// </summary>\r\n\t[Group(\"Bays\"), Range(80.0f, 400.0f)] public float BayWidth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 150.0f;\r\n\r\n\t/// <summary>\r\n\t/// How far a bay reaches in from the edge.\r\n\t/// </summary>\r\n\t[Group(\"Bays\"), Range(100.0f, 800.0f)] public float BayDepth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 260.0f;\r\n\r\n\t/// <summary>\r\n\t/// Bays turned this far from square to the edge they stand on, so cars drive into them at an angle. The bays\r\n\t/// on the far side of an aisle lean the other way, as they do in a real car park.\r\n\t/// </summary>\r\n\t[Group(\"Bays\"), Range(0.0f, 60.0f)] public float BayAngle { get; set { field = value.Clamp(0.0f, 80.0f); } } = 0.0f;\r\n\r\n\t/// <summary>\r\n\t/// Bays round the inside of the lot's edges.\r\n\t/// </summary>\r\n\t[Group(\"Bays\")] public bool EdgeBays { get; set; } = true;\r\n\r\n\t/// <summary>\r\n\t/// Back to back rows of bays down the middle of the lot, as many as fit with an aisle between each of them.\r\n\t/// </summary>\r\n\t[Group(\"Rows\")] public bool CentreRows { get; set; } = true;\r\n\r\n\t/// <summary>\r\n\t/// How much room is left for driving between one row of bays and the next.\r\n\t/// </summary>\r\n\t[Group(\"Rows\"), Range(300.0f, 1500.0f)] public float AisleWidth { get; set { field = value.Clamp(50.0f, 100000.0f); } } = 600.0f;\r\n\r\n\t/// <summary>\r\n\t/// How the bay lines are painted. Empty paints them like a road's lane lines.\r\n\t/// </summary>\r\n\t[Group(\"Markings\")] public RoadLineStyle Line { get; set; } = new() { Width = 8.0f };\r\n\r\n\t/// <summary>\r\n\t/// A line along the back of each row, closing its bays off.\r\n\t/// </summary>\r\n\t[Group(\"Markings\")] public bool BackLine { get; set; } = false;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedLineMaterial => Line?.Material ?? Material.Load(\"roadtool2/materials/linewhite.vmat\");\r\n\r\n\t/// <summary>\r\n\t/// How far apart two bay lines sit along the edge, which is wider than a bay once the bays lean.\r\n\t/// </summary>\r\n\t[Hide, JsonIgnore] public float Pitch => BayWidth / float.Cos(BayAngle.DegreeToRadian());\r\n\r\n\r\n\r\n\tpublic void AddToHash(ref StableHash _Hash)\r\n\t{\r\n\t\t_Hash.Add(BayWidth);\r\n\t\t_Hash.Add(BayDepth);\r\n\t\t_Hash.Add(BayAngle);\r\n\t\t_Hash.Add(EdgeBays ? 1 : 0);\r\n\t\t_Hash.Add(CentreRows ? 1 : 0);\r\n\t\t_Hash.Add(AisleWidth);\r\n\t\t_Hash.Add(BackLine ? 1 : 0);\r\n\t\t_Hash.Add(Line is null ? 0 : 1);\r\n\t\tLine?.AddToHash(ref _Hash);\r\n\t}\r\n\r\n\r\n\r\n\t/// <summary>\r\n\t/// The asset was saved: every network with a lot in this style has stale pieces now.\r\n\t/// </summary>\r\n\tprotected override void PostReload()\r\n\t{\r\n\t\tbase.PostReload();\r\n\r\n\t\tRoadNetwork.Editor_MarkDirty();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\r\n\t{\r\n\t\treturn CreateSimpleAssetTypeIcon(\"local_parking\", _Width, _Height, \"#8fd3ff\", \"black\");\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Network/BorderDebug.cs",
"FileName": "BorderDebug.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System.Collections.Generic;\r\nusing System.Text;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\n/// <summary>\r\n/// What a border piece was built from, kept on the piece while the network's debug overlay is on: one line per row\r\n/// from the kerb to the outer edge, coloured by the run it belongs to, plus a plain-text record of every number\r\n/// that went into the rows, corners and patches (the editor writes those out to a file). Diagnostic only.\r\n/// </summary>\r\npublic sealed class BorderDebug\r\n{\r\n\tpublic readonly record struct DebugLine(Vector3 From, Vector3 To, Color Color);\r\n\r\n\tpublic readonly record struct DebugMarker(Vector3 Position, Color Color);\r\n\r\n\tpublic static readonly Color ArmColor = Color.Green;\r\n\tpublic static readonly Color FirstHalfColor = Color.Red;\r\n\tpublic static readonly Color SecondHalfColor = Color.Cyan;\r\n\tpublic static readonly Color PatchColor = Color.Yellow;\r\n\tpublic static readonly Color CapColor = Color.Magenta;\r\n\r\n\tpublic List<DebugLine> Lines { get; } = new();\r\n\r\n\tpublic List<DebugMarker> Markers { get; } = new();\r\n\r\n\t/// <summary>\r\n\t/// The record, one entry per line.\r\n\t/// </summary>\r\n\tpublic StringBuilder Text { get; } = new();\r\n\r\n\r\n\r\n\tpublic void Line(Vector3 _From, Vector3 _To, Color _Color)\r\n\t{\r\n\t\tLines.Add(new DebugLine(_From, _To, _Color));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Marker(Vector3 _Position, Color _Color)\r\n\t{\r\n\t\tMarkers.Add(new DebugMarker(_Position, _Color));\r\n\t}\r\n\r\n\r\n\r\n\tpublic void Note(string _Text)\r\n\t{\r\n\t\tText.AppendLine(_Text);\r\n\t}\r\n\r\n\r\n\r\n\tpublic static string Format(Vector3 _Vector)\r\n\t{\r\n\t\treturn $\"({_Vector.x:0.###} {_Vector.y:0.###} {_Vector.z:0.###})\";\r\n\t}\r\n\r\n\r\n\r\n\tpublic static string Format(Vector2 _Vector)\r\n\t{\r\n\t\treturn $\"({_Vector.x:0.###} {_Vector.y:0.###})\";\r\n\t}\r\n}\r\n"
},
{
"Ident": "redsnail.roadtool2",
"Path": "Network/RoadNetwork.Debug.cs",
"FileName": "RoadNetwork.Debug.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 426010,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool2;\r\n\r\npublic sealed partial class RoadNetwork\r\n{\r\n\t/// <summary>\r\n\t/// The whole border debug record as text: the graph and generation settings, then what every border piece was\r\n\t/// built from. Only pieces built while <see cref=\"DebugBorders\"/> was on carry a record.\r\n\t/// </summary>\r\n\tpublic string BuildDebugRecord()\r\n\t{\r\n\t\tvar text = new StringBuilder();\r\n\r\n\t\ttext.AppendLine($\"ROADTOOL2 BORDER DEBUG generator={GeneratorVersion} revision={DebugRevision} time={DateTime.Now:HH:mm:ss}\");\r\n\t\ttext.AppendLine($\"NETWORK name=\\\"{GameObject.Name}\\\" surfaceOffset={SurfaceOffset} spacing={SampleSpacing} chunkLength={ChunkLength} defaultProfile={DefaultProfile?.ResourceName ?? \"(none)\"}\");\r\n\r\n\t\tforeach (var node in Nodes)\r\n\t\t\ttext.AppendLine($\"NODE id={node.Id} position={BorderDebug.Format(node.Position)} cornerRadius={node.CornerRadius} autoTangents={(node.AutoTangents ? 1 : 0)} degree={GetDegree(node.Id)}\");\r\n\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\ttext.AppendLine($\"SEGMENT id={segment.Id} start={segment.StartNodeId} end={segment.EndNodeId} startTangent={BorderDebug.Format(segment.StartTangent)} endTangent={BorderDebug.Format(segment.EndTangent)} width={segment.Width} profile={ProfileOf(segment).ResourceName}\");\r\n\r\n\t\t\tif (TryGetCurve(segment, out var curve))\r\n\t\t\t\ttext.AppendLine($\"CURVE segment={segment.Id} p0={BorderDebug.Format(curve.P0)} p1={BorderDebug.Format(curve.P1)} p2={BorderDebug.Format(curve.P2)} p3={BorderDebug.Format(curve.P3)} length={GetCurveTable(segment)?.Length ?? 0.0f:0.###}\");\r\n\t\t}\r\n\r\n\t\tforeach (var chunk in GetChunks().OrderBy(chunk => chunk.IsJunction ? 1 : 0).ThenBy(chunk => chunk.IsJunction ? chunk.NodeId : chunk.SegmentId).ThenBy(chunk => chunk.ChunkIndex))\r\n\t\t{\r\n\t\t\tif (chunk.Kind != RoadChunkKind.Border)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\ttext.AppendLine();\r\n\t\t\ttext.AppendLine($\"PIECE name=\\\"{chunk.GameObject.Name}\\\" segment={chunk.SegmentId} node={chunk.NodeId} index={chunk.ChunkIndex} count={chunk.ChunkCount} record={(chunk.Debug is not null ? 1 : 0)}\");\r\n\r\n\t\t\tif (chunk.Debug is not null)\r\n\t\t\t\ttext.Append(chunk.Debug.Text);\r\n\t\t}\r\n\r\n\t\treturn text.ToString();\r\n\t}\r\n}\r\n"
}
]
}