{"TotalCount":111,"Files":[{"Ident":"redsnail.roadtool","Path":"Editor/IntersectionTool.cs","FileName":"IntersectionTool.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":342326,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Editor;\r\n\r\nnamespace RedSnail.RoadTool.Editor;\r\n\r\n/// \u003Csummary\u003E\r\n/// Create and manage road and road intersection.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Create Road/Intersection\u0022)]\r\n[Icon(\u0022roundabout_left\u0022)]\r\n[Alias(\u0022intersection\u0022)]\r\n[Group(\u00221\u0022)]\r\n[Order(0)]\r\npublic class IntersectionTool : EditorTool\r\n{\r\n\tpublic override void OnEnabled()\r\n\t{\r\n\r\n\t}\r\n\r\n\tpublic override Widget CreateToolSidebar()\r\n\t{\r\n\t\tToolSidebarWidget sidebar = new ToolSidebarWidget();\r\n\t\tsidebar.AddTitle(\u0022Intersection\u0022, \u0022roundabout_left\u0022);\r\n\r\n\t\tLayout group = sidebar.AddGroup(\u0022Create\u0022);\r\n\t\tLayout row = Layout.Row();\r\n\r\n\t\tIconButton road = sidebar.CreateButton(\u0022Create Road\u0022, \u0022route\u0022, null, CreateRoad, true, row);\r\n\t\tIconButton inter = sidebar.CreateButton(\u0022Create Intersection\u0022, \u0022roundabout_left\u0022, null, CreateIntersection, true, row);\r\n\r\n\t\trow.Spacing = 5;\r\n\t\trow.AddStretchCell();\r\n\r\n\t\tgroup.Add(row);\r\n\r\n\t\tsidebar.Layout.Add(group);\r\n\t\tsidebar.Layout.AddStretchCell();\r\n\t\treturn sidebar;\r\n\t}\r\n\r\n\tprivate static void CreateRoad()\r\n\t{\r\n\t\tGameObject go = SceneEditorSession.Active.Scene.CreateObject();\r\n\t\tgo.Name = \u0022Road\u0022;\r\n\t\tgo.AddComponent\u003CRoadComponent\u003E();\r\n\t}\r\n\r\n\tprivate static void CreateIntersection()\r\n\t{\r\n\t\tGameObject go = SceneEditorSession.Active.Scene.CreateObject();\r\n\t\tgo.Name = \u0022Road Intersection\u0022;\r\n\t\tgo.AddComponent\u003CRoadIntersectionComponent\u003E();\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"RoadIntersectionComponent/RoadIntersectionComponent.Sidewalk.cs","FileName":"RoadIntersectionComponent.Sidewalk.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// \u003Csummary\u003E\n/// How much of a junction reaches the pedestrian graph. Geometry is never affected \u2014 the pavement mesh builds\n/// the same either way; this only decides what pedestrians are allowed to route over.\n///\n/// Ordered most-to-least deliberately, so the zero value is the harmless one: a component that somehow arrives\n/// without this field set behaves like every other junction rather than silently vanishing from the graph.\n/// \u003C/summary\u003E\npublic enum SidewalkGraphMode\n{\n\t/// \u003Csummary\u003EPavement round the corners and a crossing at every arm.\u003C/summary\u003E\n\tAll,\n\n\t/// \u003Csummary\u003E\n\t/// Corners only. The pavement stays joined all the way round and connects to every road that meets here,\n\t/// but there\u0027s nothing to step off the kerb onto \u2014 pedestrians walk round the junction instead of over it.\n\t/// \u003C/summary\u003E\n\tNoCrossing,\n\n\t/// \u003Csummary\u003E\n\t/// Nothing at all. The roads that meet here keep their own pavements; they just stop being connected\n\t/// THROUGH this junction, so expect their ends to show as dead ends.\n\t/// \u003C/summary\u003E\n\tNone\n}\n\n\n\npublic partial class RoadIntersectionComponent\n{\n\t/// \u003Csummary\u003E\n\t/// What this junction contributes to the pedestrian graph \u2014 the pedestrian counterpart of\n\t/// \u003Csee cref=\u0022ExcludeTraffic\u0022/\u003E.\n\t///\n\t/// \u003Csee cref=\u0022SidewalkGraphMode.None\u0022/\u003E for somewhere nobody should be walking round at all: a slip road, a\n\t/// service yard, a junction whose pavement exists only because the mesh needs an edge.\n\t/// \u003Csee cref=\u0022SidewalkGraphMode.NoCrossing\u0022/\u003E for somewhere they may walk past but not across \u2014 a forecourt\n\t/// or car park entrance, where the kerb should stay continuous and stepping into the vehicle route is the\n\t/// thing you\u0027re trying to prevent.\n\t/// \u003C/summary\u003E\n\t[Property, Feature(\u0022General\u0022), Category(\u0022Sidewalk\u0022), Order(3)] public SidewalkGraphMode SidewalkGraph { get; set; } = SidewalkGraphMode.All;\n\n\n\n\t/// \u003Csummary\u003E\n\t/// The walking line around this junction\u0027s pavement, in world space \u2014 a closed loop running down the\n\t/// middle of the sidewalk slab.\n\t///\n\t/// Built from the junction\u0027s OWN outline, which is the whole point: a rectangular intersection\u0027s pavement\n\t/// runs along its edges and turns at its corners, and approximating that with an arc around the centre\n\t/// bows the path out into the road at the middle of each side and cuts the corners off. A circle really is\n\t/// an arc, so it gets one.\n\t///\n\t/// The loop runs all the way round, arm mouths included \u2014 whoever consumes it is expected to split it at\n\t/// the kerbs, because where the pavement is interrupted is the same question as where the crossings go.\n\t/// \u003C/summary\u003E\n\tpublic List\u003CVector3\u003E GetSidewalkOutline(float _Spacing)\n\t{\n\t\tvar points = new List\u003CVector3\u003E();\n\n\t\tif (!HasSidewalks)\n\t\t\treturn points;\n\n\t\tfloat spacing = Math.Max(1.0f, _Spacing);\n\n\t\t// Centre of the slab, so it sits where someone would actually walk rather than on either kerb.\n\t\tfloat outset = SidewalkWidth * 0.5f;\n\t\tVector3 lift = Vector3.Up * SidewalkHeight;\n\n\t\tif (Shape == IntersectionShape.Circle)\n\t\t{\n\t\t\tfloat radius = Radius \u002B outset;\n\t\t\tint steps = Math.Max(8, (int)MathF.Ceiling(MathF.Tau * radius / spacing));\n\n\t\t\tfor (int i = 0; i \u003C steps; i\u002B\u002B)\n\t\t\t{\n\t\t\t\tfloat angle = MathF.Tau * i / steps;\n\n\t\t\t\tpoints.Add(new Vector3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0.0f) \u002B lift);\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Written with the same right/forward vectors BuildRectangleRoad uses, rather than as raw x/y\n\t\t\t// components. Width runs along Right and Length along Forward, which in s\u0026box axes is Y and X \u2014\n\t\t\t// spelling that out by hand gets them the wrong way round, and a junction outline rotated 90\u00B0\n\t\t\t// looks almost plausible until nothing connects to it.\n\t\t\tVector3 right = Vector3.Right;\n\t\t\tVector3 forward = Vector3.Forward;\n\n\t\t\tfloat hw = Width * 0.5f \u002B outset;\n\t\t\tfloat hl = Length * 0.5f \u002B outset;\n\n\t\t\t// Round the rectangle in order, so the loop has a consistent winding for anything that walks it.\n\t\t\tVector3[] corners =\n\t\t\t[\n\t\t\t\t-right * hw - forward * hl,\n\t\t\t\t right * hw - forward * hl,\n\t\t\t\t right * hw \u002B forward * hl,\n\t\t\t\t-right * hw \u002B forward * hl,\n\t\t\t];\n\n\t\t\tfor (int i = 0; i \u003C corners.Length; i\u002B\u002B)\n\t\t\t{\n\t\t\t\tVector3 from = corners[i];\n\t\t\t\tVector3 to = corners[(i \u002B 1) % corners.Length];\n\n\t\t\t\tint steps = Math.Max(1, (int)MathF.Ceiling(Vector3.DistanceBetween(from, to) / spacing));\n\n\t\t\t\t// Last point of each edge is skipped \u2014 it\u0027s the first of the next one, and a closed loop\n\t\t\t\t// mustn\u0027t repeat its corners.\n\t\t\t\tfor (int s = 0; s \u003C steps; s\u002B\u002B)\n\t\t\t\t\tpoints.Add(Vector3.Lerp(from, to, (float)s / steps) \u002B lift);\n\t\t\t}\n\t\t}\n\n\t\tfor (int i = 0; i \u003C points.Count; i\u002B\u002B)\n\t\t\tpoints[i] = WorldTransform.PointToWorld(points[i]);\n\n\t\treturn points;\n\t}\n\n\n\n\t/// \u003Csummary\u003EWhether this junction has pavement to walk on at all.\u003C/summary\u003E\n\tpublic bool HasSidewalks =\u003E SidewalkWidth \u003E 0.0f;\n}\n"},{"Ident":"redsnail.roadtool","Path":"RoadIntersectionComponent/RoadIntersectionComponent.Terrain.cs","FileName":"RoadIntersectionComponent.Terrain.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadIntersectionComponent\r\n{\r\n\t[Property, Feature(\u0022Terrain\u0022, Icon = \u0022landscape\u0022, Tint = EditorTint.Green), Hide]\r\n\tprivate Terrain TerrainTarget { get; set; }\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Range(0f, 2000f)]\r\n\tpublic float TerrainFalloffRadius { get; set; } = 500f;\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Range(-10f, 10f)]\r\n\tpublic float TerrainHeightOffset { get; set; } = 0f;\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Range(0f, 100f)]\r\n\tpublic float TerrainRoadInset { get; set; } = 10f;\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Group(\u0022Texture\u0022), Range(100f, 1000f)]\r\n\tpublic float TerrainEdgeRadius { get; set; } = 500f;\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Group(\u0022Texture\u0022)]\r\n\tpublic TerrainTextureLayer TerrainTargetLayer { get; set; } = TerrainTextureLayer.Overlay;\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Group(\u0022Texture\u0022), Range(0f, 1f)]\r\n\tpublic float TerrainTextureNoise { get; set; } = 0.2f;\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Group(\u0022Texture\u0022)]\r\n\tpublic TerrainMaterial[] TerrainEdgeMaterials { get; set; } = Array.Empty\u003CTerrainMaterial\u003E();\r\n\r\n\t[Property, Feature(\u0022Terrain\u0022), Group(\u0022Texture\u0022)]\r\n\tpublic Gradient TerrainEdgeBlendGradient = new Gradient(\r\n\t\tnew Gradient.ColorFrame(0, Color.White),\r\n\t\tnew Gradient.ColorFrame(1, Color.White.WithAlpha(0f))\r\n\t);\r\n\r\n\r\n\r\n\t[Button(\u0022Apply to the Ground\u0022), Feature(\u0022Terrain\u0022)]\r\n\tprivate void ApplyTerrainToGround()\r\n\t{\r\n\t\tif (!Scene.IsEditor)\r\n\t\t\treturn;\r\n\r\n\t\tAdaptTerrainToIntersection();\r\n\t}\r\n\r\n\r\n\r\n\tpublic void AdaptTerrainToIntersection()\r\n\t{\r\n\t\tif (!TerrainTarget.IsValid())\r\n\t\t{\r\n\t\t\t// Always take the closest terrain\r\n\t\t\tTerrainTarget = Scene.GetAllComponents\u003CTerrain\u003E().OrderBy(x =\u003E x.WorldPosition.DistanceSquared(WorldPosition)).FirstOrDefault();\r\n\t\t}\r\n\r\n\t\tif (!TerrainTarget.IsValid())\r\n\t\t{\r\n\t\t\tLog.Warning(\u0022RoadTool: No Terrain found in scene.\u0022);\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar storage = TerrainTarget.Storage;\r\n\t\tif (storage == null || storage.HeightMap == null) return;\r\n\r\n\t\t// 1. Setup Parameters \r\n\t\tint resolution = storage.Resolution;\r\n\t\tfloat terrainSize = storage.TerrainSize;\r\n\t\tfloat terrainMaxHeight = storage.TerrainHeight;\r\n\t\tfloat halfSize = terrainSize * 0.5f;\r\n\r\n\t\t// Calculate bounds including falloff\r\n\t\tfloat boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) \u002B TerrainFalloffRadius;\r\n\t\tBBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition \u002B new Vector3(boundSize));\r\n\r\n\t\tvar heightMap = storage.HeightMap;\r\n\r\n\t\t// Capture initial state for Undo\r\n\t\tbool hasModified = false;\r\n\r\n\t\t// Initialize buffers for height calculation \r\n\t\tvar updatedHeights = new float[heightMap.Length];\r\n\t\tvar bestDistance = new float[heightMap.Length];\r\n\r\n\t\tfor (int i = 0; i \u003C heightMap.Length; i\u002B\u002B)\r\n\t\t{\r\n\t\t\t// Decode: Map [0..1] ushort to [0 .. MaxHeight] to match RoadComponent\r\n\t\t\tupdatedHeights[i] = (heightMap[i] / (float)ushort.MaxValue) * terrainMaxHeight;\r\n\t\t\tbestDistance[i] = float.MaxValue;\r\n\t\t}\r\n\r\n\t\tBuildRectangleExitCorridors();\r\n\r\n\t\t// 2. Grid Traversal\r\n\t\tfor (int ix = 0; ix \u003C resolution; ix\u002B\u002B)\r\n\t\t{\r\n\t\t\tfor (int iy = 0; iy \u003C resolution; iy\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\t// 1. Adaptive coordinate detection (Center vs Corner) matching RoadComponent\r\n\t\t\t\tfloat nodeLocalX_corner = (ix / (float)(resolution - 1)) * terrainSize;\r\n\t\t\t\tfloat nodeLocalY_corner = (iy / (float)(resolution - 1)) * terrainSize;\r\n\r\n\t\t\t\tfloat nodeLocalX = nodeLocalX_corner;\r\n\t\t\t\tfloat nodeLocalY = nodeLocalY_corner;\r\n\r\n\t\t\t\t// Check if the intersection is in the centered range\r\n\t\t\t\tvar checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);\r\n\t\t\t\tif (checkPos.x \u003C 0f || checkPos.x \u003E terrainSize || checkPos.y \u003C 0f || checkPos.y \u003E terrainSize)\r\n\t\t\t\t{\r\n\t\t\t\t\tnodeLocalX = nodeLocalX_corner - halfSize;\r\n\t\t\t\t\tnodeLocalY = nodeLocalY_corner - halfSize;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tVector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));\r\n\r\n\t\t\t\tif (!worldBounds.Contains(pixelWorldPos)) continue;\r\n\r\n\t\t\t\tint index = iy * resolution \u002B ix;\r\n\r\n\t\t\t\t// 2. Distance to intersection shape \r\n\t\t\t\tVector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);\r\n\t\t\t\tfloat distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));\r\n\r\n\t\t\t\tif (distance \u003E TerrainFalloffRadius) continue;\r\n\r\n\t\t\t\t// 3. Target height matching RoadComponent (0 to MaxHeight range)\r\n\t\t\t\tVector3 intersectionLocalPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);\r\n\t\t\t\tfloat roadSurfaceHeight = Math.Clamp(intersectionLocalPos.z \u002B TerrainHeightOffset, 0f, terrainMaxHeight);\r\n\t\t\t\tfloat roadInsetHeight = Math.Clamp(roadSurfaceHeight - TerrainRoadInset, 0f, terrainMaxHeight);\r\n\t\t\t\tfloat currentPixelHeight = (heightMap[index] / (float)ushort.MaxValue) * terrainMaxHeight;\r\n\r\n\t\t\t\tfloat candidateHeight;\r\n\t\t\t\tif (distance \u003C= 0) // Inside the intersection \u2014 sink terrain below road to prevent Z-fighting\r\n\t\t\t\t{\r\n\t\t\t\t\tcandidateHeight = roadInsetHeight;\r\n\t\t\t\t}\r\n\t\t\t\telse if (SidewalkWidth \u003E 0f \u0026\u0026 distance \u003C= SidewalkWidth) // Sidewalk ring \u2014 flush with road surface\r\n\t\t\t\t{\r\n\t\t\t\t\tcandidateHeight = roadSurfaceHeight;\r\n\t\t\t\t}\r\n\t\t\t\telse // Falloff \u2014 blend from surface/inset back to original terrain\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat transitionStart = SidewalkWidth \u003E 0f ? SidewalkWidth : 0f;\r\n\t\t\t\t\tfloat transitionBaseHeight = SidewalkWidth \u003E 0f ? roadSurfaceHeight : roadInsetHeight;\r\n\t\t\t\t\tfloat t = Math.Clamp((distance - transitionStart) / TerrainFalloffRadius, 0f, 1f);\r\n\t\t\t\t\tfloat smoothT = t * t * (3f - 2f * t);\r\n\t\t\t\t\tcandidateHeight = MathX.Lerp(transitionBaseHeight, currentPixelHeight, smoothT);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (distance \u003C bestDistance[index])\r\n\t\t\t\t{\r\n\t\t\t\t\tbestDistance[index] = distance;\r\n\t\t\t\t\tupdatedHeights[index] = candidateHeight;\r\n\t\t\t\t\thasModified = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (hasModified)\r\n\t\t{\r\n\t\t\t// 4. Final encoding to ushort (Mapping back to 0..1 without the 0.5 offset)\r\n\t\t\tfor (int i = 0; i \u003C heightMap.Length; i\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\theightMap[i] = (ushort)MathF.Round(Math.Clamp(updatedHeights[i], 0f, terrainMaxHeight) / terrainMaxHeight * ushort.MaxValue);\r\n\t\t\t}\r\n\r\n\t\t\tstorage.HeightMap = heightMap;\r\n\t\t\tstorage.StateHasChanged();\r\n\t\t\tTerrainTarget.Create();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void PaintTerrainToIntersection()\r\n\t{\r\n\t\tif (!TerrainTarget.IsValid() || TerrainEdgeMaterials == null || TerrainEdgeMaterials.Length == 0) return;\r\n\r\n\t\tvar storage = TerrainTarget.Storage;\r\n\t\tif (storage == null || storage.ControlMap == null) return;\r\n\r\n\t\tint resolution = storage.Resolution;\r\n\t\tfloat terrainSize = storage.TerrainSize;\r\n\t\tfloat halfSize = terrainSize * 0.5f;\r\n\r\n\t\t// Identify all material indices in the terrain storage \r\n\t\tbool materialsAdded = false;\r\n\t\tvar materialIndices = new int[TerrainEdgeMaterials.Length];\r\n\t\tfor (int m = 0; m \u003C TerrainEdgeMaterials.Length; m\u002B\u002B)\r\n\t\t{\r\n\t\t\tif (TerrainEdgeMaterials[m] == null) continue;\r\n\r\n\t\t\tint idx = storage.Materials.IndexOf(TerrainEdgeMaterials[m]);\r\n\t\t\tif (idx == -1)\r\n\t\t\t{\r\n\t\t\t\tstorage.Materials.Add(TerrainEdgeMaterials[m]);\r\n\t\t\t\tidx = storage.Materials.Count - 1;\r\n\t\t\t\tmaterialsAdded = true;\r\n\t\t\t}\r\n\r\n\t\t\tif (idx \u003E 31)\r\n\t\t\t{\r\n\t\t\t\tLog.Error($\u0022RoadTool: Terrain has too many materials ({idx}). Material \u0027{TerrainEdgeMaterials[m].ResourceName}\u0027 cannot be painted.\u0022);\r\n\t\t\t\tidx = 0;\r\n\t\t\t}\r\n\r\n\t\t\tmaterialIndices[m] = idx;\r\n\t\t}\r\n\r\n\t\tif (materialsAdded)\r\n\t\t{\r\n\t\t\tstorage.StateHasChanged();\r\n\t\t\tTerrainTarget.Create();\r\n\t\t}\r\n\r\n\t\tfloat boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) \u002B TerrainEdgeRadius; // This line is unchanged \r\n\t\tBBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition \u002B new Vector3(boundSize)); // This line is unchanged\r\n\r\n\t\tBuildRectangleExitCorridors();\r\n\r\n\t\tvar controlMap = storage.ControlMap;\r\n\t\tbool hasModified = false;\r\n\r\n\t\tfor (int ix = 0; ix \u003C resolution; ix\u002B\u002B)\r\n\t\t{\r\n\t\t\tfor (int iy = 0; iy \u003C resolution; iy\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tfloat nodeLocalX = (ix / (float)(resolution - 1)) * terrainSize;\r\n\t\t\t\tfloat nodeLocalY = (iy / (float)(resolution - 1)) * terrainSize;\r\n\r\n\t\t\t\tvar checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);\r\n\t\t\t\tif (checkPos.x \u003C 0f || checkPos.x \u003E terrainSize || checkPos.y \u003C 0f || checkPos.y \u003E terrainSize)\r\n\t\t\t\t{\r\n\t\t\t\t\tnodeLocalX -= halfSize;\r\n\t\t\t\t\tnodeLocalY -= halfSize;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tVector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));\r\n\t\t\t\tif (!worldBounds.Contains(pixelWorldPos)) continue;\r\n\r\n\t\t\t\tVector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);\r\n\t\t\t\tfloat distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));\r\n\r\n\t\t\t\tif (distance \u003E TerrainEdgeRadius) continue;\r\n\r\n\t\t\t\tint index = iy * resolution \u002B ix;\r\n\t\t\t\tfloat t = Math.Clamp(distance / TerrainEdgeRadius, 0f, 1f);\r\n\t\t\t\tfloat blendStrength = TerrainEdgeBlendGradient.Evaluate(t).a;\r\n\r\n\t\t\t\tif (blendStrength \u003E 0.01f)\r\n\t\t\t\t{\r\n\t\t\t\t\t// Add deterministic noise to blend textures together (Dithering)\r\n\t\t\t\t\tfloat pixelNoise = ((float)((index * 1103515245 \u002B 12345) \u0026 0x7FFFFFFF) / 0x7FFFFFFF) * TerrainTextureNoise - (TerrainTextureNoise * 0.5f);\r\n\t\t\t\t\tfloat noisyT = Math.Clamp(t \u002B pixelNoise, 0f, 1f);\r\n\t\t\t\t\tfloat noisyDistance = distance \u002B (pixelNoise * TerrainEdgeRadius);\r\n\r\n\t\t\t\t\tint materialIndex;\r\n\t\t\t\t\tif (noisyDistance \u003C= 0)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmaterialIndex = materialIndices[0];\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tint edgeMatCount = materialIndices.Length - 1;\r\n\t\t\t\t\t\t// Using noisyT for index selection \r\n\t\t\t\t\t\tint edgeIdx = edgeMatCount \u003E 0 ? Math.Clamp((int)(noisyT * edgeMatCount), 0, edgeMatCount - 1) \u002B 1 : 0;\r\n\t\t\t\t\t\tmaterialIndex = materialIndices[edgeIdx];\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tuint packed = controlMap[index];\r\n\t\t\t\t\tvar mat = new CompactTerrainMaterial(packed);\r\n\r\n\t\t\t\t\tif (TerrainTargetLayer == TerrainTextureLayer.Base)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmat.BaseTextureId = (byte)materialIndex;\r\n\t\t\t\t\t\tmat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 0, blendStrength);\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t// Otherwise, we place it in Overlay and increase the BlendFactor to display it\r\n\t\t\t\t\t\tmat.OverlayTextureId = (byte)materialIndex;\r\n\t\t\t\t\t\tmat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 255, blendStrength);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tcontrolMap[index] = mat.Packed;\r\n\t\t\t\t\thasModified = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (hasModified)\r\n\t\t{\r\n\t\t\tstorage.ControlMap = controlMap;\r\n\t\t\tstorage.StateHasChanged();\r\n\t\t\tTerrainTarget.SyncGPUTexture();\r\n\t\t}\r\n\t}\r\n\r\n\t// Per-opening exit corridors in local space, rebuilt once per flatten so the per-pixel distance test stays cheap.\r\n\t// Each entry is an opening\u0027s road-edge centre, its outward direction, its lateral direction, and half its width.\r\n\tprivate (Vector3 Center, Vector3 Outward, Vector3 Lateral, float Half)[] m_RectangleExitCorridors = Array.Empty\u003C(Vector3, Vector3, Vector3, float)\u003E();\r\n\r\n\tprivate void BuildRectangleExitCorridors()\r\n\t{\r\n\t\tif (Shape != IntersectionShape.Rectangle)\r\n\t\t{\r\n\t\t\tm_RectangleExitCorridors = Array.Empty\u003C(Vector3, Vector3, Vector3, float)\u003E();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEnsureRectangleExits();\r\n\r\n\t\tm_RectangleExitCorridors = Exits\r\n\t\t\t.Where(exit =\u003E exit != null)\r\n\t\t\t.Select(exit =\u003E\r\n\t\t\t{\r\n\t\t\t\tTransform t = GetRectangleExitLocalTransform(exit.Side, false, exit.Offset);\r\n\t\t\t\treturn (t.Position, t.Rotation.Forward, t.Rotation.Right, exit.Width * 0.5f);\r\n\t\t\t})\r\n\t\t\t.ToArray();\r\n\t}\r\n\r\n\r\n\r\n\tprivate float GetDistanceToIntersectionShape(Vector3 localPixelPos)\r\n\t{\r\n\t\tif (Shape == IntersectionShape.Rectangle)\r\n\t\t{\r\n\t\t\tfloat hl = Length * 0.5f;\r\n\t\t\tfloat hw = Width * 0.5f;\r\n\t\t\tfloat dx = MathF.Max(MathF.Abs(localPixelPos.x) - hl, 0);\r\n\t\t\tfloat dy = MathF.Max(MathF.Abs(localPixelPos.y) - hw, 0);\r\n\t\t\tfloat dist = MathF.Sqrt(dx * dx \u002B dy * dy);\r\n\r\n\t\t\t// Treat each open exit\u0027s corridor as inside, so terrain doesn\u0027t poke up through a road opening. A corridor is\r\n\t\t\t// the band beyond an opening\u0027s road edge (outward) and within that opening\u0027s width (lateral) \u2014 one per opening.\r\n\t\t\tif (dist \u003E 0)\r\n\t\t\t{\r\n\t\t\t\tforeach (var corridor in m_RectangleExitCorridors)\r\n\t\t\t\t{\r\n\t\t\t\t\tVector3 toPixel = localPixelPos - corridor.Center;\r\n\r\n\t\t\t\t\tif (Vector3.Dot(toPixel, corridor.Outward) \u003E= 0.0f \u0026\u0026 MathF.Abs(Vector3.Dot(toPixel, corridor.Lateral)) \u003C= corridor.Half)\r\n\t\t\t\t\t\treturn 0;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\treturn dist;\r\n\t\t}\r\n\r\n\t\t// Circle\r\n\t\tfloat radDist = MathF.Max(localPixelPos.WithZ(0).Length - Radius, 0);\r\n\r\n\t\tif (radDist \u003E 0 \u0026\u0026 CircleExits != null \u0026\u0026 CircleExits.Length \u003E 0)\r\n\t\t{\r\n\t\t\tVector3 pixelDir = localPixelPos.WithZ(0);\r\n\t\t\tif (pixelDir.LengthSquared \u003E 0.0001f)\r\n\t\t\t\tpixelDir = pixelDir.Normal;\r\n\r\n\t\t\tforeach (var exit in CircleExits)\r\n\t\t\t{\r\n\t\t\t\t// Use dot product to stay independent of angle conventions\r\n\t\t\t\tVector3 exitDir = Rotation.FromYaw(exit.AngleDegrees).Forward;\r\n\t\t\t\tfloat cosHalfAngle = MathF.Cos(MathF.Atan(exit.RoadWidth / Radius));\r\n\t\t\t\tif (Vector3.Dot(pixelDir, exitDir) \u003E= cosHalfAngle)\r\n\t\t\t\t\treturn 0;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn radDist;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"RoadIntersectionComponent/RoadIntersectionComponent.Traffic.cs","FileName":"RoadIntersectionComponent.Traffic.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\npublic partial class RoadIntersectionComponent\n{\n\t/// \u003Csummary\u003E\n\t/// A single drivable exit of an intersection, expressed in world space.\n\t/// \u003Csee cref=\u0022Transform\u0022/\u003E.Forward points outward (away from the intersection), matching the snap targets.\n\t/// \u003C/summary\u003E\n\tpublic readonly struct TrafficExit\n\t{\n\t\tpublic Transform Transform { get; init; }\n\t\tpublic float RoadWidth { get; init; }\n\n\t\t/// \u003Csummary\u003EPedestrians get no crossing over this arm\u0027s mouth. Vehicles are unaffected.\u003C/summary\u003E\n\t\tpublic bool NoCrossing { get; init; }\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// When enabled, this intersection is ignored by the traffic system: vehicles will not route through it.\n\t/// \u003C/summary\u003E\n\t[Property, Feature(\u0022General\u0022), Category(\u0022Traffic\u0022), Order(2)] public bool ExcludeTraffic { get; set; } = false;\n\n\t/// \u003Csummary\u003ESpeed limit for traffic crossing this intersection, in km/h.\u003C/summary\u003E\n\t[Property, Feature(\u0022General\u0022), Category(\u0022Traffic\u0022), Order(2), Range(5.0f, 130.0f)] public float SpeedLimit { get; set; } = 30.0f;\n\n\n\n\t/// \u003Csummary\u003E\n\t/// Enumerates every active exit of this intersection (rectangle or circle) as a world transform plus road width.\n\t/// These are the same outer-edge positions that \u003Csee cref=\u0022SnapNearbyRoads\u0022/\u003E snaps roads to, so the traffic\n\t/// graph can match road endpoints against them by proximity.\n\t/// \u003C/summary\u003E\n\tpublic List\u003CTrafficExit\u003E GetTrafficExits()\n\t{\n\t\tvar exits = new List\u003CTrafficExit\u003E();\n\n\t\tif (Shape == IntersectionShape.Rectangle)\n\t\t{\n\t\t\tEnsureRectangleExits();\n\n\t\t\tforeach (var exit in Exits)\n\t\t\t{\n\t\t\t\tif (exit is null)\n\t\t\t\t\tcontinue;\n\n\t\t\t\texits.Add(new TrafficExit\n\t\t\t\t{\n\t\t\t\t\tTransform = GetRectangleExitTransform(exit.Side, true, exit.Offset),\n\t\t\t\t\tRoadWidth = exit.Width,\n\t\t\t\t\tNoCrossing = exit.NoCrossing\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tvar circleExits = CircleExits ?? Array.Empty\u003CCircleExit\u003E();\n\n\t\t\tfor (int i = 0; i \u003C circleExits.Length; i\u002B\u002B)\n\t\t\t{\n\t\t\t\texits.Add(new TrafficExit\n\t\t\t\t{\n\t\t\t\t\tTransform = GetCircleExitTransform(i, true),\n\t\t\t\t\tRoadWidth = circleExits[i].RoadWidth,\n\t\t\t\t\tNoCrossing = circleExits[i].NoCrossing\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn exits;\n\t}\n}\n"},{"Ident":"redsnail.roadtool","Path":"RoadIntersectionComponent/RoadIntersectionComponent.Utility.cs","FileName":"RoadIntersectionComponent.Utility.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadIntersectionComponent\r\n{\r\n\t// Computes a quadratic Bezier control point at the intersection of the two tangent lines.\r\n\t// Returns true if the lines intersect, false if parallel (in which case the midpoint is used as a fallback).\r\n\t// The control distance along _StartTan is clamped to the chord length so asymmetric tangents (e.g. an\r\n\t// off-grid exit angle whose disc tangent points well past the outer corner) don\u0027t drive the bezier past\r\n\t// the outer endpoint \u2014 overshoot produces samples beyond the endpoint and flips downstream triangle winding.\r\n\tprivate static bool TryBezierControl(Vector3 _Start, Vector3 _StartTan, Vector3 _End, Vector3 _EndTan, out Vector3 _Control)\r\n\t{\r\n\t\tfloat det = _StartTan.x * _EndTan.y - _EndTan.x * _StartTan.y;\r\n\r\n\t\tif (MathF.Abs(det) \u003C 0.0001f)\r\n\t\t{\r\n\t\t\t_Control = (_Start \u002B _End) * 0.5f;\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tVector3 d = _End - _Start;\r\n\t\tfloat r = (d.x * _EndTan.y - _EndTan.x * d.y) / det;\r\n\t\tfloat rMax = d.Length;\r\n\t\tfloat rClamped = Math.Clamp(r, 0.0f, rMax);\r\n\t\t_Control = _Start \u002B rClamped * _StartTan;\r\n\t\treturn true;\r\n\t}\r\n\r\n\tprivate static Vector3 SampleQuadBezier(Vector3 _B0, Vector3 _B1, Vector3 _B2, float _T)\r\n\t{\r\n\t\tfloat u = 1.0f - _T;\r\n\t\treturn u * u * _B0 \u002B 2.0f * u * _T * _B1 \u002B _T * _T * _B2;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"RoadManager/RoadTrafficGraph.Routing.cs","FileName":"RoadTrafficGraph.Routing.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// \u003Csummary\u003E\n/// Routing queries over the lane graph \u2014 how far it actually is to drive from one place to another.\n///\n/// \u0022Actually\u0022 is the point: straight-line distance is useless for anything that has to obey roads. A delivery\n/// two blocks away as the crow flies can be a mile of one-way streets, and a job that times you on crow-flight\n/// distance is unwinnable in exactly the places that are most interesting to drive.\n/// \u003C/summary\u003E\npublic sealed partial class RoadTrafficGraph\n{\n\t/// \u003Csummary\u003E\n\t/// How far either end of a route may be from a lane and still count as being on it. A road carries a lane\n\t/// per direction, so a single point sits near several \u2014 see \u003Csee cref=\u0022FindNearbyLanes\u0022/\u003E for why taking\n\t/// them all matters.\n\t/// \u003C/summary\u003E\n\tpublic const float DefaultRouteSnapRadius = 1000.0f;\n\n\n\n\t/// \u003Csummary\u003E\n\t/// Driving distance from \u003Cparamref name=\u0022_From\u0022/\u003E to \u003Cparamref name=\u0022_To\u0022/\u003E, following lanes in their legal\n\t/// direction. False when neither end is anywhere near a road, or when no route exists at all \u2014 a one-way\n\t/// system can genuinely have no way round.\n\t///\n\t/// Dijkstra over whole lanes rather than individual waypoints: lanes are the unit the graph is linked in,\n\t/// and a city\u0027s worth of them is a few thousand nodes. Costs are measured to the START of each lane, with\n\t/// the partial lengths at both ends added on, so the answer is measured between the actual points.\n\t/// \u003C/summary\u003E\n\tpublic bool TryGetDrivingDistance(Vector3 _From, Vector3 _To, out float _Distance)\n\t{\n\t\t_Distance = 0.0f;\n\n\t\tList\u003C(TrafficLane Lane, int Index)\u003E starts = FindNearbyLanes(_From);\n\t\tList\u003C(TrafficLane Lane, int Index)\u003E goals = FindNearbyLanes(_To);\n\n\t\tif (starts.Count == 0 || goals.Count == 0)\n\t\t\treturn false;\n\n\t\tvar goalIndices = new Dictionary\u003CTrafficLane, int\u003E();\n\n\t\tforeach ((TrafficLane lane, int index) in goals)\n\t\t\tgoalIndices[lane] = index;\n\n\t\tvar best = new Dictionary\u003CTrafficLane, float\u003E();\n\t\tvar queue = new PriorityQueue\u003CTrafficLane, float\u003E();\n\n\t\tfloat bestTotal = float.MaxValue;\n\n\t\tforeach ((TrafficLane lane, int index) in starts)\n\t\t{\n\t\t\t// Goal on the same lane and ahead of us: straight down the road, no junction involved. Behind us\n\t\t\t// doesn\u0027t count \u2014 a lane is one-way, so that really does mean driving round and coming back, which\n\t\t\t// the search below works out properly.\n\t\t\tif (goalIndices.TryGetValue(lane, out int goalIndex) \u0026\u0026 goalIndex \u003E= index)\n\t\t\t\tbestTotal = Math.Min(bestTotal, lane.DistanceFromStart(goalIndex) - lane.DistanceFromStart(index));\n\n\t\t\t// We start partway along, so what\u0027s reachable is the successors, at the cost of finishing this lane.\n\t\t\tfloat toEnd = lane.DistanceToEnd(index);\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t\tRelax(next, toEnd, best, queue);\n\t\t}\n\n\t\twhile (queue.TryDequeue(out TrafficLane lane, out float cost))\n\t\t{\n\t\t\t// Min-ordered, so once the cheapest thing left already costs more than an answer we have, nothing\n\t\t\t// better can come out of it.\n\t\t\tif (cost \u003E= bestTotal)\n\t\t\t\tbreak;\n\n\t\t\tif (best.TryGetValue(lane, out float known) \u0026\u0026 cost \u003E known)\n\t\t\t\tcontinue;\n\n\t\t\t// Reaching a goal lane doesn\u0027t end the search: another route might arrive at a different goal\n\t\t\t// candidate \u2014 the other side of the same street, say \u2014 for less.\n\t\t\tif (goalIndices.TryGetValue(lane, out int goalIndex))\n\t\t\t\tbestTotal = Math.Min(bestTotal, cost \u002B lane.DistanceFromStart(goalIndex));\n\n\t\t\tfloat exit = cost \u002B lane.Length;\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t\tRelax(next, exit, best, queue);\n\t\t}\n\n\t\tif (bestTotal \u003E= float.MaxValue)\n\t\t\treturn false;\n\n\t\t_Distance = Math.Max(0.0f, bestTotal);\n\n\t\treturn true;\n\t}\n\n\n\n\t/// \u003Csummary\u003E\n\t/// The actual waypoints to DRIVE from one point to another, following lanes in their legal direction. False\n\t/// when there\u0027s no route, same as \u003Csee cref=\u0022TryGetDrivingDistance\u0022/\u003E.\n\t///\n\t/// This is the vehicle counterpart of a navmesh path, and it has to be a separate structure rather than the\n\t/// navmesh itself. A navmesh is baked around a person \u2014 it runs over pavements, through doorways and up\n\t/// stairs, and its corridors are person-wide. A route down one is a perfectly valid walk and an impossible\n\t/// drive, and unlike a pedestrian scraping a wall, a car routed somewhere it doesn\u0027t fit is stuck for good.\n\t/// Lanes are the drivable surface by construction, and they carry direction, which a navmesh has no concept\n\t/// of at all.\n\t///\n\t/// The list is reused rather than returned, because anything chasing anything re-routes constantly.\n\t/// \u003C/summary\u003E\n\tpublic bool TryGetDrivingRoute(Vector3 _From, Vector3 _To, List\u003CVector3\u003E _Route)\n\t{\n\t\t_Route.Clear();\n\n\t\tif (!TrySearchRoute(_From, _To, out RouteSearch search))\n\t\t\treturn false;\n\n\t\tBuildRoute(search.Starts, search.GoalIndices, search.CameFrom, search.Goal, search.Start, search.StartIndex, _Route);\n\n\t\treturn _Route.Count \u003E 0;\n\t}\n\n\n\n\t/// \u003Csummary\u003E\n\t/// The same route as \u003Csee cref=\u0022TryGetDrivingRoute\u0022/\u003E, but as the LANES to drive rather than the waypoints\n\t/// along them \u2014 including the one we\u0027re starting on.\n\t///\n\t/// This is the form anything that already knows how to drive a lane wants. \u003Csee cref=\u0022TrafficVehicle\u0022/\u003E\n\t/// follows lanes and picks a successor at every junction; handing it a lane list turns \u0022wander\u0022 into \u0022go\n\t/// here\u0022 without touching a single line of how it actually steers, brakes or corners.\n\t/// \u003C/summary\u003E\n\tpublic bool TryGetDrivingLaneRoute(Vector3 _From, Vector3 _To, List\u003CTrafficLane\u003E _Route)\n\t{\n\t\treturn TryGetDrivingLaneRoute(FindNearbyLanes(_From), _To, _Route);\n\t}\n\n\n\n\t/// \u003Csummary\u003E\n\t/// The same, but starting from the lane a vehicle is ALREADY DRIVING rather than from its position.\n\t///\n\t/// This is the overload anything mid-journey wants, and the difference is not subtle. Asking by position\n\t/// seeds the search with every lane in range \u2014 including the one going the other way down the same road \u2014\n\t/// and Dijkstra will happily return the shortest route from whichever of those is cheapest. That route\n\t/// starts on a lane the vehicle is not on, so it never matches, and a driver that can\u0027t find itself in its\n\t/// own route falls back to picking turns at random. The symptom is a car that mostly goes the right way and\n\t/// occasionally sets off round the block for no visible reason.\n\t/// \u003C/summary\u003E\n\tpublic bool TryGetDrivingLaneRoute(TrafficLane _FromLane, Vector3 _FromPosition, Vector3 _To, List\u003CTrafficLane\u003E _Route)\n\t{\n\t\t_Route.Clear();\n\n\t\tif (_FromLane is null || _FromLane.Waypoints.Count == 0)\n\t\t\treturn false;\n\n\t\treturn TryGetDrivingLaneRoute([(_FromLane, NearestWaypointIndex(_FromLane, _FromPosition))], _To, _Route);\n\t}\n\n\n\n\t/// \u003Csummary\u003EWhich waypoint of a lane is closest to a point.\u003C/summary\u003E\n\tprivate static int NearestWaypointIndex(TrafficLane _Lane, Vector3 _Point)\n\t{\n\t\tint best = 0;\n\t\tfloat bestDistance = float.MaxValue;\n\n\t\tfor (int i = 0; i \u003C _Lane.Waypoints.Count; i\u002B\u002B)\n\t\t{\n\t\t\tfloat distance = _Lane.Waypoints[i].DistanceSquared(_Point);\n\n\t\t\tif (distance \u003E= bestDistance)\n\t\t\t\tcontinue;\n\n\t\t\tbestDistance = distance;\n\t\t\tbest = i;\n\t\t}\n\n\t\treturn best;\n\t}\n\n\n\n\tprivate bool TryGetDrivingLaneRoute(List\u003C(TrafficLane Lane, int Index)\u003E _Starts, Vector3 _To, List\u003CTrafficLane\u003E _Route)\n\t{\n\t\t_Route.Clear();\n\n\t\tif (!TrySearchRoute(_Starts, _To, out RouteSearch search))\n\t\t\treturn false;\n\n\t\t// Never left the start lane \u2014 the route is just that one.\n\t\tif (search.Goal is null)\n\t\t{\n\t\t\tif (search.Start is null)\n\t\t\t\treturn false;\n\n\t\t\t_Route.Add(search.Start);\n\n\t\t\treturn true;\n\t\t}\n\n\t\tList\u003CTrafficLane\u003E chain = BuildLaneChain(search.CameFrom, search.Goal);\n\n\t\tif (chain.Count == 0)\n\t\t\treturn false;\n\n\t\t// The lane we\u0027re ON isn\u0027t in the chain (the chain begins at one of its successors), and a driver already\n\t\t// travelling it needs to see it in the list or its very first junction is an unplanned one.\n\t\tforeach ((TrafficLane lane, int _) in search.Starts)\n\t\t{\n\t\t\tif (!lane.Successors.Contains(chain[0]))\n\t\t\t\tcontinue;\n\n\t\t\t_Route.Add(lane);\n\n\t\t\tbreak;\n\t\t}\n\n\t\t_Route.AddRange(chain);\n\n\t\treturn true;\n\t}\n\n\n\n\t/// \u003Csummary\u003EWhat a completed search found: the winning route\u0027s ends, and the map to walk it back with.\u003C/summary\u003E\n\tprivate struct RouteSearch\n\t{\n\t\tpublic List\u003C(TrafficLane Lane, int Index)\u003E Starts;\n\t\tpublic Dictionary\u003CTrafficLane, int\u003E GoalIndices;\n\t\tpublic Dictionary\u003CTrafficLane, TrafficLane\u003E CameFrom;\n\n\t\t/// \u003Csummary\u003EThe lane the route ends on, or null when it never left the lane it started on.\u003C/summary\u003E\n\t\tpublic TrafficLane Goal;\n\n\t\tpublic TrafficLane Start;\n\t\tpublic int StartIndex;\n\t}\n\n\n\n\t/// \u003Csummary\u003E\n\t/// The Dijkstra itself, shared by both route shapes so there\u0027s one search to be correct rather than two to\n\t/// keep in step.\n\t/// \u003C/summary\u003E\n\tprivate bool TrySearchRoute(Vector3 _From, Vector3 _To, out RouteSearch _Result)\n\t{\n\t\treturn TrySearchRoute(FindNearbyLanes(_From), _To, out _Result);\n\t}\n\n\n\n\t/// \u003Cinheritdoc cref=\u0022TrySearchRoute(Vector3, Vector3, out RouteSearch)\u0022/\u003E\n\tprivate bool TrySearchRoute(List\u003C(TrafficLane Lane, int Index)\u003E _Starts, Vector3 _To, out RouteSearch _Result)\n\t{\n\t\t_Result = default;\n\n\t\tList\u003C(TrafficLane Lane, int Index)\u003E starts = _Starts;\n\t\tList\u003C(TrafficLane Lane, int Index)\u003E goals = FindNearbyLanes(_To);\n\n\t\tif (starts is null || starts.Count == 0 || goals.Count == 0)\n\t\t\treturn false;\n\n\t\tvar goalIndices = new Dictionary\u003CTrafficLane, int\u003E();\n\n\t\tforeach ((TrafficLane lane, int index) in goals)\n\t\t\tgoalIndices[lane] = index;\n\n\t\tvar best = new Dictionary\u003CTrafficLane, float\u003E();\n\t\tvar cameFrom = new Dictionary\u003CTrafficLane, TrafficLane\u003E();\n\t\tvar queue = new PriorityQueue\u003CTrafficLane, float\u003E();\n\n\t\t// The winning route so far: where it ends, and which of the several starts it began at.\n\t\tfloat bestTotal = float.MaxValue;\n\t\tTrafficLane bestGoal = null;\n\t\tTrafficLane bestStart = null;\n\t\tint bestStartIndex = 0;\n\n\t\tforeach ((TrafficLane lane, int index) in starts)\n\t\t{\n\t\t\t// Goal on the same lane and ahead of us \u2014 no junction involved, so the route is just this stretch.\n\t\t\tif (goalIndices.TryGetValue(lane, out int sameLaneGoal) \u0026\u0026 sameLaneGoal \u003E= index)\n\t\t\t{\n\t\t\t\tfloat direct = lane.DistanceFromStart(sameLaneGoal) - lane.DistanceFromStart(index);\n\n\t\t\t\tif (direct \u003C bestTotal)\n\t\t\t\t{\n\t\t\t\t\tbestTotal = direct;\n\t\t\t\t\tbestGoal = null;      // null goal marks \u0022never left the start lane\u0022\n\t\t\t\t\tbestStart = lane;\n\t\t\t\t\tbestStartIndex = index;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfloat toEnd = lane.DistanceToEnd(index);\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t{\n\t\t\t\tif (Relax(next, toEnd, best, queue))\n\t\t\t\t\tcameFrom[next] = lane;\n\t\t\t}\n\t\t}\n\n\t\twhile (queue.TryDequeue(out TrafficLane lane, out float cost))\n\t\t{\n\t\t\tif (cost \u003E= bestTotal)\n\t\t\t\tbreak;\n\n\t\t\tif (best.TryGetValue(lane, out float known) \u0026\u0026 cost \u003E known)\n\t\t\t\tcontinue;\n\n\t\t\tif (goalIndices.TryGetValue(lane, out int goalIndex))\n\t\t\t{\n\t\t\t\tfloat total = cost \u002B lane.DistanceFromStart(goalIndex);\n\n\t\t\t\tif (total \u003C bestTotal)\n\t\t\t\t{\n\t\t\t\t\tbestTotal = total;\n\t\t\t\t\tbestGoal = lane;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfloat exit = cost \u002B lane.Length;\n\n\t\t\tforeach (TrafficLane next in lane.Successors)\n\t\t\t{\n\t\t\t\tif (Relax(next, exit, best, queue))\n\t\t\t\t\tcameFrom[next] = lane;\n\t\t\t}\n\t\t}\n\n\t\tif (bestTotal \u003E= float.MaxValue)\n\t\t\treturn false;\n\n\t\t_Result = new RouteSearch\n\t\t{\n\t\t\tStarts = starts,\n\t\t\tGoalIndices = goalIndices,\n\t\t\tCameFrom = cameFrom,\n\t\t\tGoal = bestGoal,\n\t\t\tStart = bestStart,\n\t\t\tStartIndex = bestStartIndex\n\t\t};\n\n\t\treturn true;\n\t}\n\n\n\n\t/// \u003Csummary\u003E\n\t/// Walks the predecessor chain back from the winning goal lane to whichever start it came from, then lays\n\t/// the waypoints down in travel order.\n\t///\n\t/// The two ends are partial lanes \u2014 we join partway along the first and stop partway along the last \u2014 which\n\t/// is why they\u0027re handled separately from the whole lanes in between.\n\t/// \u003C/summary\u003E\n\tprivate void BuildRoute(List\u003C(TrafficLane Lane, int Index)\u003E _Starts, Dictionary\u003CTrafficLane, int\u003E _GoalIndices,\n\t                        Dictionary\u003CTrafficLane, TrafficLane\u003E _CameFrom, TrafficLane _Goal,\n\t                        TrafficLane _Start, int _StartIndex, List\u003CVector3\u003E _Route)\n\t{\n\t\t// Never left the start lane: one straight run down it.\n\t\tif (_Goal is null)\n\t\t{\n\t\t\tif (_Start is null || !_GoalIndices.TryGetValue(_Start, out int stop))\n\t\t\t\treturn;\n\n\t\t\tfor (int i = _StartIndex; i \u003C= stop; i\u002B\u002B)\n\t\t\t\t_Route.Add(_Start.Waypoints[i]);\n\n\t\t\treturn;\n\t\t}\n\n\t\tList\u003CTrafficLane\u003E chain = BuildLaneChain(_CameFrom, _Goal);\n\n\t\tif (chain.Count == 0)\n\t\t\treturn;\n\n\t\t// The first lane in the chain is a successor of the start lane, so the start lane itself isn\u0027t in it \u2014\n\t\t// find which of the candidates fed it and lay down the tail of that one first.\n\t\tTrafficLane head = chain[0];\n\n\t\tforeach ((TrafficLane lane, int index) in _Starts)\n\t\t{\n\t\t\tif (!lane.Successors.Contains(head))\n\t\t\t\tcontinue;\n\n\t\t\tfor (int i = index; i \u003C lane.Waypoints.Count; i\u002B\u002B)\n\t\t\t\t_Route.Add(lane.Waypoints[i]);\n\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (int c = 0; c \u003C chain.Count; c\u002B\u002B)\n\t\t{\n\t\t\tTrafficLane lane = chain[c];\n\n\t\t\t// The last one stops at the goal waypoint rather than running to the end of the road.\n\t\t\tint stop = c == chain.Count - 1 \u0026\u0026 _GoalIndices.TryGetValue(lane, out int goalIndex)\n\t\t\t\t? goalIndex\n\t\t\t\t: lane.Waypoints.Count - 1;\n\n\t\t\tfor (int i = 0; i \u003C= stop; i\u002B\u002B)\n\t\t\t\t_Route.Add(lane.Waypoints[i]);\n\t\t}\n\t}\n\n\n\n\t/// \u003Csummary\u003EWalks the predecessor map back from a goal lane and returns the chain in travel order.\u003C/summary\u003E\n\tprivate List\u003CTrafficLane\u003E BuildLaneChain(Dictionary\u003CTrafficLane, TrafficLane\u003E _CameFrom, TrafficLane _Goal)\n\t{\n\t\tvar chain = new List\u003CTrafficLane\u003E();\n\t\tTrafficLane current = _Goal;\n\n\t\t// Bounded by the lane count so a cycle in the map can\u0027t spin forever.\n\t\tfor (int step = 0; step \u003C= Lanes.Count \u0026\u0026 current is not null; step\u002B\u002B)\n\t\t{\n\t\t\tchain.Add(current);\n\n\t\t\tif (!_CameFrom.TryGetValue(current, out TrafficLane previous))\n\t\t\t\tbreak;\n\n\t\t\tcurrent = previous;\n\t\t}\n\n\t\tchain.Reverse();\n\n\t\treturn chain;\n\t}\n\n\n\n\t/// \u003Csummary\u003ETrue when this was an improvement, so the caller knows whether to record the predecessor.\u003C/summary\u003E\n\tprivate static bool Relax(TrafficLane _Lane, float _Cost, Dictionary\u003CTrafficLane, float\u003E _Best, PriorityQueue\u003CTrafficLane, float\u003E _Queue)\n\t{\n\t\tif (_Best.TryGetValue(_Lane, out float existing) \u0026\u0026 existing \u003C= _Cost)\n\t\t\treturn false;\n\n\t\t_Best[_Lane] = _Cost;\n\n\t\t_Queue.Enqueue(_Lane, _Cost);\n\n\t\treturn true;\n\t}\n\n\n\n\t/// \u003Csummary\u003E\n\t/// Every drivable lane with a waypoint within \u003Cparamref name=\u0022_Radius\u0022/\u003E of the point, and which waypoint\n\t/// that was \u2014 at most one entry per lane.\n\t///\n\t/// Taking ALL of them, rather than just the closest, is what makes routing reliable. A road carries a lane\n\t/// per direction, so any point on it is near at least two; picking only the nearest is a coin flip that can\n\t/// land on the one pointing away from where you\u0027re going, or on one nothing feeds into. The route then comes\n\t/// back as impossible even though the lane a few metres over is trivially routable. Seeding the search with\n\t/// every candidate \u2014 and accepting any of them at the far end \u2014 also gets the natural answer for free:\n\t/// either side of the street will do, whichever is closer to drive.\n\t///\n\t/// Road lanes only. Snapping an endpoint onto an intersection cross-lane would measure from the middle of\n\t/// a junction.\n\t/// \u003C/summary\u003E\n\tpublic List\u003C(TrafficLane Lane, int Index)\u003E FindNearbyLanes(Vector3 _Point, float _Radius = DefaultRouteSnapRadius)\n\t{\n\t\tvar results = new List\u003C(TrafficLane, int)\u003E();\n\n\t\tTrafficLane nearestLane = null;\n\t\tint nearestIndex = 0;\n\t\tfloat nearestDistance = float.MaxValue;\n\n\t\tfloat radiusSquared = _Radius * _Radius;\n\n\t\tforeach (TrafficLane lane in Lanes)\n\t\t{\n\t\t\tif (!lane.IsRoadLane)\n\t\t\t\tcontinue;\n\n\t\t\tint laneIndex = -1;\n\t\t\tfloat laneDistance = float.MaxValue;\n\n\t\t\tfor (int i = 0; i \u003C lane.Waypoints.Count; i\u002B\u002B)\n\t\t\t{\n\t\t\t\tfloat distance = lane.Waypoints[i].DistanceSquared(_Point);\n\n\t\t\t\tif (distance \u003E= laneDistance)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tlaneDistance = distance;\n\t\t\t\tlaneIndex = i;\n\t\t\t}\n\n\t\t\tif (laneIndex \u003C 0)\n\t\t\t\tcontinue;\n\n\t\t\tif (laneDistance \u003C nearestDistance)\n\t\t\t{\n\t\t\t\tnearestDistance = laneDistance;\n\t\t\t\tnearestLane = lane;\n\t\t\t\tnearestIndex = laneIndex;\n\t\t\t}\n\n\t\t\tif (laneDistance \u003C= radiusSquared)\n\t\t\t\tresults.Add((lane, laneIndex));\n\t\t}\n\n\t\t// Off-road entirely (a car park, a field) \u2014 the closest lane is still the honest answer, so don\u0027t come\n\t\t// back empty and turn a long route into \u0022no route\u0022.\n\t\tif (results.Count == 0 \u0026\u0026 nearestLane is not null)\n\t\t\tresults.Add((nearestLane, nearestIndex));\n\n\t\treturn results;\n\t}\n\n\n\n\t/// \u003Csummary\u003E\n\t/// The single drivable lane closest to a point, and which waypoint that was. Prefer\n\t/// \u003Csee cref=\u0022FindNearbyLanes\u0022/\u003E for routing \u2014 one lane is rarely the whole answer for a two-way road.\n\t/// \u003C/summary\u003E\n\tpublic TrafficLane FindNearestLane(Vector3 _Point, out int _Index)\n\t{\n\t\tTrafficLane bestLane = null;\n\t\tfloat bestDistance = float.MaxValue;\n\n\t\t_Index = 0;\n\n\t\tforeach (TrafficLane lane in Lanes)\n\t\t{\n\t\t\tif (!lane.IsRoadLane)\n\t\t\t\tcontinue;\n\n\t\t\tfor (int i = 0; i \u003C lane.Waypoints.Count; i\u002B\u002B)\n\t\t\t{\n\t\t\t\tfloat distance = lane.Waypoints[i].DistanceSquared(_Point);\n\n\t\t\t\tif (distance \u003E= bestDistance)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tbestDistance = distance;\n\t\t\t\tbestLane = lane;\n\n\t\t\t\t_Index = i;\n\t\t\t}\n\t\t}\n\n\t\treturn bestLane;\n\t}\n}\n"},{"Ident":"redsnail.roadtool","Path":"RoadManager/RoadVehicleDriver.cs","FileName":"RoadVehicleDriver.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// \u003Csummary\u003E\n/// A primitive control surface the traffic AI (\u003Csee cref=\u0022TrafficVehicle\u0022/\u003E) uses to drive ONE vehicle. It\u0027s a plain\n/// bag of delegates \u2014 there is no interface for a vehicle controller to implement, and your vehicle code never needs\n/// to reference this library.\n///\n/// The seam is filled in by whoever uses both this tool AND a vehicle controller \u2014 i.e. your GAME \u2014 via\n/// \u003Csee cref=\u0022RoadManager.ResolveVehicleDriver\u0022/\u003E. The game maps whatever its controller looks like onto these few\n/// delegates. Any field left null is simply skipped. The demo wires \u003Csee cref=\u0022DemoCarController\u0022/\u003E automatically.\n/// \u003C/summary\u003E\npublic sealed class RoadVehicleDriver\n{\n\t/// \u003Csummary\u003ETrue while a player is at the wheel \u2014 the AI then hands this car over for good and never reclaims it.\u003C/summary\u003E\n\tpublic Func\u003Cbool\u003E IsPlayerDriving;\n\n\t/// \u003Csummary\u003EThe vehicle body\u0027s world velocity. The brain reads it to chase a target speed and to detect being jammed.\u003C/summary\u003E\n\tpublic Func\u003CVector3\u003E Velocity;\n\n\t/// \u003Csummary\u003ETell the controller whether the AI is currently driving this vehicle (vs parked / player-driven). Pushed every frame.\u003C/summary\u003E\n\tpublic Action\u003Cbool\u003E SetAiControlled;\n\n\t/// \u003Csummary\u003EPush the AI\u0027s per-frame inputs: throttle and steer in [-1, 1] (steer \u002B = left), plus handbrake.\u003C/summary\u003E\n\tpublic Action\u003Cfloat, float, bool\u003E Drive;\n\n\t/// \u003Csummary\u003EOptional: max steering angle in degrees, used to widen the entity look-ahead toward where the car is turning.\u003C/summary\u003E\n\tpublic Func\u003Cfloat\u003E MaxSteering;\n}\n"},{"Ident":"redsnail.roadtool","Path":"Editor/RoadToolWindow/RoadToolWindow.Gizmo.cs","FileName":"RoadToolWindow.Gizmo.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":342326,"IsPrivate":false,"Code":"using Sandbox;\r\n\r\nnamespace RedSnail.RoadTool.Editor;\r\n\r\npublic partial class RoadToolWindow\r\n{\r\n\tprivate const float GIZMO_BOX_SIZE = 2.0f;\r\n\tprivate const float LINE_THICKNESS = 2.0f;\r\n\tprivate const float TANGENT_LINE_THICKNESS = 0.8f;\r\n\r\n\r\n\r\n\tprivate void DrawGizmos()\r\n\t{\r\n\t\tusing (Gizmo.Scope(\u0022road_editor\u0022, _targetComponent.WorldTransform))\r\n\t\t{\r\n\t\t\tDrawSplineSegments();\r\n\t\t\tDrawPositionGizmo();\r\n\t\t\tDrawPointControls();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawSplineSegments()\r\n\t{\r\n\t\t_targetComponent.Spline.ConvertToPolyline(ref _polyLine);\r\n\r\n\t\tfor (var i = 0; i \u003C _polyLine.Count - 1; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tDrawSegment(i, _polyLine[i], _polyLine[i \u002B 1]);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawSegment(int index, Vector3 start, Vector3 end)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\u0022segment\u0022 \u002B index))\r\n\t\tusing (Gizmo.Hitbox.LineScope())\r\n\t\t{\r\n\t\t\tGizmo.Draw.LineThickness = LINE_THICKNESS;\r\n\t\t\tGizmo.Hitbox.AddPotentialLine(start, end, LINE_THICKNESS * 2f);\r\n\t\t\tGizmo.Draw.Line(start, end);\r\n\r\n\t\t\tif (Gizmo.IsHovered \u0026\u0026 Gizmo.HasMouseFocus)\r\n\t\t\t{\r\n\t\t\t\tHandleSegmentHover(start, end);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandleSegmentHover(Vector3 start, Vector3 end)\r\n\t{\r\n\t\tGizmo.Draw.Color = Color.Cyan;\r\n\r\n\t\tif (!new Line(start, end).ClosestPoint(Gizmo.CurrentRay.ToLocal(Gizmo.Transform),\r\n\t\t\tout Vector3 pointOnLine, out _))\r\n\t\t\treturn;\r\n\r\n\t\tvar hoverSample = _targetComponent.Spline.SampleAtClosestPosition(pointOnLine);\r\n\t\tDrawHoverHandle(pointOnLine, hoverSample.Tangent);\r\n\r\n\t\tif (Gizmo.HasClicked \u0026\u0026 Gizmo.Pressed.This)\r\n\t\t{\r\n\t\t\tInsertPointAtHover(hoverSample.Distance);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawHoverHandle(Vector3 position, Vector3 tangent)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\u0022hover_handle\u0022, new Transform(position, Rotation.LookAt(tangent))))\r\n\t\tusing (Gizmo.GizmoControls.PushFixedScale())\r\n\t\t{\r\n\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void InsertPointAtHover(float distance)\r\n\t{\r\n\t\tusing (CreateUndoScope(\u0022Added spline point\u0022))\r\n\t\t{\r\n\t\t\tvar newPointIndex = _targetComponent.Spline.AddPointAtDistance(distance, true);\r\n\t\t\tSelectedPointIndex = newPointIndex;\r\n\t\t\t_inTangentSelected = false;\r\n\t\t\t_outTangentSelected = false;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPositionGizmo()\r\n\t{\r\n\t\t// The first point sits on the component origin, so its move handle lands right on top of the GameObject\u0027s\r\n\t\t// own transform gizmo \u2014 two move gizmos fighting over the same spot. Hide the point-body handle there and\r\n\t\t// let the object transform gizmo move the whole track instead. Tangent editing still gets its own gizmo.\r\n\t\tif (SelectedPointIndex == 0 \u0026\u0026 !_inTangentSelected \u0026\u0026 !_outTangentSelected)\r\n\t\t\treturn;\r\n\r\n\t\tvar gizmoPosition = CalculateGizmoPosition();\r\n\r\n\t\tif (!Gizmo.IsShiftPressed)\r\n\t\t{\r\n\t\t\t_draggingOutNewPoint = false;\r\n\t\t}\r\n\r\n\t\tusing (Gizmo.Scope(\u0022position\u0022, new Transform(gizmoPosition)))\r\n\t\t{\r\n\t\t\tHandlePositionControl();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate Vector3 CalculateGizmoPosition()\r\n\t{\r\n\t\tvar position = _selectedPoint.Position;\r\n\r\n\t\tif (_inTangentSelected)\r\n\t\t\tposition \u002B= _selectedPoint.In;\r\n\t\telse if (_outTangentSelected)\r\n\t\t\tposition \u002B= _selectedPoint.Out;\r\n\r\n\t\treturn position;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandlePositionControl()\r\n\t{\r\n\t\t_moveInProgress = false;\r\n\r\n\t\tif (Gizmo.Control.Position(\u0022spline_control_\u0022, Vector3.Zero, out var delta, GetHandleRotation()))\r\n\t\t{\r\n\t\t\t_moveInProgress = true;\r\n\t\t\t_movementUndoScope ??= CreateUndoScope(\u0022Moved spline point\u0022);\r\n\r\n\t\t\tif (_inTangentSelected)\r\n\t\t\t\tMoveSelectedPointInTangent(delta);\r\n\t\t\telse if (_outTangentSelected)\r\n\t\t\t\tMoveSelectedPointOutTangent(delta);\r\n\t\t\telse\r\n\t\t\t\tHandlePointMove(delta);\r\n\t\t}\r\n\r\n\t\tif (!_moveInProgress \u0026\u0026 Gizmo.WasLeftMouseReleased)\r\n\t\t{\r\n\t\t\t_movementUndoScope?.Dispose();\r\n\t\t\t_movementUndoScope = null;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Orientation for the move gizmo\u0027s arrows, honouring the editor\u0027s Global/Local space toggle\r\n\t/// (the same \u003Csee cref=\u0022Gizmo.Settings.GlobalSpace\u0022/\u003E button the object move tool uses).\r\n\t/// Global keeps the arrows world-aligned; Local aligns them to the selected point\u0027s tangent frame\r\n\t/// so a point can be dragged straight along the track. We are already inside the component-transform\r\n\t/// scope, so returning the point\u0027s \u003Cem\u003Elocal\u003C/em\u003E frame is what places the arrows on the world tangent \u2014\r\n\t/// and \u003Csee cref=\u0022Gizmo.Control.Position\u0022/\u003E hands the delta back in component-local space, matching how\r\n\t/// \u003Csee cref=\u0022MoveSelectedPoint\u0022/\u003E applies it.\r\n\t/// \u003C/summary\u003E\r\n\tprivate Rotation GetHandleRotation()\r\n\t{\r\n\t\tif (Gizmo.Settings.GlobalSpace)\r\n\t\t\treturn Rotation.Identity;\r\n\r\n\t\tif (!IsSelectedPointValid())\r\n\t\t\treturn Rotation.Identity;\r\n\r\n\t\tvar spline = _targetComponent.Spline;\r\n\t\tvar sample = spline.SampleAtDistance(spline.GetDistanceAtPoint(SelectedPointIndex));\r\n\r\n\t\tif (sample.Tangent.IsNearlyZero())\r\n\t\t\treturn Rotation.Identity;\r\n\r\n\t\tvar up = Rotation.FromAxis(sample.Tangent, sample.Roll) * sample.Up;\r\n\r\n\t\treturn Rotation.LookAt(sample.Tangent, up);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandlePointMove(Vector3 delta)\r\n\t{\r\n\t\tif (Gizmo.IsShiftPressed \u0026\u0026 !_draggingOutNewPoint)\r\n\t\t{\r\n\t\t\t_draggingOutNewPoint = true;\r\n\t\t\tvar currentPoint = _targetComponent.Spline.GetPoint(SelectedPointIndex);\r\n\t\t\t_targetComponent.Spline.InsertPoint(SelectedPointIndex \u002B 1, currentPoint);\r\n\t\t\tSelectedPointIndex\u002B\u002B;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tMoveSelectedPoint(delta);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPointControls()\r\n\t{\r\n\t\tvar spline = _targetComponent.Spline;\r\n\r\n\t\tfor (var i = 0; i \u003C spline.PointCount; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tif (spline.IsLoop \u0026\u0026 i == spline.SegmentCount)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar point = spline.GetPoint(i);\r\n\t\t\tDrawPointControl(i, point);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPointControl(int index, Spline.Point point)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\u0022point_controls\u0022 \u002B index, new Transform(point.Position)))\r\n\t\t{\r\n\t\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\t\tDrawPointPositionHandle(index);\r\n\r\n\t\t\tif (SelectedPointIndex == index)\r\n\t\t\t{\r\n\t\t\t\tDrawTangentHandles(point);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawPointPositionHandle(int index)\r\n\t{\r\n\t\tusing (Gizmo.Scope(\u0022position\u0022))\r\n\t\tusing (Gizmo.GizmoControls.PushFixedScale())\r\n\t\t{\r\n\t\t\tGizmo.Hitbox.DepthBias = 0.1f;\r\n\t\t\tGizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tbool isSelected = index == SelectedPointIndex \u0026\u0026 !_inTangentSelected \u0026\u0026 !_outTangentSelected;\r\n\r\n\t\t\tif (Gizmo.IsHovered || isSelected)\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Color = Color.Cyan;\r\n\t\t\t}\r\n\r\n\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tif (Gizmo.HasClicked \u0026\u0026 Gizmo.Pressed.This)\r\n\t\t\t{\r\n\t\t\t\tSelectPoint(index);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawTangentHandles(Spline.Point point)\r\n\t{\r\n\t\tGizmo.Draw.Color = Color.White;\r\n\t\tGizmo.Draw.LineThickness = TANGENT_LINE_THICKNESS;\r\n\r\n\t\tDrawTangentHandle(\u0022in_tangent\u0022, point.In, -point.In, ref _inTangentSelected, ref _outTangentSelected);\r\n\t\tDrawTangentHandle(\u0022out_tangent\u0022, point.Out, -point.Out, ref _outTangentSelected, ref _inTangentSelected);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawTangentHandle(string name, Vector3 offset, Vector3 lineStart, ref bool thisSelected, ref bool otherSelected)\r\n\t{\r\n\t\tusing (Gizmo.Scope(name, new Transform(offset)))\r\n\t\t{\r\n\t\t\tbool isMirroredOrAuto = _selectedPointTangentMode is HandleModeTemp.Mirrored or HandleModeTemp.Auto;\r\n\t\t\tif (isMirroredOrAuto \u0026\u0026 (thisSelected || otherSelected))\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Color = Color.Cyan;\r\n\t\t\t}\r\n\r\n\t\t\tGizmo.Draw.Line(lineStart, Vector3.Zero);\r\n\r\n\t\t\tif (_selectedPointTangentMode != HandleModeTemp.Linear)\r\n\t\t\t{\r\n\t\t\t\tDrawTangentBox(ref thisSelected, ref otherSelected);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DrawTangentBox(ref bool thisSelected, ref bool otherSelected)\r\n\t{\r\n\t\tusing (Gizmo.GizmoControls.PushFixedScale())\r\n\t\t{\r\n\t\t\tGizmo.Hitbox.DepthBias = 0.1f;\r\n\t\t\tGizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tif (Gizmo.IsHovered || thisSelected)\r\n\t\t\t{\r\n\t\t\t\tGizmo.Draw.Color = Color.Cyan;\r\n\t\t\t}\r\n\r\n\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GIZMO_BOX_SIZE));\r\n\r\n\t\t\tif (Gizmo.HasClicked \u0026\u0026 Gizmo.Pressed.This)\r\n\t\t\t{\r\n\t\t\t\tthisSelected = true;\r\n\t\t\t\totherSelected = false;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"Editor/RoadToolWindow/RoadToolWindow.UI.cs","FileName":"RoadToolWindow.UI.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":342326,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Editor;\r\n\r\nnamespace RedSnail.RoadTool.Editor;\r\n\r\npublic partial class RoadToolWindow\r\n{\r\n\tprivate const int HEADER_HEIGHT = 32;\r\n\r\n\r\n\r\n\tprivate void Rebuild()\r\n\t{\r\n\t\tLayout.Clear(true);\r\n\t\tLayout.Margin = 0;\r\n\t\tIcon = _isClosed ? \u0022\u0022 : \u0022route\u0022;\r\n\t\tUpdateWindowTitle();\r\n\t\tIsGrabbable = !_isClosed;\r\n\r\n\t\tif (_isClosed)\r\n\t\t{\r\n\t\t\tBuildClosedState();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tMinimumWidth = 400;\r\n\t\tBuildHeader();\r\n\r\n\t\tif (_targetComponent.IsValid())\r\n\t\t{\r\n\t\t\tBuildControlSheet();\r\n\t\t}\r\n\r\n\t\tLayout.Margin = 4;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildClosedState()\r\n\t{\r\n\t\tvar closedRow = Layout.AddRow();\r\n\r\n\t\tclosedRow.Add(new IconButton(\u0022route\u0022, () =\u003E { _isClosed = false; Rebuild(); })\r\n\t\t{\r\n\t\t\tToolTip = \u0022Open Spline Point Editor\u0022,\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\r\n\t\tMinimumWidth = 0;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildHeader()\r\n\t{\r\n\t\tvar headerRow = Layout.AddRow();\r\n\r\n\t\theaderRow.AddStretchCell();\r\n\r\n\t\theaderRow.Add(new IconButton(\u0022info\u0022)\r\n\t\t{\r\n\t\t\tToolTip = GetInfoTooltip(),\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\r\n\t\theaderRow.Add(new IconButton(\u0022close\u0022, CloseWindow)\r\n\t\t{\r\n\t\t\tToolTip = \u0022Close Editor\u0022,\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\t}\r\n\r\n\r\n\r\n\tprivate string GetInfoTooltip()\r\n\t{\r\n\t\treturn \u0022Controls to edit the spline points.\\n\u0022 \u002B\r\n\t\t\t   \u0022In addition to modifying the properties in the control sheet, you can also use the 3D Gizmos.\\n\u0022 \u002B\r\n\t\t\t   \u0022Clicking on the spline between points will split the spline at that position.\\n\u0022 \u002B\r\n\t\t\t   \u0022Holding shift while dragging a point\u0027s position will drag out a new point.\u0022;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildControlSheet()\r\n\t{\r\n\t\tvar serialized = this.GetSerialized();\r\n\t\tvar controlSheet = new ControlSheet();\r\n\r\n\t\t// Add property rows\r\n\t\tcontrolSheet.AddRow(serialized.GetProperty(nameof(_selectedPointTangentMode)));\r\n\t\tcontrolSheet.AddRow(serialized.GetProperty(nameof(_selectedPointPosition)));\r\n\t\t_inTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointIn)));\r\n\t\t_outTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointOut)));\r\n\r\n\t\t// Add advanced group\r\n\t\tvar roll = serialized.GetProperty(nameof(_selectedPointRoll));\r\n\t\tvar scale = serialized.GetProperty(nameof(_selectedPointScale));\r\n\t\tvar up = serialized.GetProperty(nameof(_selectedPointUp));\r\n\t\tcontrolSheet.AddGroup(\u0022Advanced\u0022, [roll, scale, up]);\r\n\r\n\t\t// Add control buttons\r\n\t\tcontrolSheet.AddLayout(BuildControlButtons());\r\n\r\n\t\tLayout.Add(controlSheet);\r\n\t\tToggleTangentInput();\r\n\t}\r\n\r\n\r\n\r\n\tprivate Layout BuildControlButtons()\r\n\t{\r\n\t\tvar row = Layout.Row();\r\n\t\trow.Spacing = 16;\r\n\t\trow.Margin = 8;\r\n\r\n\t\trow.Add(CreateNavigationButton(\u0022skip_previous\u0022, -1, \u0022Go to previous point\u0022));\r\n\t\trow.Add(CreateNavigationButton(\u0022skip_next\u0022, 1, \u0022Go to next point\u0022));\r\n\t\trow.Add(CreateDeleteButton());\r\n\t\trow.Add(CreateAddButton());\r\n\r\n\t\treturn row;\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateNavigationButton(string icon, int direction, string tooltip)\r\n\t{\r\n\t\treturn new IconButton(icon, () =\u003E\r\n\t\t{\r\n\t\t\tif (direction \u003C 0)\r\n\t\t\t\tSelectedPointIndex = int.Max(0, SelectedPointIndex - 1);\r\n\t\t\telse\r\n\t\t\t\tSelectedPointIndex = int.Min(_targetComponent.Spline.PointCount - 1, SelectedPointIndex \u002B 1);\r\n\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{ ToolTip = tooltip };\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateDeleteButton()\r\n\t{\r\n\t\treturn new IconButton(\u0022delete\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tusing (CreateUndoScope(\u0022Delete Spline Point\u0022))\r\n\t\t\t{\r\n\t\t\t\t_targetComponent.Spline.RemovePoint(SelectedPointIndex);\r\n\t\t\t\tSelectedPointIndex = int.Max(0, SelectedPointIndex - 1);\r\n\t\t\t}\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{ ToolTip = \u0022Delete point\u0022 };\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateAddButton()\r\n\t{\r\n\t\treturn new IconButton(\u0022add\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tusing (CreateUndoScope(\u0022Added Spline Point\u0022))\r\n\t\t\t{\r\n\t\t\t\tInsertNewPoint();\r\n\t\t\t}\r\n\t\t\tSelectedPointIndex\u002B\u002B;\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{\r\n\t\t\tToolTip = \u0022Insert point after current point.\\n\u0022 \u002B\r\n\t\t\t\t\t  \u0022You can also hold shift while dragging a point to create a new point.\u0022\r\n\t\t};\r\n\t}\r\n\r\n\r\n\r\n\tprivate void InsertNewPoint()\r\n\t{\r\n\t\tvar spline = _targetComponent.Spline;\r\n\r\n\t\tif (SelectedPointIndex == spline.PointCount - 1)\r\n\t\t{\r\n\t\t\tvar distance = spline.GetDistanceAtPoint(SelectedPointIndex);\r\n\t\t\tvar tangent = spline.SampleAtDistance(distance).Tangent;\r\n\t\t\tvar newPosition = _selectedPoint.Position \u002B tangent * 200;\r\n\r\n\t\t\tspline.InsertPoint(SelectedPointIndex \u002B 1, _selectedPoint with { Position = newPosition });\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar currentDist = spline.GetDistanceAtPoint(SelectedPointIndex);\r\n\t\t\tvar nextDist = spline.GetDistanceAtPoint(SelectedPointIndex \u002B 1);\r\n\t\t\tvar midDist = (currentDist \u002B nextDist) / 2;\r\n\r\n\t\t\tspline.AddPointAtDistance(midDist, true);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateWindowTitle()\r\n\t{\r\n\t\tWindowTitle = _isClosed ? \u0022\u0022 : $\u0022Spline Point [{SelectedPointIndex}] Editor - {_targetComponent?.GameObject?.Name ?? \u0022\u0022}\u0022;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CloseWindow()\r\n\t{\r\n\t\t_isClosed = true;\r\n\t\tRebuild();\r\n\t\tPosition = Parent.Size - 32;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"Code/RoadComponent/RoadComponent.Lampposts.cs","FileName":"RoadComponent.Lampposts.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\r\nusing System.Linq;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadComponent\r\n{\r\n\tprivate bool m_DoesLamppostsNeedRebuild = false;\r\n\r\n\t[Property, FeatureEnabled(\u0022Lampposts\u0022, Icon = \u0022light_mode\u0022, Tint = EditorTint.Red), Change] private bool HasLampposts { get; set; } = false;\r\n\t[Property, Feature(\u0022Lampposts\u0022)] public GameObject LamppostPrefab { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } }\r\n\t[Property, Feature(\u0022Lampposts\u0022), Range(50.0f, 2000.0f)] private float LamppostSpacing { get; set { field = value.Clamp(10.0f, 100000.0f); m_DoesLamppostsNeedRebuild = true; } } = 50.0f;\r\n\t[Property, Feature(\u0022Lampposts\u0022), Range(-200.0f, 200.0f)] private float LamppostOffsetFromSidewalk { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 10.0f;\r\n\t[Property, Feature(\u0022Lampposts\u0022), Range(0.0f, 10.0f)] private float LamppostHeightOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\t[Property, Feature(\u0022Lampposts\u0022)] private LamppostSide LamppostPlacement { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = LamppostSide.Both;\r\n\t[Property, Feature(\u0022Lampposts\u0022)] private bool AlignToSplineRotation { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = true;\r\n\t[Property(Title = \u0022Keep Vertical (World Up)\u0022), Feature(\u0022Lampposts\u0022)] private bool KeepVertical { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = true;\r\n\t[Property, Feature(\u0022Lampposts\u0022), Range(0.0f, 360.0f)] private float LamppostRotationOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\t[Property, Feature(\u0022Lampposts\u0022), Range(0.0f, 100.0f)] private float StartOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\t[Property, Feature(\u0022Lampposts\u0022), Range(0.0f, 100.0f)] private float EndOffset { get; set { field = value; m_DoesLamppostsNeedRebuild = true; } } = 0.0f;\r\n\r\n\tpublic enum LamppostSide\r\n\t{\r\n\t\tLeft,\r\n\t\tRight,\r\n\t\tBoth,\r\n\t\tAlternating\r\n\t}\r\n\r\n\r\n\r\n\tprivate void OnHasLamppostsChanged(bool _OldValue, bool _NewValue)\r\n\t{\r\n\t\tm_DoesLamppostsNeedRebuild = true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLampposts()\r\n\t{\r\n\t\tRemoveLampposts();\r\n\r\n\t\tif (!HasLampposts || !LamppostPrefab.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tBuildLampposts();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveLampposts()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = GameObject.Children.FirstOrDefault(x =\u003E x.Name == \u0022Lampposts\u0022);\r\n\r\n\t\tif (containerObject.IsValid())\r\n\t\t{\r\n\t\t\tcontainerObject.Destroy();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateLampposts()\r\n\t{\r\n\t\tif (m_DoesLamppostsNeedRebuild)\r\n\t\t{\r\n\t\t\tCreateLampposts();\r\n\r\n\t\t\tm_DoesLamppostsNeedRebuild = false;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildLampposts()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = new GameObject(GameObject, true, \u0022Lampposts\u0022);\r\n\t\tcontainerObject.Tags.Add(\u0022road_props\u0022);\r\n\r\n\t\tfloat splineLength = Spline.Length;\r\n\t\tfloat effectiveLength = splineLength - StartOffset - EndOffset;\r\n\r\n\t\tif (effectiveLength \u003C= 0)\r\n\t\t\treturn;\r\n\r\n\t\tGetSplineFrameData(out var frames, out var segmentsToKeep);\r\n\r\n\t\tvar simplifiedPositions = new List\u003C(Transform _Frame, float _Distance)\u003E();\r\n\r\n\t\tforeach (int index in segmentsToKeep)\r\n\t\t{\r\n\t\t\tfloat t = (float)index / (frames.Length - 1);\r\n\t\t\tfloat distance = t * splineLength;\r\n\r\n\t\t\tsimplifiedPositions.Add((frames[index], distance));\r\n\t\t}\r\n\r\n\t\tint lamppostCount = Math.Max(1, (int)MathF.Ceiling(effectiveLength / LamppostSpacing));\r\n\t\tint frameCount = lamppostCount \u002B 1;\r\n\r\n\t\tfloat roadEdgeOffset = RoadWidth * 0.5f;\r\n\t\tfloat sidewalkOffset = HasSidewalk ? SidewalkWidth : 0.0f;\r\n\t\tfloat totalOffset = roadEdgeOffset \u002B sidewalkOffset \u002B LamppostOffsetFromSidewalk;\r\n\r\n\t\tfor (int i = 0; i \u003C frameCount; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat t = (float)i / (frameCount - 1);\r\n\t\t\tfloat distance = t * splineLength;\r\n\r\n\t\t\t// Skip if outside the start/end offset range\r\n\t\t\tif (distance \u003C StartOffset || distance \u003E splineLength - EndOffset)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Interpolate frame at this distance along the simplified spline\r\n\t\t\tTransform frame = InterpolateFrameAtDistance(simplifiedPositions, distance);\r\n\r\n\t\t\tVector3 basePosition = frame.Position;\r\n\t\t\tVector3 forward = frame.Rotation.Forward;\r\n\t\t\tVector3 up = frame.Rotation.Up;\r\n\t\t\tVector3 right = frame.Rotation.Right;\r\n\r\n\t\t\tbool placeLeft = false;\r\n\t\t\tbool placeRight = false;\r\n\r\n\t\t\tswitch (LamppostPlacement)\r\n\t\t\t{\r\n\t\t\t\tcase LamppostSide.Left:\r\n\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase LamppostSide.Right:\r\n\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase LamppostSide.Both:\r\n\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase LamppostSide.Alternating:\r\n\t\t\t\t\tif (i % 2 == 0)\r\n\t\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tdefault:\r\n\t\t\t\t\tplaceLeft = true;\r\n\t\t\t\t\tplaceRight = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\r\n\t\t\tif (placeLeft)\r\n\t\t\t{\r\n\t\t\t\tVector3 leftPosition = basePosition - right * totalOffset \u002B up * (LamppostHeightOffset \u002B (HasSidewalk ? SidewalkHeight : 0.0f));\r\n\t\t\t\tRotation leftRotation = CalculateLamppostRotation(forward, up, LamppostRotationOffset);\r\n\r\n\t\t\t\tCreateLamppost(containerObject, leftPosition, leftRotation);\r\n\t\t\t}\r\n\r\n\t\t\tif (placeRight)\r\n\t\t\t{\r\n\t\t\t\tVector3 rightPosition = basePosition \u002B right * totalOffset \u002B up * (LamppostHeightOffset \u002B (HasSidewalk ? SidewalkHeight : 0.0f));\r\n\t\t\t\tRotation rightRotation = CalculateLamppostRotation(forward, up, LamppostRotationOffset \u002B 180.0f);\r\n\r\n\t\t\t\tCreateLamppost(containerObject, rightPosition, rightRotation);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLamppost(GameObject _Parent, Vector3 _Position, Rotation _Rotation)\r\n\t{\r\n\t\tif (!LamppostPrefab.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tGameObject lamppostObject = LamppostPrefab.Clone(_Parent, _Position, _Rotation, Vector3.One);\r\n\r\n\t\tif (!lamppostObject.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tlamppostObject.LocalPosition = _Position;\r\n\t\tlamppostObject.LocalRotation = _Rotation;\r\n\t}\r\n\r\n\r\n\r\n\tprivate Rotation CalculateLamppostRotation(Vector3 _Forward, Vector3 _SplineUp, float _YawOffset)\r\n\t{\r\n\t\tif (!AlignToSplineRotation)\r\n\t\t\treturn Rotation.FromYaw(_YawOffset);\r\n\r\n\t\tRotation finalRotation;\r\n\r\n\t\tif (KeepVertical)\r\n\t\t{\r\n\t\t\tVector3 flatForward = _Forward.WithZ(0).Normal;\r\n\r\n\t\t\tif (flatForward.Length \u003E 0.001f)\r\n\t\t\t{\r\n\t\t\t\tfinalRotation = Rotation.LookAt(flatForward, Vector3.Up);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tfinalRotation = Rotation.FromYaw(_YawOffset);\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tfinalRotation = Rotation.LookAt(_Forward, _SplineUp);\r\n\t\t}\r\n\r\n\t\treturn finalRotation * Rotation.FromYaw(_YawOffset);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"Code/RoadComponent/RoadComponent.Lines.cs","FileName":"RoadComponent.Lines.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadComponent\r\n{\r\n\t[Property, FeatureEnabled(\u0022Lines\u0022, Icon = \u0022show_chart\u0022, Tint = EditorTint.Yellow), Change] private bool HasLines { get; set; } = false;\r\n\t[Property(Title = \u0022Lines\u0022), Feature(\u0022Lines\u0022)] public RoadLineDefinition[] LineDefinitions { get; set { field = value; IsDirty = true; } }\r\n\t[Property(Title = \u0022Offset\u0022), Feature(\u0022Lines\u0022), Range(0.01f, 1.0f)] private float LinesOffset { get; set { field = value; IsDirty = true; } } = 0.1f;\r\n\t[Property(Title = \u0022Width\u0022), Feature(\u0022Lines\u0022), Range(1.0f, 50.0f)] private float LinesWidth { get; set { field = value; IsDirty = true; } } = 5.0f;\r\n\t[Property(Title = \u0022Extra Spacing\u0022), Feature(\u0022Lines\u0022), Range(0.0f, 1000.0f)] private float LinesExtraSpacing { get; set { field = value; IsDirty = true; } } = 0.0f;\r\n\t[Property(Title = \u0022Texture Repeat\u0022), Feature(\u0022Lines\u0022)] private float LinesTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 10.0f;\r\n\r\n\r\n\r\n\tprivate void OnHasLinesChanged(bool _OldValue, bool _NewValue)\r\n\t{\r\n\t\tIsDirty = true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLines()\r\n\t{\r\n\t\tEnsureLinesMeshExist();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateLines()\r\n\t{\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveLines()\r\n\t{\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureLinesMeshExist()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (HasGeneratedMeshChildren(LineSurfaceTag))\r\n\t\t\treturn;\r\n\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildLinesMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildLinesMesh()\r\n\t{\r\n\t\tif (!HasLines || LineDefinitions == null || LineDefinitions.Length == 0)\r\n\t\t\treturn;\r\n\r\n\t\tGetSplineFrameData(out var frames, out var segmentsToKeep);\r\n\r\n\t\tint finalSegmentCount = segmentsToKeep.Count - 1;\r\n\r\n\t\tif (finalSegmentCount \u003C= 0)\r\n\t\t\treturn;\r\n\r\n\t\tfloat roadWidth = RoadWidth \u002B LinesExtraSpacing;\r\n\t\tfloat lineSpacing = roadWidth / (LineDefinitions.Length \u002B 1);\r\n\r\n\t\tvar polygonMeshes = new PolygonMesh[LineDefinitions.Length];\r\n\t\tfor (int i = 0; i \u003C LineDefinitions.Length; i\u002B\u002B)\r\n\t\t\tpolygonMeshes[i] = new PolygonMesh();\r\n\r\n\t\tfloat[] lineDistances = new float[LineDefinitions.Length];\r\n\r\n\t\tfor (int i = 0; i \u003C finalSegmentCount; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tint idx0 = segmentsToKeep[i];\r\n\t\t\tint idx1 = segmentsToKeep[i \u002B 1];\r\n\r\n\t\t\tTransform f0 = frames[idx0];\r\n\t\t\tTransform f1 = frames[idx1];\r\n\r\n\t\t\tVector3 p0 = f0.Position;\r\n\t\t\tVector3 p1 = f1.Position;\r\n\r\n\t\t\tVector3 right0 = f0.Rotation.Right;\r\n\r\n\t\t\tfor (int line = 0; line \u003C LineDefinitions.Length; line\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tfloat offsetFromCenter = ((line \u002B 1) * lineSpacing) - (roadWidth * 0.5f);\r\n\r\n\t\t\t\tVector3 center0 =\r\n\t\t\t\t\tp0 \u002B\r\n\t\t\t\t\tf0.Rotation.Right * offsetFromCenter \u002B\r\n\t\t\t\t\tf0.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tVector3 center1 =\r\n\t\t\t\t\tp1 \u002B\r\n\t\t\t\t\tf1.Rotation.Right * offsetFromCenter \u002B\r\n\t\t\t\t\tf1.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tfloat segmentLength = Vector3.DistanceBetween(center0, center1);\r\n\t\t\t\tVector3 dir = (center1 - center0).Normal;\r\n\r\n\t\t\t\tfloat remaining = segmentLength;\r\n\t\t\t\tVector3 curCenter = center0;\r\n\r\n\t\t\t\tfloat dashSpacing = LineDefinitions[line]?.DashSpacing ?? 0.0f;\r\n\t\t\t\tfloat dashFillRatio = LineDefinitions[line]?.DashFillRatio ?? 1.0f;\r\n\t\t\t\tfloat dashLength = dashSpacing * dashFillRatio;\r\n\t\t\t\tfloat halfWidth = LinesWidth * 0.5f;\r\n\r\n\t\t\t\tvar polygonMesh = polygonMeshes[line];\r\n\t\t\t\tvar material = LineDefinitions[line]?.Material ?? Material.Load(\u0022materials/default.vmat\u0022);\r\n\r\n\t\t\t\twhile (remaining \u003E 0.001f)\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat linePos = lineDistances[line];\r\n\t\t\t\t\tfloat cyclePos = dashSpacing \u003E 0 ? linePos % dashSpacing : 0;\r\n\r\n\t\t\t\t\tif (cyclePos \u003C 0.0001f)\r\n\t\t\t\t\t\tcyclePos = 0.0f;\r\n\r\n\t\t\t\t\tif (dashSpacing \u003E 0 \u0026\u0026 dashSpacing - cyclePos \u003C 0.0001f)\r\n\t\t\t\t\t\tcyclePos = dashSpacing;\r\n\r\n\t\t\t\t\tbool inDash = dashSpacing \u003C= 0 || cyclePos \u003C= dashLength - 0.0001f;\r\n\r\n\t\t\t\t\tfloat step;\r\n\r\n\t\t\t\t\tif (dashSpacing \u003C= 0)\r\n\t\t\t\t\t\tstep = remaining;\r\n\t\t\t\t\telse if (inDash)\r\n\t\t\t\t\t\tstep = dashLength - cyclePos;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tstep = dashSpacing - cyclePos;\r\n\r\n\t\t\t\t\tstep = Math.Max(step, 0.01f);\r\n\t\t\t\t\tstep = Math.Min(step, remaining);\r\n\r\n\t\t\t\t\tVector3 nextCenter = curCenter \u002B dir * step;\r\n\r\n\t\t\t\t\tif (inDash)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tVector3 l0 = curCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r0 = curCenter \u002B right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 l1 = nextCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r1 = nextCenter \u002B right0 * halfWidth;\r\n\r\n\t\t\t\t\t\tfloat v0 = linePos / LinesTextureRepeat;\r\n\t\t\t\t\t\tfloat v1 = (linePos \u002B step) / LinesTextureRepeat;\r\n\r\n\t\t\t\t\t\tvar verts = polygonMesh.AddVertices(l0, r0, r1, l1);\r\n\t\t\t\t\t\tMeshUtility.AddTexturedQuad(polygonMesh, material, verts[0], verts[1], verts[2], verts[3],\r\n\t\t\t\t\t\t\tnew Vector2(0, v0), new Vector2(1, v0),\r\n\t\t\t\t\t\t\tnew Vector2(1, v1), new Vector2(0, v1));\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tlineDistances[line] \u002B= step;\r\n\t\t\t\t\tcurCenter = nextCenter;\r\n\t\t\t\t\tremaining -= step;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor (int line = 0; line \u003C LineDefinitions.Length; line\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar child = new GameObject(GameObject, true, $\u0022Line_{line}\u0022);\r\n\t\t\tchild.Tags.Add(LineSurfaceTag);\r\n\r\n\t\t\tvar meshComponent = child.AddComponent\u003CMeshComponent\u003E();\r\n\t\t\tmeshComponent.Mesh = polygonMeshes[line];\r\n\t\t\tmeshComponent.Collision = MeshComponent.CollisionType.None;\r\n\t\t\tmeshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;\r\n\t\t\tmeshComponent.SmoothingAngle = 40.0f;\r\n\t\t\tmeshComponent.Static = true;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"Code/RoadParkingLotComponent/RoadParkingLotComponent.cs","FileName":"RoadParkingLotComponent.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Generates parking lot lines for parking spaces\r\n/// \u003C/summary\u003E\r\n[Icon(\u0022local_parking\u0022)]\r\npublic partial class RoadParkingLotComponent : Component, Component.ExecuteInEditor\r\n{\r\n\tprivate bool m_IsDirty;\r\n\r\n\tprivate const string LinesTag = \u0022parking_lines\u0022;\r\n\tprivate const string CurbsTag = \u0022parking_curbs\u0022;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// An optional prefab, if non-empty the parking lot will generate a bunch of child gameobjects positioned at each parking spots center.\r\n\t/// (e.g. this allows you to use a gameobject prefab with a car spawner system component attached to it)\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature(\u0022General\u0022, Icon = \u0022public\u0022, Tint = EditorTint.White)] private GameObject SpotPrefab { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The amount of parking spots you want to generate.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature(\u0022General\u0022), Range(1, 50)] private int SpotCount { get; set { field = value; m_IsDirty = true; } } = 10;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Well that\u0027s the parking spot length\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature(\u0022General\u0022), Range(10.0f, 1000.0f)] private float SpotLength { get; set { field = value; m_IsDirty = true; } } = 250.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// and width...\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature(\u0022General\u0022), Range(10.0f, 1000.0f)] private float SpotWidth { get; set { field = value; m_IsDirty = true; } } = 150.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The angle of the parking spots in degrees (0 = perpendicular, 45 = angled, 90 = parallel)\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature(\u0022General\u0022), Range(-90.0f, 90.0f), Step(1.0f)] private float SpotAngle { get; set { field = value; m_IsDirty = true; } } = 0.0f;\r\n\t[Property, Feature(\u0022General\u0022), Range(0.5f, 1.0f)] private float SpotAngleThreshold { get; set { field = value; m_IsDirty = true; } } = 0.5f;\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tBuildAllMeshes();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tDestroyMeshChildren();\r\n\t\tRemoveParkingSpots();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif (m_IsDirty)\r\n\t\t{\r\n\t\t\tif (!SandboxUtility.IsInPlayMode)\r\n\t\t\t{\r\n\t\t\t\tDestroyMeshChildren();\r\n\t\t\t\tBuildAllMeshes();\r\n\t\t\t}\r\n\r\n\t\t\tm_IsDirty = false;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void DestroyMeshChildren()\r\n\t{\r\n\t\tvar toRemove = GameObject.Children\r\n\t\t\t.Where(c =\u003E c.Tags.Has(LinesTag) || c.Tags.Has(CurbsTag))\r\n\t\t\t.ToList();\r\n\r\n\t\tforeach (var child in toRemove)\r\n\t\t\tchild.Destroy();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildAllMeshes()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tBuildParkingLines();\r\n\t\tBuildCurbs();\r\n\r\n\t\tRemoveParkingSpots();\r\n\t\tCreateParkingSpots();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif (!Gizmo.IsSelected)\r\n\t\t\treturn;\r\n\r\n\t\tGizmo.Draw.LineThickness = 2.0f;\r\n\t\tGizmo.Draw.Color = Color.Green.WithAlpha(0.5f);\r\n\r\n\t\tfloat angleRad = SpotAngle.DegreeToRadian();\r\n\t\tfloat sinAngle = float.Sin(angleRad);\r\n\t\tfloat cosAngle = float.Cos(angleRad);\r\n\r\n\t\tfloat spacing = CalculateSpacing();\r\n\r\n\t\t// Draw parking spot outlines\r\n\t\tfor (int i = 0; i \u003C SpotCount; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat xPos = i * spacing;\r\n\r\n\t\t\tVector3 frontLeft = new Vector3(xPos, 0, LinesOffset);\r\n\t\t\tVector3 frontRight = new Vector3(xPos \u002B SpotWidth * cosAngle, SpotWidth * sinAngle, LinesOffset);\r\n\t\t\tVector3 backLeft = new Vector3(xPos - SpotLength * sinAngle, SpotLength * cosAngle, LinesOffset);\r\n\t\t\tVector3 backRight = new Vector3(xPos \u002B SpotWidth * cosAngle - SpotLength * sinAngle, SpotWidth * sinAngle \u002B SpotLength * cosAngle, LinesOffset);\r\n\r\n\t\t\tGizmo.Draw.Line(frontLeft, frontRight);\r\n\t\t\tGizmo.Draw.Line(frontRight, backRight);\r\n\t\t\tGizmo.Draw.Line(backRight, backLeft);\r\n\t\t\tGizmo.Draw.Line(backLeft, frontLeft);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateParkingSpots()\r\n\t{\r\n\t\t// If we\u0027re in play mode, do not build (Since they\u0027re already saved in the scene file)\r\n\t\tif (LoadingScreen.IsVisible || Game.IsPlaying)\r\n\t\t\treturn;\r\n\r\n\t\tif (!SpotPrefab.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = GameObject.Children.FirstOrDefault(x =\u003E x.Name == \u0022ParkingSpots\u0022);\r\n\r\n\t\tif (!containerObject.IsValid())\r\n\t\t\tcontainerObject = new GameObject(GameObject, true, \u0022ParkingSpots\u0022);\r\n\r\n\t\tfloat angleRad = SpotAngle.DegreeToRadian();\r\n\t\tfloat sinAngle = float.Sin(angleRad);\r\n\t\tfloat cosAngle = float.Cos(angleRad);\r\n\r\n\t\tfloat spacing = CalculateSpacing();\r\n\r\n\t\tfor (int i = 0; i \u003C SpotCount; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat xPos = i * spacing;\r\n\r\n\t\t\tfloat centerX = xPos \u002B (SpotWidth * 0.5f * cosAngle) - (SpotLength * 0.5f * sinAngle);\r\n\t\t\tfloat centerY = (SpotWidth * 0.5f * sinAngle) \u002B (SpotLength * 0.5f * cosAngle);\r\n\r\n\t\t\tVector3 position = new Vector3(centerX, centerY, 0);\r\n\r\n\t\t\tGameObject gameObject = SpotPrefab.Clone(new Transform(), containerObject);\r\n\t\t\tgameObject.LocalPosition = position;\r\n\t\t\tgameObject.LocalRotation = Rotation.FromYaw(SpotAngle);\r\n\t\t\tgameObject.NetworkMode = NetworkMode.Object;\r\n\t\t\tgameObject.Network.SetOrphanedMode(NetworkOrphaned.Host);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveParkingSpots()\r\n\t{\r\n\t\t// If we\u0027re in play mode, do not remove (Since they\u0027re already saved in the scene file)\r\n\t\tif (LoadingScreen.IsVisible || Game.IsPlaying)\r\n\t\t\treturn;\r\n\r\n\t\tGameObject containerObject = GameObject.Children.FirstOrDefault(x =\u003E x.Name == \u0022ParkingSpots\u0022);\r\n\r\n\t\tif (!containerObject.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tforeach (var gameObject in containerObject.Children.Where(x =\u003E x.IsValid()))\r\n\t\t{\r\n\t\t\tgameObject.Destroy();\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Utility button to directly snap the parking lot to the nearest solid ground\r\n\t/// \u003C/summary\u003E\r\n\t[Button(\u0022Snap to Ground\u0022), Feature(\u0022General\u0022), Order(100)]\r\n\tpublic void SnapToGround()\r\n\t{\r\n\t\tSceneTraceResult trace = Scene.Trace.Ray(WorldPosition, WorldPosition \u002B Vector3.Down * 10000.0f).Run();\r\n\r\n\t\tif (trace.Distance \u003C 0.1f) // Ignore really close hits, bcs that mean the parking lot is already properly grounded\r\n\t\t\treturn;\r\n\r\n\t\tif (!trace.Hit)\r\n\t\t\treturn;\r\n\r\n\t\tWorldPosition = trace.HitPosition;\r\n\t\tWorldRotation = Rotation.LookAt(trace.Normal, Vector3.Up) * Rotation.FromPitch(90.0f);\r\n\t}\r\n\r\n\r\n\r\n\tprivate float CalculateSpacing()\r\n\t{\r\n\t\tfloat angleRad = SpotAngle.DegreeToRadian();\r\n\t\tfloat cosAngle = float.Cos(angleRad);\r\n\r\n\t\treturn SpotWidth / float.Max(cosAngle, SpotAngleThreshold);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"Code/Utility/VehicleSpawnResource.cs","FileName":"VehicleSpawnResource.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n[AssetType(Name = \u0022Vehicle Spawn\u0022, Extension = \u0022vspawn\u0022, Category = \u0022Road Tool\u0022)]\npublic sealed class VehicleSpawnResource : GameResource\n{\n\t[Property] public GameObject Prefab { get; set; }\n\n\t[Property, Range(0.0f, 10.0f)] public float Probability { get; set; } = 1.0f;\n\t\n\t\n\t\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\n\t{\n\t\treturn CreateSimpleAssetTypeIcon(\u0022car_rental\u0022, _Width, _Height, \u0022#00ccff\u0022, \u0022black\u0022);\n\t}\n}\n"},{"Ident":"redsnail.roadtool","Path":"ISplineComponent.cs","FileName":"ISplineComponent.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// \u003Csummary\u003E\n/// A component that owns an editable \u003Csee cref=\u0022Sandbox.Spline\u0022/\u003E. Lets the spline editor tool/window drive any of\n/// them (roads, rails, \u2026) without being tied to a single component type.\n/// \u003C/summary\u003E\npublic interface ISplineComponent : IValid\n{\n\tSpline Spline { get; }\n\tTransform WorldTransform { get; }\n\tGameObject GameObject { get; }\n}\n"},{"Ident":"redsnail.roadtool","Path":"RoadComponent/RoadComponent.cs","FileName":"RoadComponent.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Represents a road component that can be manipulated within the editor and at runtime.\r\n/// \u003C/summary\u003E\r\n[Icon(\u0022signpost\u0022)]\r\npublic partial class RoadComponent : Component, Component.ExecuteInEditor, Component.IHasBounds, ISplineComponent\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Undo/redo and prefab reloads deserialize a brand new \u003Csee cref=\u0022Sandbox.Spline\u0022/\u003E into this component\r\n\t/// instead of mutating the existing one, so the setter has to move our change listener over. Otherwise the\r\n\t/// subscription stays on the discarded instance and later edits never mark the mesh dirty again.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature(\u0022General\u0022), Hide]\r\n\tpublic Spline Spline\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\tSubscribeToSpline();\r\n\t\t\tUpdateData();\r\n\t\t}\r\n\t} = new();\r\n\r\n\tprivate Spline m_SubscribedSpline;\r\n\r\n\tprivate bool m_DoesRoadMeshNeedRebuild;\r\n\r\n\tprivate const string RoadMeshTag = \u0022road_mesh\u0022;\r\n\tprivate const string RoadSurfaceTag = \u0022road_surface\u0022;\r\n\tprivate const string SidewalkSurfaceTag = \u0022road_sidewalk\u0022;\r\n\tprivate const string LineSurfaceTag = \u0022road_lines\u0022;\r\n\r\n\t[Property, Feature(\u0022General\u0022, Icon = \u0022public\u0022, Tint = EditorTint.White), Category(\u0022Optimization\u0022)] private bool AutoSimplify { get; set { field = value; IsDirty = true; } } = false;\r\n\t[Property, Feature(\u0022General\u0022), Category(\u0022Optimization\u0022), Range(0.1f, 10.0f)] private float StraightThreshold { get; set { field = value; IsDirty = true; } } = 1.0f; // Degrees - how straight before merging\r\n\t[Property, Feature(\u0022General\u0022), Category(\u0022Optimization\u0022), Range(2, 50)] private int MinSegmentsToMerge { get; set { field = value; IsDirty = true; } } = 3; // Minimum consecutive straight segments before merging\r\n\r\n\t[Property, Feature(\u0022General\u0022), Category(\u0022Miscellaneous\u0022)] public bool UseRotationMinimizingFrames { get; set { field = value; IsDirty = true; } }\r\n\r\n\t[Property, FeatureEnabled(\u0022Bridge\u0022, Icon = \u0022architecture\u0022, Tint = EditorTint.Blue)]\r\n\tprivate bool HasBridge { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = false;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// This will prevent the bridge from being rebuilt if any property is edited or if the road component get disable and re-enabled.\r\n\t/// Really useful if you plan to edit the mesh with the mapping tool so you don\u0027t accidently erase/rebuild the bridge.\r\n\t/// \u003C/summary\u003E\r\n\t[Property(Title = \u0022\uD83D\uDD12 Locked\u0022), Feature(\u0022Bridge\u0022)]\r\n\tprivate bool IsLocked { get; set; } = false;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// This is your bridge material you wanna use.\r\n\t/// I recommend using a tileable texture for better result.\r\n\t/// \u003C/summary\u003E\r\n\t[Property(Title = \u0022Material\u0022), Feature(\u0022Bridge\u0022), Group(\u0022Texturing\u0022), Order(1)]\r\n\tprivate Material BridgeMaterial { get; set { field = value; m_DoesBridgeNeedRebuild = true; } }\r\n\r\n\t[Property(Title = \u0022Texture Repeat\u0022), Feature(\u0022Bridge\u0022), Group(\u0022Texturing\u0022), Order(1), Step(1)]\r\n\tprivate float BridgeTextureRepeat { get; set { field = value.Clamp(10.0f, 10000.0f); m_DoesBridgeNeedRebuild = true; } } = 500.0f;\r\n\r\n\t[Property(Title = \u0022Border Width\u0022), Feature(\u0022Bridge\u0022), Group(\u0022Shape\u0022), Order(0), Range(10.0f, 500.0f)]\r\n\tprivate float BridgeBorderWidth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;\r\n\r\n\t[Property(Title = \u0022Border Height\u0022), Feature(\u0022Bridge\u0022), Group(\u0022Shape\u0022), Range(10.0f, 500.0f)]\r\n\tprivate float BridgeBorderHeight { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;\r\n\r\n\t[Property(Title = \u0022Bottom Depth\u0022), Feature(\u0022Bridge\u0022), Group(\u0022Shape\u0022), Range(10.0f, 500.0f)]\r\n\tprivate float BridgeBottomDepth { get; set { field = value.Clamp(10.0f, 500.0f); m_DoesBridgeNeedRebuild = true; } } = 80.0f;\r\n\r\n\t[Property(Title = \u0022Close Caps\u0022), Feature(\u0022Bridge\u0022), Group(\u0022Shape\u0022)]\r\n\tprivate bool BridgeCloseCaps { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;\r\n\r\n\t[Property(Title = \u0022Pillars\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022), Order(2)]\r\n\tprivate bool Pillars { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;\r\n\r\n\t[Property(Title = \u0022Shape\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022)]\r\n\tprivate BridgePillarShape BridgePillarType { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = BridgePillarShape.Square;\r\n\r\n\t[Property(Title = \u0022Size\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022), Range(10.0f, 1000.0f)]\r\n\tprivate float BridgePillarSize { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 200.0f;\r\n\r\n\t[Property(Title = \u0022Height\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022), Range(10.0f, 5000.0f)]\r\n\tprivate float BridgePillarHeight { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 600.0f;\r\n\r\n\t[Property(Title = \u0022Spacing\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022), Range(100.0f, 10000.0f)]\r\n\tprivate float BridgePillarSpacing { get; set { field = value.Clamp(100.0f, 100000.0f); m_DoesBridgeNeedRebuild = true; } } = 1200.0f;\r\n\r\n\t[Property(Title = \u0022Inset\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022), Range(0.0f, 200.0f)]\r\n\tprivate float BridgePillarInset { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = 20.0f;\r\n\r\n\t[Property(Title = \u0022Segments\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022), Range(3, 24), ShowIf(nameof(BridgePillarType), BridgePillarShape.Cylinder)]\r\n\tprivate int BridgePillarRoundSegments { get; set { field = value.Clamp(3, 64); m_DoesBridgeNeedRebuild = true; } } = 12;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Does the pillars follow world up vector or follow the road shape ?\r\n\t/// \u003C/summary\u003E\r\n\t[Property(Title = \u0022Keep Vertical (World Up)\u0022), Feature(\u0022Bridge\u0022), ToggleGroup(\u0022Pillars\u0022)]\r\n\tprivate bool BridgePillarsKeepVertical { get; set { field = value; m_DoesBridgeNeedRebuild = true; } } = true;\r\n\t\r\n\t[Property, FeatureEnabled(\u0022Crosswalks\u0022, Icon = \u0022menu\u0022, Tint = EditorTint.Pink), Change] private bool HasCrosswalks { get; set; } = false;\r\n\t[Property(Title = \u0022Config\u0022), Feature(\u0022Crosswalks\u0022)] public CrosswalkConfig CrosswalkConfig { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } } = CrosswalkConfig.Both;\r\n\t[Property(Title = \u0022Decal Definition\u0022), Feature(\u0022Crosswalks\u0022)] public DecalDefinition CrosswalkDefinition { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } }\r\n\t[Property(Title = \u0022Decal Size\u0022), Feature(\u0022Crosswalks\u0022), Range(0.1f, 10.0f)] private Vector2 CrosswalkSize { get; set { field = value; m_DoesCrosswalksNeedsRebuild = true; } } = Vector2.One;\r\n\t\r\n\tprivate bool IsDirty\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\tm_DoesRoadMeshNeedRebuild = value;\r\n\t\t\tm_DoesLamppostsNeedRebuild = value;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic BBox LocalBounds =\u003E Spline.Bounds;\r\n\r\n\r\n\r\n\tpublic RoadComponent()\r\n\t{\r\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(0, 0, 0) });\r\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1000, 0, 0) });\r\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1600, 1000, 0) });\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tSubscribeToSpline();\r\n\r\n\t\tEnsureLanes(); // migrate/seed the lane layout before anything that reads it (lines mesh, traffic graph) runs\r\n\r\n\t\tEnsureRoadMeshExist();\r\n\t\tEnsureSidewalkMeshExist();\r\n\t\tEnsureBridgeMeshExist();\r\n\r\n\t\tCreateLines();\r\n\t\tCreateDecals();\r\n\t\tCreateLampposts();\r\n\t\tCreateCrosswalks();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tUnsubscribeFromSpline();\r\n\r\n\t\tRemoveRoadMesh();\r\n\t\tRemoveSidewalkMesh();\r\n\t\tRemoveLines();\r\n\t\tRemoveDecals();\r\n\t\tRemoveLampposts();\r\n\t\tRemoveCrosswalks();\r\n\t\tRemoveBridge();\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Undo/redo restores the serialized spline data in place, which does not raise\r\n\t/// \u003Csee cref=\u0022Sandbox.Spline.SplineChanged\u0022/\u003E. Rebuilding from here is what makes the mesh follow an undo.\r\n\t/// \u003C/summary\u003E\r\n\tprotected override void OnValidate()\r\n\t{\r\n\t\tSubscribeToSpline();\r\n\t\tUpdateData();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tSyncSplineSubscription();\r\n\r\n\t\tUpdateRoadMeshes();\r\n\t\tUpdateLines();\r\n\t\tUpdateDecals();\r\n\t\tUpdateLampposts();\r\n\t\tUpdateCrosswalks();\r\n\t\tUpdateBridge();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateRoadMeshes()\r\n\t{\r\n\t\tif (!m_DoesRoadMeshNeedRebuild)\r\n\t\t\treturn;\r\n\r\n\t\tRebuildRoadMesh();\r\n\t\tRebuildSidewalkMesh();\r\n\t\tRebuildLinesMesh();\r\n\r\n\t\tm_DoesRoadMeshNeedRebuild = false;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildRoadMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (IsRoadLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(RoadSurfaceTag);\r\n\t\tBuildRoadMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildSidewalkMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (IsSidewalkLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(SidewalkSurfaceTag);\r\n\t\tBuildSidewalkMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveRoadMesh()\r\n\t{\r\n\t\tif (IsRoadLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(RoadSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveSidewalkMesh()\r\n\t{\r\n\t\tif (IsSidewalkLocked)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(SidewalkSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveGeneratedMeshChildren(string _Tag)\r\n\t{\r\n\t\tvar toRemove = GameObject.Children.Where(child =\u003E child.Tags.Has(_Tag)).ToList();\r\n\r\n\t\tforeach (var child in toRemove)\r\n\t\t\tchild.Destroy();\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool HasGeneratedMeshChildren(string _Tag)\r\n\t{\r\n\t\treturn GameObject.Children.Any(child =\u003E child.Tags.Has(_Tag));\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Safety net for editor state changes that swap the spline instance without ever touching the\r\n\t/// property setter or \u003Csee cref=\u0022OnValidate\u0022/\u003E. A reference compare per frame is cheap enough to\r\n\t/// be worth never silently losing the subscription again.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void SyncSplineSubscription()\r\n\t{\r\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\r\n\t\t\treturn;\r\n\r\n\t\tSubscribeToSpline();\r\n\t\tUpdateData();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void SubscribeToSpline()\r\n\t{\r\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\r\n\t\t\treturn;\r\n\r\n\t\tUnsubscribeFromSpline();\r\n\r\n\t\tm_SubscribedSpline = Spline;\r\n\r\n\t\tif (m_SubscribedSpline is not null)\r\n\t\t\tm_SubscribedSpline.SplineChanged \u002B= UpdateData;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UnsubscribeFromSpline()\r\n\t{\r\n\t\tif (m_SubscribedSpline is null)\r\n\t\t\treturn;\r\n\r\n\t\tm_SubscribedSpline.SplineChanged -= UpdateData;\r\n\t\tm_SubscribedSpline = null;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateData()\r\n\t{\r\n\t\tif (!GameObject.IsValid() || !Scene.IsEditor)\r\n\t\t\treturn;\r\n\r\n\t\tIsDirty = true;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"RoadComponent/RoadComponent.Lines.cs","FileName":"RoadComponent.Lines.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\r\n\r\npublic partial class RoadComponent\r\n{\r\n\t[Property, FeatureEnabled(\u0022Lines\u0022, Icon = \u0022show_chart\u0022, Tint = EditorTint.Yellow), Change] private bool HasLines { get; set; } = false;\r\n\t[Property(Title = \u0022Lines\u0022), Feature(\u0022Lines\u0022)] public RoadLineDefinition[] LineDefinitions { get; set { field = value; IsDirty = true; } }\r\n\t[Property(Title = \u0022Offset\u0022), Feature(\u0022Lines\u0022), Range(0.01f, 1.0f)] private float LinesOffset { get; set { field = value; IsDirty = true; } } = 0.1f;\r\n\t[Property(Title = \u0022Width\u0022), Feature(\u0022Lines\u0022), Range(1.0f, 50.0f)] private float LinesWidth { get; set { field = value; IsDirty = true; } } = 5.0f;\r\n\t[Property(Title = \u0022Extra Spacing\u0022), Feature(\u0022Lines\u0022), Range(0.0f, 1000.0f)] private float LinesExtraSpacing { get; set { field = value; IsDirty = true; } } = 0.0f;\r\n\t[Property(Title = \u0022Texture Repeat\u0022), Feature(\u0022Lines\u0022)] private float LinesTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 10.0f;\r\n\r\n\r\n\r\n\tprivate void OnHasLinesChanged(bool _OldValue, bool _NewValue)\r\n\t{\r\n\t\tIsDirty = true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateLines()\r\n\t{\r\n\t\tEnsureLinesMeshExist();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateLines()\r\n\t{\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RemoveLines()\r\n\t{\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureLinesMeshExist()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tif (HasGeneratedMeshChildren(LineSurfaceTag))\r\n\t\t\treturn;\r\n\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RebuildLinesMesh()\r\n\t{\r\n\t\tif (SandboxUtility.IsInPlayMode)\r\n\t\t\treturn;\r\n\r\n\t\tRemoveGeneratedMeshChildren(LineSurfaceTag);\r\n\t\tBuildLinesMesh();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildLinesMesh()\r\n\t{\r\n\t\tif (!HasLines || LineDefinitions == null || LineDefinitions.Length == 0)\r\n\t\t\treturn;\r\n\r\n\t\tGetSplineFrameData(out var frames, out var segmentsToKeep);\r\n\r\n\t\tint finalSegmentCount = segmentsToKeep.Count - 1;\r\n\r\n\t\tif (finalSegmentCount \u003C= 0)\r\n\t\t\treturn;\r\n\r\n\t\tfloat roadWidth = RoadWidth \u002B LinesExtraSpacing;\r\n\t\tfloat lineSpacing = roadWidth / (LineDefinitions.Length \u002B 1);\r\n\r\n\t\tvar polygonMeshes = new PolygonMesh[LineDefinitions.Length];\r\n\t\tfor (int i = 0; i \u003C LineDefinitions.Length; i\u002B\u002B)\r\n\t\t\tpolygonMeshes[i] = new PolygonMesh();\r\n\r\n\t\tfloat[] lineDistances = new float[LineDefinitions.Length];\r\n\r\n\t\tfor (int i = 0; i \u003C finalSegmentCount; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tint idx0 = segmentsToKeep[i];\r\n\t\t\tint idx1 = segmentsToKeep[i \u002B 1];\r\n\r\n\t\t\tTransform f0 = frames[idx0];\r\n\t\t\tTransform f1 = frames[idx1];\r\n\r\n\t\t\tVector3 p0 = f0.Position;\r\n\t\t\tVector3 p1 = f1.Position;\r\n\r\n\t\t\tVector3 right0 = f0.Rotation.Right;\r\n\r\n\t\t\tfor (int line = 0; line \u003C LineDefinitions.Length; line\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tfloat offsetFromCenter = ((line \u002B 1) * lineSpacing) - (roadWidth * 0.5f);\r\n\r\n\t\t\t\tVector3 center0 =\r\n\t\t\t\t\tp0 \u002B\r\n\t\t\t\t\tf0.Rotation.Right * offsetFromCenter \u002B\r\n\t\t\t\t\tf0.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tVector3 center1 =\r\n\t\t\t\t\tp1 \u002B\r\n\t\t\t\t\tf1.Rotation.Right * offsetFromCenter \u002B\r\n\t\t\t\t\tf1.Rotation.Up * LinesOffset;\r\n\r\n\t\t\t\tfloat segmentLength = Vector3.DistanceBetween(center0, center1);\r\n\t\t\t\tVector3 dir = (center1 - center0).Normal;\r\n\r\n\t\t\t\tfloat remaining = segmentLength;\r\n\t\t\t\tVector3 curCenter = center0;\r\n\r\n\t\t\t\tfloat dashSpacing = LineDefinitions[line]?.DashSpacing ?? 0.0f;\r\n\t\t\t\tfloat dashFillRatio = LineDefinitions[line]?.DashFillRatio ?? 1.0f;\r\n\t\t\t\tfloat dashLength = dashSpacing * dashFillRatio;\r\n\t\t\t\tfloat halfWidth = LinesWidth * 0.5f;\r\n\r\n\t\t\t\tvar polygonMesh = polygonMeshes[line];\r\n\t\t\t\tvar material = LineDefinitions[line]?.Material ?? Material.Load(\u0022materials/default.vmat\u0022);\r\n\r\n\t\t\t\twhile (remaining \u003E 0.001f)\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat linePos = lineDistances[line];\r\n\t\t\t\t\tfloat cyclePos = dashSpacing \u003E 0 ? linePos % dashSpacing : 0;\r\n\r\n\t\t\t\t\tif (cyclePos \u003C 0.0001f)\r\n\t\t\t\t\t\tcyclePos = 0.0f;\r\n\r\n\t\t\t\t\tif (dashSpacing \u003E 0 \u0026\u0026 dashSpacing - cyclePos \u003C 0.0001f)\r\n\t\t\t\t\t\tcyclePos = dashSpacing;\r\n\r\n\t\t\t\t\tbool inDash = dashSpacing \u003C= 0 || cyclePos \u003C= dashLength - 0.0001f;\r\n\r\n\t\t\t\t\tfloat step;\r\n\r\n\t\t\t\t\tif (dashSpacing \u003C= 0)\r\n\t\t\t\t\t\tstep = remaining;\r\n\t\t\t\t\telse if (inDash)\r\n\t\t\t\t\t\tstep = dashLength - cyclePos;\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tstep = dashSpacing - cyclePos;\r\n\r\n\t\t\t\t\tstep = Math.Max(step, 0.01f);\r\n\t\t\t\t\tstep = Math.Min(step, remaining);\r\n\r\n\t\t\t\t\tVector3 nextCenter = curCenter \u002B dir * step;\r\n\r\n\t\t\t\t\tif (inDash)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tVector3 l0 = curCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r0 = curCenter \u002B right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 l1 = nextCenter - right0 * halfWidth;\r\n\t\t\t\t\t\tVector3 r1 = nextCenter \u002B right0 * halfWidth;\r\n\r\n\t\t\t\t\t\tfloat v0 = linePos / LinesTextureRepeat;\r\n\t\t\t\t\t\tfloat v1 = (linePos \u002B step) / LinesTextureRepeat;\r\n\r\n\t\t\t\t\t\tvar verts = polygonMesh.AddVertices(l0, r0, r1, l1);\r\n\t\t\t\t\t\tMeshUtility.AddTexturedQuad(polygonMesh, material, verts[0], verts[1], verts[2], verts[3],\r\n\t\t\t\t\t\t\tnew Vector2(0, v0), new Vector2(1, v0),\r\n\t\t\t\t\t\t\tnew Vector2(1, v1), new Vector2(0, v1));\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tlineDistances[line] \u002B= step;\r\n\t\t\t\t\tcurCenter = nextCenter;\r\n\t\t\t\t\tremaining -= step;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor (int line = 0; line \u003C LineDefinitions.Length; line\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar child = new GameObject(GameObject, true, $\u0022Line_{line}\u0022);\r\n\t\t\tchild.Tags.Add(LineSurfaceTag);\r\n\r\n\t\t\tvar meshComponent = child.AddComponent\u003CMeshComponent\u003E();\r\n\t\t\tmeshComponent.Mesh = polygonMeshes[line];\r\n\t\t\tmeshComponent.Collision = MeshComponent.CollisionType.None;\r\n\t\t\tmeshComponent.RenderType = ModelRenderer.ShadowRenderType.Off;\r\n\t\t\tmeshComponent.SmoothingAngle = 40.0f;\r\n\t\t\tmeshComponent.Static = true;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.roadtool","Path":"Utility/MeshUtility.cs","FileName":"MeshUtility.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.RoadTool;\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\tpublic static void AddTexturedQuad(PolygonMesh _Mesh, Material _Material,\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\n\t\tHalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D,\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)\n\t{\n\t\tif (HasDuplicateVertex(_A, _B, _C, _D))\n\t\t\treturn;\n\n\t\tvar face = _Mesh.AddFace(_A, _B, _C, _D);\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn;\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\t}\r\n\r\n\r\n\r\n\tpublic static void AddTexturedTriangle(PolygonMesh _Mesh, Material _Material,\n\t\tHalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B,\n\t\tHalfEdgeMesh.VertexHandle _C,\n\t\tVector2 _UvA, Vector2 _UvB, Vector2 _UvC)\n\t{\n\t\tif (HasDuplicateVertex(_A, _B, _C))\n\t\t\treturn;\n\n\t\tvar face = _Mesh.AddFace(_A, _B, _C);\n\r\n\t\tif (!face.IsValid)\r\n\t\t\treturn;\r\n\r\n\t\t_Mesh.SetFaceMaterial(face, _Material);\n\t\t_Mesh.SetFaceTextureCoords(face, new List\u003CVector2\u003E { _UvA, _UvB, _UvC });\n\t}\n\n\n\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C)\n\t{\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _B.Equals(_C);\n\t}\n\n\n\n\tprivate static bool HasDuplicateVertex(HalfEdgeMesh.VertexHandle _A, HalfEdgeMesh.VertexHandle _B, HalfEdgeMesh.VertexHandle _C, HalfEdgeMesh.VertexHandle _D)\n\t{\n\t\treturn _A.Equals(_B) || _A.Equals(_C) || _A.Equals(_D) ||\n\t\t\t_B.Equals(_C) || _B.Equals(_D) ||\n\t\t\t_C.Equals(_D);\n\t}\n}\n"},{"Ident":"redsnail.roadtool","Path":"Code/RailComponent/RailComponent.Fishplates.cs","FileName":"RailComponent.Fishplates.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\npublic partial class RailComponent\n{\n\t[Property, FeatureEnabled(\u0022Fishplates\u0022, Icon = \u0022link\u0022, Tint = EditorTint.Green)] private bool HasFishplates { get; set { field = value; IsDirty = true; } } = true;\n\t[Property(Title = \u0022Material\u0022), Feature(\u0022Fishplates\u0022)] private Material FishplateMaterial { get; set { field = value; IsDirty = true; } }\n\n\t/// \u003Csummary\u003EDistance along the track between joints. Real rail comes in long sections, so this is much larger than the sleeper spacing.\u003C/summary\u003E\n\t[Property(Title = \u0022Spacing\u0022), Feature(\u0022Fishplates\u0022), Range(100.0f, 5000.0f)] private float FishplateSpacing { get; set { field = value; IsDirty = true; } } = 1200.0f;\n\n\t/// \u003Csummary\u003EPlate length along the track (it straddles the joint between two rail sections).\u003C/summary\u003E\n\t[Property(Title = \u0022Length\u0022), Feature(\u0022Fishplates\u0022), Range(10.0f, 500.0f)] private float FishplateLength { get; set { field = value; IsDirty = true; } } = 80.0f;\n\n\t/// \u003Csummary\u003EPlate height \u2014 sized to sit against the rail web, between the foot and the head.\u003C/summary\u003E\n\t[Property(Title = \u0022Height\u0022), Feature(\u0022Fishplates\u0022), Range(2.0f, 100.0f)] private float FishplateHeight { get; set { field = value; IsDirty = true; } } = 14.0f;\n\n\t/// \u003Csummary\u003EHow far the plate stands out from the rail web.\u003C/summary\u003E\n\t[Property(Title = \u0022Thickness\u0022), Feature(\u0022Fishplates\u0022), Range(1.0f, 50.0f)] private float FishplateThickness { get; set { field = value; IsDirty = true; } } = 6.0f;\n\n\t/// \u003Csummary\u003ERadius of the hexagonal bolt heads.\u003C/summary\u003E\n\t[Property(Title = \u0022Bolt Radius\u0022), Feature(\u0022Fishplates\u0022), Range(0.5f, 20.0f)] private float FishplateBoltRadius { get; set { field = value; IsDirty = true; } } = 3.5f;\n\n\t/// \u003Csummary\u003EHow far the bolt heads stand out from the plate.\u003C/summary\u003E\n\t[Property(Title = \u0022Bolt Depth\u0022), Feature(\u0022Fishplates\u0022), Range(0.5f, 20.0f)] private float FishplateBoltDepth { get; set { field = value; IsDirty = true; } } = 3.0f;\n\n\t[Property(Title = \u0022Texture Repeat\u0022), Feature(\u0022Fishplates\u0022)] private float FishplateTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); IsDirty = true; } } = 100.0f;\n\n\n\n\tprivate void BuildFishplates(Transform[] _Frames)\n\t{\n\t\tfloat length = Spline.Length;\n\n\t\tif (length \u003C= 0.0f)\n\t\t\treturn;\n\n\t\tfloat spacing = Math.Max(1.0f, FishplateSpacing);\n\n\t\tint count = (int)MathF.Floor(length / spacing);\n\n\t\tif (count \u003C 1)\n\t\t\treturn; // track is shorter than one rail section \u2014 no joints\n\n\t\tvar material = FishplateMaterial ?? Material.Load(\u0022materials/dev/reflectivity_50.vmat\u0022);\n\t\tvar polygonMesh = new PolygonMesh();\n\n\t\tfloat textureRepeat = Math.Max(1.0f, FishplateTextureRepeat);\n\t\tfloat baseHeight = HasSleepers ? SleeperHeight : 0.0f;\n\t\tfloat halfGauge = RailGauge * 0.5f;\n\n\t\tint placed = 0;\n\n\t\tfor (int s = 1; s \u003C= count; s\u002B\u002B)\n\t\t{\n\t\t\tfloat distance = s * spacing;\n\n\t\t\tif (distance \u003E= length)\n\t\t\t\tbreak; // don\u0027t drop a joint right on the end of the track\n\n\t\t\tvar frame = SampleFrameAtDistance(_Frames, distance, length);\n\n\t\t\t// One plate on the OUTER side of each rail (the outward sign flips the side for the left rail).\n\t\t\tAddFishplate(polygonMesh, material, frame, halfGauge, 1.0f, baseHeight, textureRepeat);\n\t\t\tAddFishplate(polygonMesh, material, frame, halfGauge, -1.0f, baseHeight, textureRepeat);\n\n\t\t\tplaced\u002B\u002B;\n\t\t}\n\n\t\tif (placed == 0)\n\t\t\treturn;\n\n\t\tCreateFishplateChild(\u0022Fishplates\u0022, polygonMesh);\n\t}\n\n\n\n\t/// \u003Csummary\u003EAdds one bolted joint plate against the outer face of a single rail\u0027s web.\u003C/summary\u003E\n\tprivate void AddFishplate(PolygonMesh _Mesh, Material _Material, Transform _Frame, float _HalfGauge, float _OutwardSign, float _BaseHeight, float _TextureRepeat)\n\t{\n\t\tVector3 origin = _Frame.Position;\n\t\tVector3 right = _Frame.Rotation.Right;\n\t\tVector3 forward = _Frame.Rotation.Forward;\n\t\tVector3 up = _Frame.Rotation.Up;\n\n\t\tVector3 outward = right * _OutwardSign; // points away from the track centre, toward the outer side of this rail\n\n\t\tfloat halfWeb = RailWidth * RailWebScale * 0.5f;\n\t\tfloat webMid = _BaseHeight \u002B RailHeight * 0.5f;\n\n\t\tfloat embed = MathF.Min(1.0f, halfWeb * 0.5f); // bite into the web so the hidden inner face never z-fights it\n\t\tfloat innerDist = halfWeb - embed;\n\t\tfloat outerDist = halfWeb \u002B FishplateThickness;\n\t\tfloat centreDist = (innerDist \u002B outerDist) * 0.5f;\n\t\tfloat halfOut = (outerDist - innerDist) * 0.5f;\n\n\t\tfloat halfLength = FishplateLength * 0.5f;\n\t\tfloat halfHeight = FishplateHeight * 0.5f;\n\n\t\tVector3 railCentre = origin \u002B right * (_HalfGauge * _OutwardSign);\n\t\tVector3 plateCentre = railCentre \u002B outward * centreDist \u002B up * webMid;\n\n\t\tAddBox(_Mesh, _Material, plateCentre, outward, forward, up, halfOut, halfLength, halfHeight, _TextureRepeat);\n\n\t\t// Two hex bolts standing out of the plate\u0027s outer face, spread along the joint.\n\t\tVector3 outerFace = plateCentre \u002B outward * halfOut;\n\t\tfloat boltOffset = FishplateLength * 0.25f;\n\n\t\tAddHexBolt(_Mesh, _Material, outerFace \u002B forward * boltOffset, outward, forward, FishplateBoltRadius, FishplateBoltDepth, _TextureRepeat);\n\t\tAddHexBolt(_Mesh, _Material, outerFace - forward * boltOffset, outward, forward, FishplateBoltRadius, FishplateBoltDepth, _TextureRepeat);\n\t}\n\n\n\n\t/// \u003Csummary\u003EA closed box built from three frame axes, textured with a uniform box (cube) projection.\u003C/summary\u003E\n\tprivate static void AddBox(PolygonMesh _Mesh, Material _Material, Vector3 _Centre, Vector3 _AxisR, Vector3 _AxisF, Vector3 _AxisU, float _HalfR, float _HalfF, float _HalfU, float _TextureRepeat)\n\t{\n\t\t// The faces below wind outward only for a right-handed basis. The outward axis flips for the left rail, which\n\t\t// would make it left-handed and flip every face to a backface \u2014 mirror one axis to keep it right-handed (the\n\t\t// box is symmetric, so this changes nothing but the winding).\n\t\tif (Vector3.Dot(Vector3.Cross(_AxisR, _AxisF), _AxisU) \u003C 0.0f)\n\t\t\t_AxisF = -_AxisF;\n\n\t\tVector3 Corner(float r, float f, float u) =\u003E _Centre \u002B _AxisR * r \u002B _AxisF * f \u002B _AxisU * u;\n\n\t\tvar c000 = Corner(-_HalfR, -_HalfF, -_HalfU);\n\t\tvar c100 = Corner(_HalfR, -_HalfF, -_HalfU);\n\t\tvar c110 = Corner(_HalfR, _HalfF, -_HalfU);\n\t\tvar c010 = Corner(-_HalfR, _HalfF, -_HalfU);\n\t\tvar c001 = Corner(-_HalfR, -_HalfF, _HalfU);\n\t\tvar c101 = Corner(_HalfR, -_HalfF, _HalfU);\n\t\tvar c111 = Corner(_HalfR, _HalfF, _HalfU);\n\t\tvar c011 = Corner(-_HalfR, _HalfF, _HalfU);\n\n\t\tfloat r0 = -_HalfR / _TextureRepeat, r1 = _HalfR / _TextureRepeat;\n\t\tfloat f0 = -_HalfF / _TextureRepeat, f1 = _HalfF / _TextureRepeat;\n\t\tfloat u0 = -_HalfU / _TextureRepeat, u1 = _HalfU / _TextureRepeat;\n\n\t\tAddQuad(_Mesh, _Material, c100, c110, c111, c101, new Vector2(f0, u0), new Vector2(f1, u0), new Vector2(f1, u1), new Vector2(f0, u1)); // \u002BR\n\t\tAddQuad(_Mesh, _Material, c010, c000, c001, c011, new Vector2(f1, u0), new Vector2(f0, u0), new Vector2(f0, u1), new Vector2(f1, u1)); // -R\n\t\tAddQuad(_Mesh, _Material, c110, c010, c011, c111, new Vector2(r1, u0), new Vector2(r0, u0), new Vector2(r0, u1), new Vector2(r1, u1)); // \u002BF\n\t\tAddQuad(_Mesh, _Material, c000, c100, c101, c001, new Vector2(r0, u0), new Vector2(r1, u0), new Vector2(r1, u1), new Vector2(r0, u1)); // -F\n\t\tAddQuad(_Mesh, _Material, c001, c101, c111, c011, new Vector2(r0, f0), new Vector2(r1, f0), new Vector2(r1, f1), new Vector2(r0, f1)); // \u002BU\n\t\tAddQuad(_Mesh, _Material, c000, c010, c110, c100, new Vector2(r0, f0), new Vector2(r0, f1), new Vector2(r1, f1), new Vector2(r1, f0)); // -U\n\t}\n\n\n\n\t/// \u003Csummary\u003EA hexagonal bolt head \u2014 a 6-sided prism standing out of the plate along \u003Cparamref name=\u0022_AxisOut\u0022/\u003E, capped on the outer end.\u003C/summary\u003E\n\tprivate static void AddHexBolt(PolygonMesh _Mesh, Material _Material, Vector3 _Base, Vector3 _AxisOut, Vector3 _Forward, float _Radius, float _Depth, float _TextureRepeat)\n\t{\n\t\tconst int sides = 6;\n\n\t\t// t2 completes a right-handed (out, t1, t2) basis so the winding below stays outward-facing on either rail.\n\t\tVector3 t1 = _Forward;\n\t\tVector3 t2 = Vector3.Cross(_AxisOut, t1);\n\n\t\tvar baseRing = new Vector3[sides];\n\t\tvar topRing = new Vector3[sides];\n\n\t\tfor (int k = 0; k \u003C sides; k\u002B\u002B)\n\t\t{\n\t\t\tfloat angle = MathF.PI * 2.0f * k / sides;\n\t\t\tVector3 radial = t1 * MathF.Cos(angle) \u002B t2 * MathF.Sin(angle);\n\n\t\t\tbaseRing[k] = _Base \u002B radial * _Radius;\n\t\t\ttopRing[k] = baseRing[k] \u002B _AxisOut * _Depth;\n\t\t}\n\n\t\tfloat edge = _Radius; // a regular hexagon\u0027s side length equals its radius\n\t\tfloat v1 = _Depth / _TextureRepeat;\n\n\t\tfor (int k = 0; k \u003C sides; k\u002B\u002B)\n\t\t{\n\t\t\tint next = (k \u002B 1) % sides;\n\t\t\tfloat u0 = edge * k / _TextureRepeat;\n\t\t\tfloat u1 = edge * (k \u002B 1) / _TextureRepeat;\n\n\t\t\tAddQuad(_Mesh, _Material, baseRing[k], baseRing[next], topRing[next], topRing[k],\n\t\t\t\tnew Vector2(u0, 0.0f), new Vector2(u1, 0.0f), new Vector2(u1, v1), new Vector2(u0, v1));\n\t\t}\n\n\t\tVector2 CapUv(Vector3 _P) =\u003E new Vector2(Vector3.Dot(_P - _Base, t1), Vector3.Dot(_P - _Base, t2)) / _TextureRepeat;\n\n\t\tfor (int k = 1; k \u003C sides - 1; k\u002B\u002B)\n\t\t{\n\t\t\tAddTri(_Mesh, _Material, topRing[0], topRing[k], topRing[k \u002B 1],\n\t\t\t\tCapUv(topRing[0]), CapUv(topRing[k]), CapUv(topRing[k \u002B 1]));\n\t\t}\n\t}\n\n\n\n\t// Own vertices per face so every plate edge and bolt facet stays hard.\n\tprivate static void AddQuad(PolygonMesh _Mesh, Material _Material, Vector3 _A, Vector3 _B, Vector3 _C, Vector3 _D, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)\n\t{\n\t\tvar v = _Mesh.AddVertices(_A, _B, _C, _D);\n\n\t\tMeshUtility.AddTexturedQuad(_Mesh, _Material, v[0], v[1], v[2], v[3], _UvA, _UvB, _UvC, _UvD);\n\t}\n\n\n\n\tprivate static void AddTri(PolygonMesh _Mesh, Material _Material, Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC)\n\t{\n\t\tvar v = _Mesh.AddVertices(_A, _B, _C);\n\n\t\tMeshUtility.AddTexturedTriangle(_Mesh, _Material, v[0], v[1], v[2], _UvA, _UvB, _UvC);\n\t}\n\n\n\n\tprivate void CreateFishplateChild(string _Name, PolygonMesh _PolygonMesh)\n\t{\n\t\tvar child = new GameObject(GameObject, true, _Name);\n\t\tchild.Tags.Add(RailMeshTag);\n\t\tchild.Tags.Add(FishplateSurfaceTag);\n\n\t\tvar meshComponent = child.AddComponent\u003CMeshComponent\u003E();\n\t\tmeshComponent.Mesh = _PolygonMesh;\n\t}\n}\n"},{"Ident":"redsnail.roadtool","Path":"Code/RailComponent/RailComponent.Utility.cs","FileName":"RailComponent.Utility.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\npublic partial class RailComponent\n{\n\tprivate static Transform[] CalculateTangentFramesUsingUpDir(Spline _Spline, int _FrameCount)\n\t{\n\t\tvar frames = new Transform[_FrameCount];\n\t\tfloat totalSplineLength = _Spline.Length;\n\n\t\tvar sample = _Spline.SampleAtDistance(0.0f);\n\t\tsample.Up = Vector3.Up;\n\n\t\t// Fall back to a different up if the tangent runs (nearly) straight up.\n\t\tif (MathF.Abs(Vector3.Dot(sample.Tangent, sample.Up)) \u003E 0.999f)\n\t\t\tsample.Up = Vector3.Right;\n\n\t\tfor (int i = 0; i \u003C _FrameCount; i\u002B\u002B)\n\t\t{\n\t\t\tfloat t = _FrameCount \u003E 1 ? (float)i / (_FrameCount - 1) : 0.0f;\n\t\t\tfloat distance = t * totalSplineLength;\n\n\t\t\tsample = _Spline.SampleAtDistance(distance);\n\n\t\t\tvar up = Rotation.FromAxis(sample.Tangent, sample.Roll) * sample.Up;\n\t\t\tRotation rotation = Rotation.LookAt(sample.Tangent, up);\n\n\t\t\tframes[i] = new Transform(sample.Position, rotation, sample.Scale);\n\t\t}\n\n\t\treturn frames;\n\t}\n\n\n\n\tprivate static Transform[] CalculateRotationMinimizingTangentFrames(Spline _Spline, int _FrameCount)\n\t{\n\t\tvar frames = new Transform[_FrameCount];\n\t\tfloat totalSplineLength = _Spline.Length;\n\n\t\tvar previousSample = _Spline.SampleAtDistance(0.0f);\n\t\tVector3 up = Vector3.Up;\n\n\t\tif (MathF.Abs(Vector3.Dot(previousSample.Tangent, up)) \u003E 0.999f)\n\t\t\tup = Vector3.Right;\n\n\t\tup = Rotation.FromAxis(previousSample.Tangent, previousSample.Roll) * up;\n\n\t\tframes[0] = new Transform(previousSample.Position, Rotation.LookAt(previousSample.Tangent, up), previousSample.Scale);\n\n\t\tfor (int i = 1; i \u003C _FrameCount; i\u002B\u002B)\n\t\t{\n\t\t\tfloat t = _FrameCount \u003E 1 ? (float)i / (_FrameCount - 1) : 0.0f;\n\t\t\tfloat distance = t * totalSplineLength;\n\n\t\t\tvar sample = _Spline.SampleAtDistance(distance);\n\n\t\t\t// Parallel-transport the up vector so the profile doesn\u0027t twist through 3D curves.\n\t\t\tup = GetRotationMinimizingNormal(previousSample.Position, previousSample.Tangent, up, sample.Position, sample.Tangent);\n\n\t\t\tfloat deltaRoll = sample.Roll - previousSample.Roll;\n\t\t\tup = Rotation.FromAxis(sample.Tangent, deltaRoll) * up;\n\n\t\t\tRotation rotation = Rotation.LookAt(sample.Tangent, up);\n\t\t\tframes[i] = new Transform(sample.Position, rotation, sample.Scale);\n\n\t\t\tpreviousSample = sample;\n\t\t}\n\n\t\treturn frames;\n\t}\n\n\n\n\tprivate static Vector3 GetRotationMinimizingNormal(Vector3 _PosA, Vector3 _TangentA, Vector3 _NormalA, Vector3 _PosB, Vector3 _TangentB)\n\t{\n\t\t// Source: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf\n\t\tVector3 v1 = _PosB - _PosA;\n\n\t\tfloat v1DotV1Half = Vector3.Dot(v1, v1) / 2.0f;\n\n\t\tif (v1DotV1Half \u003C= 0.0001f)\n\t\t\treturn _NormalA;\n\n\t\tfloat r1 = Vector3.Dot(v1, _NormalA) / v1DotV1Half;\n\t\tfloat r2 = Vector3.Dot(v1, _TangentA) / v1DotV1Half;\n\n\t\tVector3 nL = _NormalA - r1 * v1;\n\t\tVector3 tL = _TangentA - r2 * v1;\n\t\tVector3 v2 = _TangentB - tL;\n\n\t\tfloat r3 = Vector3.Dot(v2, nL) / Vector3.Dot(v2, v2);\n\n\t\treturn (nL - 2.0f * r3 * v2).Normal;\n\t}\n}\n"},{"Ident":"redsnail.roadtool","Path":"Code/RoadIntersectionComponent/RoadIntersectionComponent.Sidewalk.cs","FileName":"RoadIntersectionComponent.Sidewalk.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342326,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace RedSnail.RoadTool;\n\n/// \u003Csummary\u003E\n/// How much of a junction reaches the pedestrian graph. Geometry is never affected \u2014 the pavement mesh builds\n/// the same either way; this only decides what pedestrians are allowed to route over.\n///\n/// Ordered most-to-least deliberately, so the zero value is the harmless one: a component that somehow arrives\n/// without this field set behaves like every other junction rather than silently vanishing from the graph.\n/// \u003C/summary\u003E\npublic enum SidewalkGraphMode\n{\n\t/// \u003Csummary\u003EPavement round the corners and a crossing at every arm.\u003C/summary\u003E\n\tAll,\n\n\t/// \u003Csummary\u003E\n\t/// Corners only. The pavement stays joined all the way round and connects to every road that meets here,\n\t/// but there\u0027s nothing to step off the kerb onto \u2014 pedestrians walk round the junction instead of over it.\n\t/// \u003C/summary\u003E\n\tNoCrossing,\n\n\t/// \u003Csummary\u003E\n\t/// Nothing at all. The roads that meet here keep their own pavements; they just stop being connected\n\t/// THROUGH this junction, so expect their ends to show as dead ends.\n\t/// \u003C/summary\u003E\n\tNone\n}\n\n\n\npublic partial class RoadIntersectionComponent\n{\n\t/// \u003Csummary\u003E\n\t/// What this junction contributes to the pedestrian graph \u2014 the pedestrian counterpart of\n\t/// \u003Csee cref=\u0022ExcludeTraffic\u0022/\u003E.\n\t///\n\t/// \u003Csee cref=\u0022SidewalkGraphMode.None\u0022/\u003E for somewhere nobody should be walking round at all: a slip road, a\n\t/// service yard, a junction whose pavement exists only because the mesh needs an edge.\n\t/// \u003Csee cref=\u0022SidewalkGraphMode.NoCrossing\u0022/\u003E for somewhere they may walk past but not across \u2014 a forecourt\n\t/// or car park entrance, where the kerb should stay continuous and stepping into the vehicle route is the\n\t/// thing you\u0027re trying to prevent.\n\t/// \u003C/summary\u003E\n\t[Property, Feature(\u0022General\u0022), Category(\u0022Sidewalk\u0022), Order(3)] public SidewalkGraphMode SidewalkGraph { get; set; } = SidewalkGraphMode.All;\n\n\n\n\t/// \u003Csummary\u003E\n\t/// The walking line around this junction\u0027s pavement, in world space \u2014 a closed loop running down the\n\t/// middle of the sidewalk slab.\n\t///\n\t/// Built from the junction\u0027s OWN outline, which is the whole point: a rectangular intersection\u0027s pavement\n\t/// runs along its edges and turns at its corners, and approximating that with an arc around the centre\n\t/// bows the path out into the road at the middle of each side and cuts the corners off. A circle really is\n\t/// an arc, so it gets one.\n\t///\n\t/// The loop runs all the way round, arm mouths included \u2014 whoever consumes it is expected to split it at\n\t/// the kerbs, because where the pavement is interrupted is the same question as where the crossings go.\n\t/// \u003C/summary\u003E\n\tpublic List\u003CVector3\u003E GetSidewalkOutline(float _Spacing)\n\t{\n\t\tvar points = new List\u003CVector3\u003E();\n\n\t\tif (!HasSidewalks)\n\t\t\treturn points;\n\n\t\tfloat spacing = Math.Max(1.0f, _Spacing);\n\n\t\t// Centre of the slab, so it sits where someone would actually walk rather than on either kerb.\n\t\tfloat outset = SidewalkWidth * 0.5f;\n\t\tVector3 lift = Vector3.Up * SidewalkHeight;\n\n\t\tif (Shape == IntersectionShape.Circle)\n\t\t{\n\t\t\tfloat radius = Radius \u002B outset;\n\t\t\tint steps = Math.Max(8, (int)MathF.Ceiling(MathF.Tau * radius / spacing));\n\n\t\t\tfor (int i = 0; i \u003C steps; i\u002B\u002B)\n\t\t\t{\n\t\t\t\tfloat angle = MathF.Tau * i / steps;\n\n\t\t\t\tpoints.Add(new Vector3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0.0f) \u002B lift);\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Written with the same right/forward vectors BuildRectangleRoad uses, rather than as raw x/y\n\t\t\t// components. Width runs along Right and Length along Forward, which in s\u0026box axes is Y and X \u2014\n\t\t\t// spelling that out by hand gets them the wrong way round, and a junction outline rotated 90\u00B0\n\t\t\t// looks almost plausible until nothing connects to it.\n\t\t\tVector3 right = Vector3.Right;\n\t\t\tVector3 forward = Vector3.Forward;\n\n\t\t\tfloat hw = Width * 0.5f \u002B outset;\n\t\t\tfloat hl = Length * 0.5f \u002B outset;\n\n\t\t\t// Round the rectangle in order, so the loop has a consistent winding for anything that walks it.\n\t\t\tVector3[] corners =\n\t\t\t[\n\t\t\t\t-right * hw - forward * hl,\n\t\t\t\t right * hw - forward * hl,\n\t\t\t\t right * hw \u002B forward * hl,\n\t\t\t\t-right * hw \u002B forward * hl,\n\t\t\t];\n\n\t\t\tfor (int i = 0; i \u003C corners.Length; i\u002B\u002B)\n\t\t\t{\n\t\t\t\tVector3 from = corners[i];\n\t\t\t\tVector3 to = corners[(i \u002B 1) % corners.Length];\n\n\t\t\t\tint steps = Math.Max(1, (int)MathF.Ceiling(Vector3.DistanceBetween(from, to) / spacing));\n\n\t\t\t\t// Last point of each edge is skipped \u2014 it\u0027s the first of the next one, and a closed loop\n\t\t\t\t// mustn\u0027t repeat its corners.\n\t\t\t\tfor (int s = 0; s \u003C steps; s\u002B\u002B)\n\t\t\t\t\tpoints.Add(Vector3.Lerp(from, to, (float)s / steps) \u002B lift);\n\t\t\t}\n\t\t}\n\n\t\tfor (int i = 0; i \u003C points.Count; i\u002B\u002B)\n\t\t\tpoints[i] = WorldTransform.PointToWorld(points[i]);\n\n\t\treturn points;\n\t}\n\n\n\n\t/// \u003Csummary\u003EWhether this junction has pavement to walk on at all.\u003C/summary\u003E\n\tpublic bool HasSidewalks =\u003E SidewalkWidth \u003E 0.0f;\n}\n"}]}