{"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/// \u003Csummary\u003E\r\n/// How one side\u0027s 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/// \u003C/summary\u003E\r\n/// \u003Cparam name=\u0022StartRelief\u0022\u003E\r\n/// The share of the profile\u0027s 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/// \u003C/param\u003E\r\n/// \u003Cparam name=\u0022StartOffset\u0022\u003E\r\n/// How far from the start node the taper ends: 0 at a plain pass-through, the arm\u0027s 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/// \u003C/param\u003E\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/// \u003Csummary\u003E\r\n\t/// The share of the profile\u0027s relief in force at a curve distance (1 away from any taper, that end\u0027s 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\u0027s neighbour spills onto it. The two ends\u0027 tapers, offsets included,\r\n\t/// are each held to half the segment, so no point is ever inside both.\r\n\t/// \u003C/summary\u003E\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 \u003E 0.0f \u0026\u0026 _Along \u003C StartOffset \u002B 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 \u003E 0.0f \u0026\u0026 _Length - _Along \u003C EndOffset \u002B 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 \u003E _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/// \u003Csummary\u003E\r\n\t/// How far towards the node\u0027s 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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// The factor the border\u0027s nominal width is scaled by at a curve distance, on its way to the neighbour\u0027s.\r\n\t/// \u003C/summary\u003E\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 \u003C= 0.0f || _NominalWidth \u003C= 0.001f)\r\n\t\t\treturn 1.0f;\r\n\r\n\t\treturn (_NominalWidth \u002B (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/// \u003Csummary\u003E\r\n\t/// Whether the border tapers at the end a node is at.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool TapersAt(bool _AtStart)\r\n\t{\r\n\t\treturn _AtStart ? StartLength \u003E 0.0f : EndLength \u003E 0.0f;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The tapers of a segment\u0027s two borders, looking along the segment.\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\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\u0027s curve, thinned where the road type allows it).\r\n/// \u003C/summary\u003E\r\ninternal static class RoadSurfaceBuilder\r\n{\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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 \u002B= 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 \u002B 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 \u003C rows.Count; i\u002B\u002B)\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 \u003C rows.Count - 1; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i], right[i \u002B 1], uvLeft[i], uvRight[i], uvRight[i \u002B 1]);\r\n\t\t\tMeshUtility.AddTexturedTriangle(mesh, _Material, left[i], right[i \u002B 1], left[i \u002B 1], uvLeft[i], uvRight[i \u002B 1], uvLeft[i \u002B 1]);\r\n\t\t}\r\n\r\n\t\treturn mesh;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\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\u0027s plane is used even for a point just off the cell (paint hanging over\r\n\t/// the road\u0027s edge), which keeps that paint on the surface\u0027s own plane.\r\n\t/// \u003C/summary\u003E\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 \u003E= 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/// \u003Csummary\u003E\r\n\t/// The height of the plane through three points over a point in plan.\r\n\t/// \u003C/summary\u003E\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) \u003C 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 \u002B weightB * _B.z \u002B (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/// \u003Csummary\u003E\r\n/// Writes each network\u0027s 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/// \u003C/summary\u003E\r\npublic static class RoadBorderDebugDump\r\n{\r\n\tprivate static readonly Dictionary\u003CGuid, int\u003E 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\u003CRoadNetwork\u003E();\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) \u0026\u0026 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($\u0022Road Tool 2: border debug written to {path}\u0022);\r\n\t\t}\r\n\t\tcatch (Exception exception)\r\n\t\t{\r\n\t\t\tLog.Warning($\u0022Road Tool 2: could not write the border debug record: {exception.Message}\u0022);\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, \u0022Libraries\u0022, \u0022RoadTool2\u0022);\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, \u0022Libraries\u0022)))\r\n\t\t\t{\r\n\t\t\t\tif (File.Exists(Path.Combine(directory, \u0022roadtool2.sbproj\u0022)))\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(\u0027 \u0027, \u0027_\u0027);\r\n\r\n\t\treturn Path.Combine(library, \u0022debug\u0022, $\u0022borders_{name}.txt\u0022);\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/// \u003Csummary\u003E\r\n/// What stands along the two edges of a bridge deck.\r\n/// \u003C/summary\u003E\r\npublic enum BridgeSideStyle\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Nothing above the deck: its side face is all there is.\r\n\t/// \u003C/summary\u003E\r\n\tNone,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A solid parapet wall.\r\n\t/// \u003C/summary\u003E\r\n\tWall,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Posts at intervals with horizontal rails running between them.\r\n\t/// \u003C/summary\u003E\r\n\tRailing,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A prefab of your own repeated along the edge (a fence panel, a lamp, a cable anchor...).\r\n\t/// \u003C/summary\u003E\r\n\tPrefab\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// What holds a bridge deck up.\r\n/// \u003C/summary\u003E\r\npublic enum BridgeSupportStyle\r\n{\r\n\tNone,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Pillars at intervals, from the underside of the deck down into the ground.\r\n\t/// \u003C/summary\u003E\r\n\tPillars,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A prefab of your own at intervals under the deck (a pier, a tower...).\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// One pillar under the middle of the deck, or one under each edge.\r\n/// \u003C/summary\u003E\r\npublic enum BridgePillarLayout\r\n{\r\n\tSingle,\r\n\tPair\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// How a stretch of road becomes a bridge: the deck under it, what stands along the deck\u0027s 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/// \u003C/summary\u003E\r\n[AssetType(Name = \u0022Bridge Style\u0022, Extension = \u0022bridg\u0022, Category = \u0022Road Tool 2\u0022)]\r\npublic sealed class BridgeStyle : GameResource\r\n{\r\n\tprivate const string DefaultMaterial = \u0022roadtool2/materials/concrete.vmat\u0022;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Material of the deck\u0027s sides and underside (and of the walls and pillars when they have none of their own).\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Deck\u0022)] public Material DeckMaterial { get; set; }\r\n\t[Group(\u0022Deck\u0022), Range(10.0f, 10000.0f)] public float DeckTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 500.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The deck\u0027s thickness below the road surface.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Deck\u0022), Range(5.0f, 500.0f)] public float DeckDepth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 60.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A bare ledge of deck outside the side (wall, railing) on each edge. 0 ends the deck at the side\u0027s outer face.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Deck\u0022), 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(\u0022Side\u0022)] public BridgeSideStyle Side { get; set; } = BridgeSideStyle.Wall;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Material of the wall or the railing. Empty uses the deck\u0027s.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Side\u0022), ShowIf(nameof(HasBuiltSide), true)] public Material SideMaterial { get; set; }\r\n\t[Group(\u0022Side\u0022), 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/// \u003Csummary\u003E\r\n\t/// The wall\u0027s thickness, or the railing\u0027s post size; also the width of the strip a prefab side stands on.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Side\u0022), 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/// \u003Csummary\u003E\r\n\t/// Height of the wall or railing above the road\u0027s border (the pavement top when there is one).\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Side\u0022), 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(\u0022Railing\u0022), 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(\u0022Railing\u0022), ShowIf(nameof(Side), BridgeSideStyle.Railing), Range(1, 6)] public int RailCount { get; set { field = value.Clamp(1, 32); } } = 2;\r\n\t[Group(\u0022Railing\u0022), 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/// \u003Csummary\u003E\r\n\t/// Placed on the deck\u0027s edge strip with its forward along the road and its right pointing away from the road.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Side\u0022), ShowIf(nameof(Side), BridgeSideStyle.Prefab)] public GameObject SidePrefab { get; set; }\r\n\t[Group(\u0022Side\u0022), 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(\u0022Supports\u0022)] public BridgeSupportStyle Supports { get; set; } = BridgeSupportStyle.Pillars;\r\n\t[Group(\u0022Supports\u0022), 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/// \u003Csummary\u003E\r\n\t/// The pillars\u0027 material. Empty uses the deck\u0027s.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Supports\u0022), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public Material SupportMaterial { get; set; }\r\n\t[Group(\u0022Supports\u0022), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarShape PillarShape { get; set; } = BridgePillarShape.Square;\r\n\t[Group(\u0022Supports\u0022), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars)] public BridgePillarLayout PillarLayout { get; set; } = BridgePillarLayout.Single;\r\n\t[Group(\u0022Supports\u0022), 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/// \u003Csummary\u003E\r\n\t/// For a pair of pillars: how far in from the deck\u0027s edges they stand.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Supports\u0022), 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(\u0022Supports\u0022), ShowIf(nameof(Supports), BridgeSupportStyle.Pillars), Range(3, 32)] public int PillarSegments { get; set { field = value.Clamp(3, 64); } } = 12;\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Group(\u0022Supports\u0022), 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/// \u003Csummary\u003E\r\n\t/// How far below the deck a support reaches when there is no ground under it to land on.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Supports\u0022), 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Group(\u0022Supports\u0022), ShowIf(nameof(Supports), BridgeSupportStyle.Prefab)] public GameObject SupportPrefab { get; set; }\r\n\r\n\t[Hide, JsonIgnore] public bool HasBuiltSide =\u003E Side is BridgeSideStyle.Wall or BridgeSideStyle.Railing;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedDeckMaterial =\u003E DeckMaterial ?? Material.Load(DefaultMaterial);\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedSideMaterial =\u003E SideMaterial ?? ResolvedDeckMaterial;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedSupportMaterial =\u003E SupportMaterial ?? ResolvedDeckMaterial;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How wide the strip along each deck edge is that the side occupies (nothing for a bare deck).\r\n\t/// \u003C/summary\u003E\r\n\t[Hide, JsonIgnore] public float SideStripWidth =\u003E Side == BridgeSideStyle.None ? 0.0f : SideWidth;\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// The asset was saved: every network with a bridge in this style has stale pieces now.\r\n\t/// \u003C/summary\u003E\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(\u0022architecture\u0022, _Width, _Height, \u0022#7fb2ff\u0022, \u0022black\u0022);\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\u003CVector3, HalfEdgeMesh.VertexHandle\u003E _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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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\u003CVector2\u003E { _UvA, _UvB, _UvC, _UvD });\r\n\r\n\t\treturn face;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Adds a triangle face; returns an invalid handle when the mesh refused it.\r\n\t/// \u003C/summary\u003E\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\u003CVector2\u003E { _UvA, _UvB, _UvC });\r\n\r\n\t\treturn face;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Paints a face\u0027s vertices with blend weights (what a blendable material mixes its layers by), in the order\r\n\t/// the face\u0027s vertices were given.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void SetFaceBlends(PolygonMesh _Mesh, HalfEdgeMesh.FaceHandle _Face, IReadOnlyList\u003CColor32\u003E _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 \u003C count; i\u002B\u002B)\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/// \u003Csummary\u003E\r\n/// Arc-length table over a \u003Csee cref=\u0022RoadCurve\u0022/\u003E, 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/// \u003C/summary\u003E\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 =\u003E m_Distances[^1];\r\n\r\n\tpublic int SampleCount =\u003E m_Positions.Length;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The box the curve itself runs through. Picking widens it by the road\u0027s own reach and skips the road\r\n\t/// altogether when the cursor\u0027s ray misses it, which is most of them in a city.\r\n\t/// \u003C/summary\u003E\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 \u002B 1];\r\n\t\tm_Distances = new float[count \u002B 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 \u003C= count; i\u002B\u002B)\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] \u002B 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/// \u003Csummary\u003E\r\n\t/// Builds a table with a sample density that suits the curve\u0027s length.\r\n\t/// \u003C/summary\u003E\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 \u003C= 0.0f)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tif (_Distance \u003E= 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 \u003E 1)\r\n\t\t{\r\n\t\t\tint mid = (low \u002B high) / 2;\r\n\r\n\t\t\tif (m_Distances[mid] \u003C= _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 \u003E 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;\r\n\r\n\t\treturn (low \u002B 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Generates the surface of a junction: the layout\u0027s 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\u0027 heights do.\r\n/// \u003C/summary\u003E\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 \u003C 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 \u003C polygon.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tint next = (i \u002B 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/// \u003Csummary\u003E\r\n\t/// Junctions have no single direction to run a texture along, so they use a top-down projection.\r\n\t/// \u003C/summary\u003E\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( \u0022AddonTitle\u0022, \u0022Road Tool\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022roadtool2\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022redsnail\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022redsnail.roadtool2\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002230\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-10-18T00:54:30.0847204Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.166.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.166.0\u0022)]"},{"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/// \u003Csummary\u003E\r\n/// Arc-length table over a \u003Csee cref=\u0022RoadCurve\u0022/\u003E, 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/// \u003C/summary\u003E\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 =\u003E m_Distances[^1];\r\n\r\n\tpublic int SampleCount =\u003E m_Positions.Length;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The box the curve itself runs through. Picking widens it by the road\u0027s own reach and skips the road\r\n\t/// altogether when the cursor\u0027s ray misses it, which is most of them in a city.\r\n\t/// \u003C/summary\u003E\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 \u002B 1];\r\n\t\tm_Distances = new float[count \u002B 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 \u003C= count; i\u002B\u002B)\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] \u002B 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/// \u003Csummary\u003E\r\n\t/// Builds a table with a sample density that suits the curve\u0027s length.\r\n\t/// \u003C/summary\u003E\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 \u003C= 0.0f)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tif (_Distance \u003E= 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 \u003E 1)\r\n\t\t{\r\n\t\t\tint mid = (low \u002B high) / 2;\r\n\r\n\t\t\tif (m_Distances[mid] \u003C= _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 \u003E 0.0f ? (_Distance - m_Distances[low]) / span : 0.0f;\r\n\r\n\t\treturn (low \u002B 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// How far a road\u0027s two edges sit from its centreline along a segment: half the road\u0027s width on each side, plus\r\n/// whatever extra each side has at the segment\u0027s 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/// \u003C/summary\u003E\r\npublic readonly struct RoadWidth\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// No road is built narrower than this: two edges on one point make faces with no area.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// A plain road of one width all along.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tpublic bool IsUniform =\u003E LeftStart == 0.0f \u0026\u0026 LeftEnd == 0.0f \u0026\u0026 RightStart == 0.0f \u0026\u0026 RightEnd == 0.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The furthest either edge gets from the centreline anywhere along the segment.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float MaxHalf =\u003E MathF.Max(MathF.Max(Base * 0.5f \u002B LeftStart, Base * 0.5f \u002B LeftEnd), MathF.Max(Base * 0.5f \u002B RightStart, Base * 0.5f \u002B RightEnd));\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Distance of the left edge from the centreline at a curve distance, looking along the segment.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float LeftAt(float _Distance)\r\n\t{\r\n\t\tfloat left = Base * 0.5f \u002B Blend(LeftStart, LeftEnd, _Distance);\r\n\t\tfloat right = Base * 0.5f \u002B Blend(RightStart, RightEnd, _Distance);\r\n\r\n\t\treturn left \u002B right \u003C MinWidth ? left \u002B (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 \u002B Blend(LeftStart, LeftEnd, _Distance);\r\n\t\tfloat right = Base * 0.5f \u002B Blend(RightStart, RightEnd, _Distance);\r\n\r\n\t\treturn left \u002B right \u003C MinWidth ? right \u002B (MinWidth - left - right) * 0.5f : right;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The road\u0027s full width at a curve distance.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float WidthAt(float _Distance)\r\n\t{\r\n\t\treturn LeftAt(_Distance) \u002B RightAt(_Distance);\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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 \u002B (_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/// \u003Csummary\u003E\r\n/// 64-bit FNV-1a hash that is stable across editor sessions, unlike \u003Csee cref=\u0022HashCode\u0022/\u003E 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/// \u003C/summary\u003E\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 =\u003E 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 \u003C 4; i\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tm_Hash ^= (byte)(_Value \u003E\u003E (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 \u003E\u003E 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 \u0022ab\u0022 \u002B \u0022c\u0022 never collides with \u0022a\u0022 \u002B \u0022bc\u0022.\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/// \u003Csummary\u003E\r\n\t/// The road surface itself.\r\n\t/// \u003C/summary\u003E\r\n\tSurface,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// What runs along the road\u0027s sides: sidewalks, verges.\r\n\t/// \u003C/summary\u003E\r\n\tBorder,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The painted lines on the surface: lane lines, centre line, edge lines.\r\n\t/// \u003C/summary\u003E\r\n\tMarking,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The bridge under a road: deck, sides, supports.\r\n\t/// \u003C/summary\u003E\r\n\tBridge,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The prefabs standing along a road and before its junctions.\r\n\t/// \u003C/summary\u003E\r\n\tProps,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The rounded end of a road that stops: surface and border together, in one piece per end.\r\n\t/// \u003C/summary\u003E\r\n\tCap\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Marks a generated mesh child of a \u003Csee cref=\u0022RoadNetwork\u0022/\u003E 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/// \u003C/summary\u003E\r\n[Icon(\u0022view_week\u0022)]\r\npublic sealed class RoadChunk : Component\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The segment this chunk is a stretch of, or -1 for a junction piece.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, ReadOnly] public int SegmentId { get; set; } = -1;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The node this chunk is the junction of, or -1 for a road stretch.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, ReadOnly] public int NodeId { get; set; } = -1;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The parking lot this chunk is a piece of, or -1 for anything else.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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\u003CRoadNetwork\u003E()?.MarkDirty();\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Property, Hide] public long Signature { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The rows a border piece was built from, when the network\u0027s debug overlay is on. Not saved.\r\n\t/// \u003C/summary\u003E\r\n\tpublic BorderDebug Debug { get; set; }\r\n\r\n\tpublic bool IsJunction =\u003E NodeId \u003E= 0;\r\n\r\n\tpublic bool IsLot =\u003E LotId \u003E= 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\u003CRoadNetwork\u003E();\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tprivate const int GeneratorVersion = 57;\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tprivate const float MinChunkLength = 1.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Where a segment\u0027s painted lines may run: between \u003Csee cref=\u0022Start\u0022/\u003E and \u003Csee cref=\u0022End\u0022/\u003E, skipping the\r\n\t/// gap between \u003Csee cref=\u0022GapStart\u0022/\u003E and \u003Csee cref=\u0022GapEnd\u0022/\u003E when there is one (a crossing in the middle of\r\n\t/// a short road between two junctions).\r\n\t/// \u003C/summary\u003E\r\n\tprivate readonly record struct LineRange(float Start, float End, float GapStart, float GapEnd)\r\n\t{\r\n\t\tpublic bool HasGap =\u003E GapEnd \u003E 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/// \u003Csummary\u003E\r\n\t/// A segment\u0027s 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\u0027s start.\r\n\t/// \u003C/summary\u003E\r\n\tprivate readonly record struct SegmentCrossings(bool AtStart, float StartCentre, bool AtEnd, float EndCentre);\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One piece of a segment to build. \u003Csee cref=\u0022Lines\u0022/\u003E bounds the painted lines; \u003Csee cref=\u0022Taper\u0022/\u003E is the\r\n\t/// border\u0027s run-out into a different road type at either end; \u003Csee cref=\u0022Bridge\u0022/\u003E is the whole built range\r\n\t/// and the ends of a bridge piece; the other kinds ignore them.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// One piece to (re)build: a chunk of a segment, or, when \u003Csee cref=\u0022NodeId\u0022/\u003E is set, a piece of a junction.\r\n\t/// \u003Csee cref=\u0022ArmBlends\u0022/\u003E and \u003Csee cref=\u0022ArmTapers\u0022/\u003E carry, for a junction\u0027s border piece, what each arm\u0027s\r\n\t/// chunks were built with, keyed by segment.\r\n\t/// \u003C/summary\u003E\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\u003Cint, RoadBorderBuilder.SegmentBorderBlends\u003E ArmBlends, Dictionary\u003Cint, SegmentBorderTapers\u003E ArmTapers, RoadLot Lot);\r\n\r\n\tprivate bool m_IsDirty;\r\n\tprivate readonly Queue\u003CRebuildJob\u003E m_Jobs = new();\r\n\tprivate readonly Dictionary\u003Cint, PendingGroup\u003E m_PendingSegments = new();\r\n\tprivate readonly Dictionary\u003C(int NodeId, RoadChunkKind Kind), PendingGroup\u003E m_PendingJunctions = new();\r\n\r\n\t/// \u003Csummary\u003E\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\u0027 change.\r\n\t/// \u003C/summary\u003E\r\n\tprivate readonly Dictionary\u003Cint, (long Key, RoadJunctionLayout Layout)\u003E m_JunctionCache = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True while pieces are still queued for (re)generation.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool IsRebuilding =\u003E m_Jobs.Count \u003E 0;\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// Asks the network in the editor\u0027s 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/// \u003C/summary\u003E\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\u003CRoadNetwork\u003E();\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/// \u003Csummary\u003E\r\n\t/// One reconcile pass, then as much of the queued work as this frame\u0027s budget allows. Driven from the\r\n\t/// component\u0027s own update in RoadNetwork.cs.\r\n\t/// \u003C/summary\u003E\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\u003CRoadChunk\u003E 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\u003CRoadChunk\u003E();\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 =\u003E !chunk.IsJunction \u0026\u0026 chunk.SegmentId == _SegmentId \u0026\u0026 chunk.ChunkIndex == _ChunkIndex \u0026\u0026 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 =\u003E chunk.IsJunction \u0026\u0026 chunk.NodeId == _NodeId \u0026\u0026 chunk.Kind == _Kind);\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tprivate void Reconcile()\r\n\t{\r\n\t\tPruneCurveTables();\r\n\r\n\t\t// A lot\u0027s 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\u003Cint, List\u003CRoadChunk\u003E\u003E();\r\n\t\tvar junctionChunks = new Dictionary\u003C(int NodeId, RoadChunkKind Kind), RoadChunk\u003E();\r\n\t\tvar lotChunks = new Dictionary\u003C(int LotId, RoadChunkKind Kind), RoadChunk\u003E();\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\u003CRoadChunk\u003E();\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\u003Cint\u003E();\r\n\t\tvar segmentBlends = new Dictionary\u003Cint, RoadBorderBuilder.SegmentBorderBlends\u003E();\r\n\t\tvar segmentTapers = new Dictionary\u003Cint, SegmentBorderTapers\u003E();\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\u0027s 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) \u0026\u0026 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 =\u003E 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\u0027s border runs continue the arms\u0027 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\u003Cint, RoadBorderBuilder.SegmentBorderBlends\u003E();\r\n\t\t\t\tvar armTapers = new Dictionary\u003Cint, SegmentBorderTapers\u003E();\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\u003C(int NodeId, RoadChunkKind Kind), RoadChunk\u003E _Existing, Dictionary\u003Cint, RoadBorderBuilder.SegmentBorderBlends\u003E _ArmBlends, Dictionary\u003Cint, SegmentBorderTapers\u003E _ArmTapers = null)\r\n\t{\r\n\t\tvar key = (_NodeId, _Kind);\r\n\r\n\t\tif (_Existing.TryGetValue(key, out var existing) \u0026\u0026 (existing.Locked || (existing.Signature == _Signature \u0026\u0026 !LacksDebugRecord(existing))))\r\n\t\t\treturn;\r\n\r\n\t\tif (m_PendingJunctions.TryGetValue(key, out var pending) \u0026\u0026 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/// \u003Csummary\u003E\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\u0027s direction, so every segment of the run agrees\r\n\t/// on where the run\u0027s distance starts and the dashes carry across the nodes.\r\n\t/// \u003C/summary\u003E\r\n\tprivate Dictionary\u003Cint, RoadMarkingBuilder.DashPhase\u003E ComputeDashPhases()\r\n\t{\r\n\t\tvar phases = new Dictionary\u003Cint, RoadMarkingBuilder.DashPhase\u003E();\r\n\r\n\t\tforeach (var seed in Segments.OrderBy(segment =\u003E 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\u003Cint\u003E { seed.Id };\r\n\t\t\tvar run = new List\u003C(RoadSegment Segment, bool Forward)\u003E();\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 \u0026\u0026 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 \u0026\u0026 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 \u002B= 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/// \u003Csummary\u003E\r\n\t/// The road a segment carries straight on into at a pass-through node, or null at anything else.\r\n\t/// \u003C/summary\u003E\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 =\u003E candidate.Id != _Segment.Id);\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How a segment\u0027s border runs out at each end where it meets another road type\u0027s: over the profile\u0027s 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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// One end of a border\u0027s taper: how long it is, the width and share of relief it runs to, whether a country road\u0027s\r\n\t/// gravel spills over it, and how far from the node it ends.\r\n\t/// \u003C/summary\u003E\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\u003Cint, RoadJunctionLayout\u003E _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 \u003E 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/// \u003Csummary\u003E\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\u0027s 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/// \u003C/summary\u003E\r\n\tprivate void SideTapers(RoadSegment _Segment, RoadProfile _Profile, int _NodeId, bool _AtStart, float _Limit, Dictionary\u003Cint, RoadJunctionLayout\u003E _Layouts, ref RoadBorderTaper _Left, ref RoadBorderTaper _Right)\r\n\t{\r\n\t\tint degree = GetDegree(_NodeId);\r\n\r\n\t\tif (degree \u003C 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 \u0026\u0026 _Layouts.TryGetValue(_NodeId, out var layout) \u0026\u0026 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 =\u003E 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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 \u003E 2 \u0026\u0026 !sameKind)\r\n\t\t\treturn false;\r\n\r\n\t\tif (sameKind \u0026\u0026 MathF.Abs(other.FlatBorderWidth - _Profile.FlatBorderWidth) \u003C 0.01f \u0026\u0026 MathF.Abs(other.ReliefHeight - _Profile.ReliefHeight) \u003C 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 \u003C= 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 \u0026\u0026 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/// \u003Csummary\u003E\r\n\t/// How far from a node the gap between this segment\u0027s 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\u0027 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/// \u003C/summary\u003E\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) \u003C 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\u0027s 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\u0027s 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) \u003C 1.0f)\r\n\t\t\t\totherLeft = true;\r\n\t\t\telse if (OtherEdge(0.0f, false).Distance(start) \u003C 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) \u003C 0.7f)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat needed = own \u002B 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 \u003C= reach; fromNode \u002B= step)\r\n\t\t\t{\r\n\t\t\t\tif (OwnEdge(fromNode).Distance(OtherEdge(fromNode, otherLeft)) \u003E= 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/// \u003Csummary\u003E\r\n\t/// Whether two road types\u0027 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/// \u003C/summary\u003E\r\n\tprivate static bool MeetsFlush(RoadProfile _A, RoadProfile _B)\r\n\t{\r\n\t\treturn _A.HasBorder \u0026\u0026 _A.Border == _B.Border \u0026\u0026 _A.TransitionLength \u003E 0.0f \u0026\u0026 _B.TransitionLength \u003E 0.0f;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\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\u0027s top sits (nothing when no arm has a sidewalk). For\r\n\t/// tools that need to know where road is.\r\n\t/// \u003C/summary\u003E\r\n\tpublic IEnumerable\u003C(Vector3 Centre, IReadOnlyList\u003CVector3\u003E Polygon, float PavementReach, float PavementTop)\u003E 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 \u003E= 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 \u003C 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tprivate Dictionary\u003Cint, RoadJunctionLayout\u003E ComputeJunctionLayouts()\r\n\t{\r\n\t\tvar radii = Nodes.ToDictionary(node =\u003E node.Id, node =\u003E MathF.Max(0.0f, node.CornerRadius));\r\n\t\tvar layouts = BuildJunctionLayouts(radii);\r\n\t\tvar shrink = new Dictionary\u003Cint, float\u003E();\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) \u002B (endArm?.SharpTrim ?? 0.0f);\r\n\t\t\tfloat fillet = (startArm is null ? 0.0f : startArm.RawTrim - startArm.SharpTrim) \u002B (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 \u003C= 0.0f || sharp \u002B fillet \u003C= available)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// The fillet\u0027s 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 \u003E 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) \u002B (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 \u003C= available || total \u003C= 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/// \u003Csummary\u003E\r\n\t/// The corner fold on one side of a segment at one of its nodes, and whether the segment is that fold\u0027s 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\u0027s left; one ending there has it on the arm\u0027s right.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static RoadJunctionLayout.CornerFold FindFold(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary\u003Cint, RoadJunctionLayout\u003E _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 =\u003E candidate.ArmA.Segment.Id == _Segment.Id)\r\n\t\t\t: layout.CornerPaths.FirstOrDefault(candidate =\u003E 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/// \u003Csummary\u003E\r\n\t/// The junction layout of every node for the given fillet radii, from the cache where the inputs match.\r\n\t/// \u003C/summary\u003E\r\n\tprivate Dictionary\u003Cint, RoadJunctionLayout\u003E BuildJunctionLayouts(Dictionary\u003Cint, float\u003E _Radii)\r\n\t{\r\n\t\tvar layouts = new Dictionary\u003Cint, RoadJunctionLayout\u003E();\r\n\t\tvar liveNodes = new HashSet\u003Cint\u003E();\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\u0027s objects.\r\n\t\t\tif (m_JunctionCache.TryGetValue(node.Id, out var cached) \u0026\u0026 cached.Key == key \u0026\u0026 (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 =\u003E !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/// \u003Csummary\u003E\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\u0027s 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/// \u003C/summary\u003E\r\n\tprivate bool WantsCrossing(RoadSegment _Segment, int _NodeId, Dictionary\u003Cint, RoadJunctionLayout\u003E _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) \u0026\u0026 layout.Arms.Count \u003E= 3 \u0026\u0026 _Trim \u003E 0.01f;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Places a road\u0027s 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/// \u003C/summary\u003E\r\n\tprivate SegmentCrossings ComputeCrossings(RoadSegment _Segment, RoadCurveTable _Table, Dictionary\u003Cint, RoadJunctionLayout\u003E _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 \u003C depth || (!wantStart \u0026\u0026 !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\u0027s own mouth, so that kind is pulled in far enough to put its outer line on the pavement\u0027s\r\n\t\t// corner instead of a crossing\u0027s 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 \u002B 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 \u002B depth * 0.5f)\r\n\t\t\t: _Table.Length - _TrimEnd - depth * 0.5f;\r\n\r\n\t\tif (wantStart \u0026\u0026 wantEnd)\r\n\t\t{\r\n\t\t\tif (clear \u003E= 2.0f * depth \u002B 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 \u002B 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/// \u003Csummary\u003E\r\n\t/// Where the painted lines run: the stretch between the junctions, kept clear of the crossings.\r\n\t/// \u003C/summary\u003E\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 \u002B _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 \u002B 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 \u003C= start \u002B 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/// \u003Csummary\u003E\r\n\t/// Which ends of a segment\u0027s 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/// \u003C/summary\u003E\r\n\tprivate BorderCaps ComputeBorderCaps(RoadSegment _Segment, Dictionary\u003Cint, RoadJunctionLayout\u003E _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/// \u003Csummary\u003E\r\n\t/// A bridge segment\u0027s 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\u0027s railing post: the one leaving the node, or the lower id when both leave\r\n\t/// or both arrive.\r\n\t/// \u003C/summary\u003E\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 \u003C startNeighbour.Id;\r\n\t\tbool postEnd = endNeighbour is null || (endNeighbour.EndNodeId == _Segment.EndNodeId \u0026\u0026 _Segment.Id \u003C 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/// \u003Csummary\u003E\r\n\t/// The bridge segment the deck carries straight on to at a node, or null where the deck ends.\r\n\t/// \u003C/summary\u003E\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 =\u003E 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)) \u003C 0.01f;\r\n\r\n\t\treturn sameExtent ? other : null;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The lift lines of a segment\u0027s two borders: at each end, the corner\u0027s height difference on that side (or\r\n\t/// nothing), run linearly along the road between the two.\r\n\t/// \u003C/summary\u003E\r\n\tprivate RoadBorderBuilder.SegmentBorderBlends ComputeBorderBlends(RoadSegment _Segment, RoadCurveTable _Table, Dictionary\u003Cint, RoadJunctionLayout\u003E _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\u003Cint, RoadJunctionLayout\u003E _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 \u0026\u0026 !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\u0027s 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\u0027s 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/// \u003Csummary\u003E\r\n\t/// Where along the segment the corner\u0027s 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/// \u003C/summary\u003E\r\n\tprivate static bool CornerLift(RoadSegment _Segment, int _NodeId, bool _AtStart, bool _Left, Dictionary\u003Cint, RoadJunctionLayout\u003E _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\u003Cint, RoadJunctionLayout\u003E _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\u0027s 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 \u0026\u0026 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 \u003C= 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 \u0026\u0026 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 =\u003E path.ArmA.Segment.Id == _Segment.Id);\r\n\t\t\tbool armRightContinued = layout.CornerPaths.Any(path =\u003E 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 =\u003E candidate.Id != _Segment.Id);\r\n\t\t\tbool continuous = other is not null \u0026\u0026 SameEdgesAt(_Segment, other, _NodeId)\r\n\t\t\t\t\u0026\u0026 (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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tprivate List\u003CChunkPlan\u003E 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\u003C(int Index, float Start, float End)\u003E(count);\r\n\r\n\t\tfor (int i = 0; i \u003C count; i\u002B\u002B)\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 \u002B 1) / count, usableEnd);\r\n\r\n\t\t\tif (end - start \u003E= MinChunkLength)\r\n\t\t\t\tranges.Add((i, start, end));\r\n\t\t}\r\n\r\n\t\tvar plans = new List\u003CChunkPlan\u003E(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) \u003E= 3, GetDegree(_Segment.EndNodeId) \u003E= 3);\r\n\t\tlong propSignature = 0;\r\n\r\n\t\tif (props is not null \u0026\u0026 !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 \u003C ranges.Count; r\u002B\u002B)\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\u0027s\r\n\t\t\t\t// own stretch of road.\r\n\t\t\t\tvar owned = new SegmentCrossings(_Crossings.AtStart \u0026\u0026 r == 0, _Crossings.StartCentre,\r\n\t\t\t\t\t_Crossings.AtEnd \u0026\u0026 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 \u0026 (BorderCaps.StartLeft | BorderCaps.StartRight);\r\n\r\n\t\t\tif (r == ranges.Count - 1)\r\n\t\t\t\tcaps |= _Caps \u0026 (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/// \u003Csummary\u003E\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\u0027s own kerb closes that), and on a road no bridge carries (a deck ends where it ends).\r\n\t/// The plan\u0027s start is the end\u0027s own distance along the curve and its end the depth the cap reaches past it.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void AddCapPlan(List\u003CChunkPlan\u003E _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 \u003C= 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\u003CChunkPlan\u003E _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\u003CRoadChunk\u003E _Existing, List\u003CChunkPlan\u003E _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 =\u003E candidate.ChunkIndex == plan.Index \u0026\u0026 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 \u0026\u0026 (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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tprivate bool LacksDebugRecord(RoadChunk _Chunk)\r\n\t{\r\n\t\treturn DebugBorders \u0026\u0026 _Chunk.Kind == RoadChunkKind.Border \u0026\u0026 (_Chunk.Debug is null || _Chunk.Debug.Text.Length == 0);\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Throws away every unlocked piece\u0027s 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/// \u003C/summary\u003E\r\n\t[Button(\u0022Regenerate All\u0022, \u0022refresh\u0022)]\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/// \u003Csummary\u003E\r\n\t/// Everything a node\u0027s junction layout depends on: the node itself and the curve and width of each road at it.\r\n\t/// \u003C/summary\u003E\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 =\u003E 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\u003Cint, RoadBorderBuilder.SegmentBorderBlends\u003E _ArmBlends, Dictionary\u003Cint, SegmentBorderTapers\u003E _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/// \u003Csummary\u003E\r\n\t/// A junction\u0027s surface takes after its main road: the widest arm.\r\n\t/// \u003C/summary\u003E\r\n\tprivate RoadProfile JunctionSurfaceProfile(RoadJunctionLayout _Layout)\r\n\t{\r\n\t\tvar main = _Layout.Arms.OrderByDescending(arm =\u003E arm.Widths.Base).ThenBy(arm =\u003E 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\u003CChunkPlan\u003E _Plans, long _GroupSignature, List\u003CRoadChunk\u003E _Existing)\r\n\t{\r\n\t\t// Chunks with no plan (swallowed by a junction, past a shorter road\u0027s 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 \u0026\u0026 _Plans.All(plan =\u003E 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 \u003E 0 \u0026\u0026 m_Jobs.Count \u003E 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 \u003E= 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 \u003C= 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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 \u0026\u0026 (existing.Locked || (existing.Signature == plan.Signature \u0026\u0026 !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\u003C(GameObject Prefab, Transform Transform)\u003E();\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 = $\u0022Road {_Job.SegmentId} border [{plan.Index \u002B 1}/{plan.Count}]\u0022;\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\u003CRoadBridgeBuilder.Placement\u003E();\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 = $\u0022Road {_Job.SegmentId} bridge [{plan.Index \u002B 1}/{plan.Count}]\u0022;\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\u003CRoadPropBuilder.Placement\u003E();\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 = $\u0022Road {_Job.SegmentId} props [{plan.Index \u002B 1}/{plan.Count}]\u0022;\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 = $\u0022Road {_Job.SegmentId} {(plan.Index == 0 ? \u0022start\u0022 : \u0022end\u0022)} cap\u0022;\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 = $\u0022Road {_Job.SegmentId} markings [{plan.Index \u002B 1}/{plan.Count}]\u0022;\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 = $\u0022Road {_Job.SegmentId} [{plan.Index \u002B 1}/{plan.Count}]\u0022;\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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) \u002B Vector3.Up * _StartAbove;\r\n\t\tvar trace = Scene.Trace.Ray(from, from \u002B 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 \u0026\u0026 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 =\u003E armBlends is not null ? armBlends.GetValueOrDefault(segment.Id) : default,\r\n\t\t\t\tsegment =\u003E armTapers is not null ? armTapers.GetValueOrDefault(segment.Id) : default,\r\n\t\t\t\tdebug);\r\n\t\t\tname = $\u0022Junction {_Job.NodeId} border\u0022;\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 = $\u0022Junction {_Job.NodeId}\u0022;\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\u003CRoadChunk\u003E();\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\u002B\u002B;\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\u003CMeshComponent\u003E();\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/// \u003Csummary\u003E\r\n/// Gizmo drawing shared by the road tools. Curves are in network-local space and drawn through the network\u0027s\r\n/// transform, in whatever colour and thickness the caller set up.\r\n/// \u003C/summary\u003E\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 \u003C= _Steps; i\u002B\u002B)\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/// \u003Csummary\u003E\r\n\t/// Draws the two edges of a road of the given width along the curve, capped at both ends.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// Draws the two edges of a road along the curve, each its own distance out (the curve\u0027s parameter standing in\r\n\t/// for its distance), capped at both ends.\r\n\t/// \u003C/summary\u003E\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 \u003C= _Steps; i\u002B\u002B)\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 \u002B 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 \u003E 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/// \u003Csummary\u003E\r\n\t/// A node marker: a flat ring on the ground, filled when it is the one being worked on.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\npublic readonly struct RoadCurve : IEquatable\u003CRoadCurve\u003E\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) \u002B P1 * (3.0f * uu * _T) \u002B P2 * (3.0f * u * tt) \u002B P3 * (tt * _T);\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Unit direction of travel at \u003Cparamref name=\u0022_T\u0022/\u003E. 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/// \u003C/summary\u003E\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) \u002B (P2 - P1) * (6.0f * u * _T) \u002B (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/// \u003Csummary\u003E\r\n\t/// Chord-summed length, good enough to size sampling tables and count chunks.\r\n\t/// \u003C/summary\u003E\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 \u003C= _Steps; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tVector3 point = Evaluate((float)i / _Steps);\r\n\t\t\tlength \u002B= 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// The road\u0027s right-hand direction for a direction of travel: flat, perpendicular to the tangent, and\r\n\t/// consistent with \u003Csee cref=\u0022Rotation.Right\u0022/\u003E for a rotation looking down the tangent with world up.\r\n\t/// \u003C/summary\u003E\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 \u0026\u0026 P1 == _Other.P1 \u0026\u0026 P2 == _Other.P2 \u0026\u0026 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 \u0026\u0026 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/// \u003Csummary\u003E\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\u0027s own end, on the end row itself, so its seam with the last chunk is exact and nothing\r\n/// about the road\u0027s length changes.\r\n/// \u003C/summary\u003E\r\ninternal static class RoadCapBuilder\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The arc is drawn in steps of about this many degrees.\r\n\t/// \u003C/summary\u003E\r\n\tprivate const float ArcStepDegrees = 9.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How deep a road\u0027s rounded end reaches past it, as a share of the road\u0027s half width. 1 is a half circle.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const float DepthFactor = 1.0f;\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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) \u002B _Width.RightAt(_Distance)) * 0.5f * DepthFactor;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The rounded end at one end of a road. \u003Cparamref name=\u0022_Distance\u0022/\u003E is the end\u0027s own distance along the curve\r\n\t/// (0 or the table\u0027s length) and \u003Cparamref name=\u0022_AtStart\u0022/\u003E which end it is; the cap reaches out from there.\r\n\t/// \u003C/summary\u003E\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 \u003C= 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 \u002B= Vector3.Up * _Lift;\r\n\r\n\t\t// Out of the road, and the road\u0027s own right at that end. The arc keeps the road\u0027s grade because the\r\n\t\t// direction it reaches along carries the curve\u0027s 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 \u002B rightHalf \u003C= 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\u003CVector3\u003E(steps \u002B 1);\r\n\t\tvar outwards = new List\u003CVector3\u003E(steps \u002B 1);\r\n\t\tvar offsets = new List\u003Cfloat\u003E(steps \u002B 1);\r\n\t\tvar aheads = new List\u003Cfloat\u003E(steps \u002B 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/// \u003Csummary\u003E\r\n\t/// The outline of a road\u0027s 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\u0027s).\r\n\t/// Empty where the road does not round that end.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static List\u003CVector3\u003E 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\u003CVector3\u003E();\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 \u002B= 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 \u003C= 0.01f || leftHalf \u002B rightHalf \u003C= 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/// \u003Csummary\u003E\r\n\t/// The half ellipse from the road\u0027s 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/// \u003C/summary\u003E\r\n\tprivate static void Arc(Vector3 _Centre, Vector3 _Forward, Vector3 _Right, float _LeftHalf, float _RightHalf, float _Depth, int _Steps,\r\n\t\tList\u003CVector3\u003E _Points, List\u003CVector3\u003E _Outwards, List\u003Cfloat\u003E _Offsets, List\u003Cfloat\u003E _Aheads)\r\n\t{\r\n\t\tfor (int i = 0; i \u003C= _Steps; i\u002B\u002B)\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 \u003E= 0.0f ? _RightHalf : _LeftHalf);\r\n\t\t\tfloat ahead = _Depth * cos;\r\n\r\n\t\t\t_Points.Add(_Centre \u002B _Right * offset \u002B _Forward * ahead);\r\n\t\t\t_Outwards?.Add((_Right * sin \u002B _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/// \u003Csummary\u003E\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\u0027s own base.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static void AddSurface(PolygonMesh _Mesh, Vector3 _Centre, List\u003CVector3\u003E _Outline, List\u003Cfloat\u003E _Offsets, List\u003Cfloat\u003E _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 \u003C _Outline.Count; i\u002B\u002B)\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\u0027s 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 \u003C 0.0f;\r\n\r\n\t\tfor (int i = 0; i \u003C _Outline.Count - 1; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar uvA = new Vector2(_Offsets[i] / repeat, (_Distance \u002B (_AtStart ? -_Aheads[i] : _Aheads[i])) / repeat);\r\n\t\t\tvar uvB = new Vector2(_Offsets[i \u002B 1] / repeat, (_Distance \u002B (_AtStart ? -_Aheads[i \u002B 1] : _Aheads[i \u002B 1])) / repeat);\r\n\r\n\t\t\tif (flip)\r\n\t\t\t\tMeshUtility.AddTexturedTriangle(_Mesh, material, centre, ring[i \u002B 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 \u002B 1], centreUv, uvA, uvB);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The border round the arc, open at both ends: what closes it off is the road\u0027s own border, which runs into\r\n\t/// it at the end row.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static void AddBorder(PolygonMesh _Mesh, List\u003CVector3\u003E _Outline, List\u003CVector3\u003E _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\u003CRoadBorderBuilder.PathPoint\u003E(_Outline.Count);\r\n\t\tfloat along = 0.0f;\r\n\r\n\t\tfor (int i = 0; i \u003C _Outline.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tif (i \u003E 0)\r\n\t\t\t\talong \u002B= _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/// \u003Csummary\u003E\r\n/// Generates the surface of a junction: the layout\u0027s 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\u0027 heights do.\r\n/// \u003C/summary\u003E\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 \u003C 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 \u003C polygon.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tint next = (i \u002B 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/// \u003Csummary\u003E\r\n\t/// Junctions have no single direction to run a texture along, so they use a top-down projection.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n[AssetType(Name = \u0022Parking Style\u0022, Extension = \u0022park\u0022, Category = \u0022Road Tool 2\u0022)]\r\npublic sealed class ParkingStyle : GameResource\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// How wide one bay is, measured along the edge it stands on.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Bays\u0022), Range(80.0f, 400.0f)] public float BayWidth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 150.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How far a bay reaches in from the edge.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Bays\u0022), Range(100.0f, 800.0f)] public float BayDepth { get; set { field = value.Clamp(10.0f, 10000.0f); } } = 260.0f;\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Group(\u0022Bays\u0022), Range(0.0f, 60.0f)] public float BayAngle { get; set { field = value.Clamp(0.0f, 80.0f); } } = 0.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Bays round the inside of the lot\u0027s edges.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Bays\u0022)] public bool EdgeBays { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Group(\u0022Rows\u0022)] public bool CentreRows { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How much room is left for driving between one row of bays and the next.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Rows\u0022), Range(300.0f, 1500.0f)] public float AisleWidth { get; set { field = value.Clamp(50.0f, 100000.0f); } } = 600.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How the bay lines are painted. Empty paints them like a road\u0027s lane lines.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Markings\u0022)] public RoadLineStyle Line { get; set; } = new() { Width = 8.0f };\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A line along the back of each row, closing its bays off.\r\n\t/// \u003C/summary\u003E\r\n\t[Group(\u0022Markings\u0022)] public bool BackLine { get; set; } = false;\r\n\r\n\t[Hide, JsonIgnore] public Material ResolvedLineMaterial =\u003E Line?.Material ?? Material.Load(\u0022roadtool2/materials/linewhite.vmat\u0022);\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Hide, JsonIgnore] public float Pitch =\u003E 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/// \u003Csummary\u003E\r\n\t/// The asset was saved: every network with a lot in this style has stale pieces now.\r\n\t/// \u003C/summary\u003E\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(\u0022local_parking\u0022, _Width, _Height, \u0022#8fd3ff\u0022, \u0022black\u0022);\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/// \u003Csummary\u003E\r\n/// What a border piece was built from, kept on the piece while the network\u0027s 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/// \u003C/summary\u003E\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\u003CDebugLine\u003E Lines { get; } = new();\r\n\r\n\tpublic List\u003CDebugMarker\u003E Markers { get; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The record, one entry per line.\r\n\t/// \u003C/summary\u003E\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 $\u0022({_Vector.x:0.###} {_Vector.y:0.###} {_Vector.z:0.###})\u0022;\r\n\t}\r\n\r\n\r\n\r\n\tpublic static string Format(Vector2 _Vector)\r\n\t{\r\n\t\treturn $\u0022({_Vector.x:0.###} {_Vector.y:0.###})\u0022;\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/// \u003Csummary\u003E\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 \u003Csee cref=\u0022DebugBorders\u0022/\u003E was on carry a record.\r\n\t/// \u003C/summary\u003E\r\n\tpublic string BuildDebugRecord()\r\n\t{\r\n\t\tvar text = new StringBuilder();\r\n\r\n\t\ttext.AppendLine($\u0022ROADTOOL2 BORDER DEBUG generator={GeneratorVersion} revision={DebugRevision} time={DateTime.Now:HH:mm:ss}\u0022);\r\n\t\ttext.AppendLine($\u0022NETWORK name=\\\u0022{GameObject.Name}\\\u0022 surfaceOffset={SurfaceOffset} spacing={SampleSpacing} chunkLength={ChunkLength} defaultProfile={DefaultProfile?.ResourceName ?? \u0022(none)\u0022}\u0022);\r\n\r\n\t\tforeach (var node in Nodes)\r\n\t\t\ttext.AppendLine($\u0022NODE id={node.Id} position={BorderDebug.Format(node.Position)} cornerRadius={node.CornerRadius} autoTangents={(node.AutoTangents ? 1 : 0)} degree={GetDegree(node.Id)}\u0022);\r\n\r\n\t\tforeach (var segment in Segments)\r\n\t\t{\r\n\t\t\ttext.AppendLine($\u0022SEGMENT 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}\u0022);\r\n\r\n\t\t\tif (TryGetCurve(segment, out var curve))\r\n\t\t\t\ttext.AppendLine($\u0022CURVE 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.###}\u0022);\r\n\t\t}\r\n\r\n\t\tforeach (var chunk in GetChunks().OrderBy(chunk =\u003E chunk.IsJunction ? 1 : 0).ThenBy(chunk =\u003E chunk.IsJunction ? chunk.NodeId : chunk.SegmentId).ThenBy(chunk =\u003E 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($\u0022PIECE name=\\\u0022{chunk.GameObject.Name}\\\u0022 segment={chunk.SegmentId} node={chunk.NodeId} index={chunk.ChunkIndex} count={chunk.ChunkCount} record={(chunk.Debug is not null ? 1 : 0)}\u0022);\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"}]}