Code/RoadIntersectionComponent/RoadIntersectionComponent.Rectangle.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.RoadTool;
[Flags]
public enum RectangleExit
{
None = 0,
North = 1 << 0, // +Forward
East = 1 << 1, // +Right
South = 1 << 2, // -Forward
West = 1 << 3 // -Right
}
public partial class RoadIntersectionComponent
{
private static readonly float QuarterTurnRad = MathF.PI * 0.5f;
[Property, Feature("General"), ShowIf(nameof(Shape), IntersectionShape.Rectangle)] private float Width { get; set { field = value; m_IsDirty = true; } } = 500.0f;
[Property, Feature("General"), ShowIf(nameof(Shape), IntersectionShape.Rectangle)] private float Length { get; set { field = value; m_IsDirty = true; } } = 500.0f;
[Property, Feature("General"), ShowIf(nameof(Shape), IntersectionShape.Rectangle), Range(0, 16), Step(2)] private int CornerSegments { get; set { field = value; m_IsDirty = true; } } = 8;
[Property(Title = "Exits"), Feature("General"), ShowIf(nameof(Shape), IntersectionShape.Rectangle)] private RectangleExit RectangleExits { get; set { field = value; m_IsDirty = true; } } = (RectangleExit)15;
private void BuildRectangleRoad(PolygonMesh _Mesh, Material _Material)
{
var cache = new Dictionary<Vector3, HalfEdgeMesh.VertexHandle>();
bool n = RectangleExits.HasFlag(RectangleExit.North);
bool s = RectangleExits.HasFlag(RectangleExit.South);
bool e = RectangleExits.HasFlag(RectangleExit.East);
bool w = RectangleExits.HasFlag(RectangleExit.West);
Vector3 right = Vector3.Right;
Vector3 forward = Vector3.Forward;
float hw = Width * 0.5f;
float hl = Length * 0.5f;
Vector3 pSW = -right * hw - forward * hl;
Vector3 pNW = -right * hw + forward * hl;
Vector3 pNE = right * hw + forward * hl;
Vector3 pSE = right * hw - forward * hl;
var vSW = MeshUtility.GetOrAddVertex(_Mesh, cache, pSW);
var vNW = MeshUtility.GetOrAddVertex(_Mesh, cache, pNW);
var vNE = MeshUtility.GetOrAddVertex(_Mesh, cache, pNE);
var vSE = MeshUtility.GetOrAddVertex(_Mesh, cache, pSE);
// Main center quad
MeshUtility.AddTexturedQuad(_Mesh, _Material, vSE, vNE, vNW, vSW,
new Vector2(pSE.x, pSE.y) / RoadTextureRepeat,
new Vector2(pNE.x, pNE.y) / RoadTextureRepeat,
new Vector2(pNW.x, pNW.y) / RoadTextureRepeat,
new Vector2(pSW.x, pSW.y) / RoadTextureRepeat);
if (n) AddRoadExtension(_Mesh, _Material, cache, pNW, pNE, forward);
if (s) AddRoadExtension(_Mesh, _Material, cache, pSE, pSW, -forward);
if (e) AddRoadExtension(_Mesh, _Material, cache, pNE, pSE, right);
if (w) AddRoadExtension(_Mesh, _Material, cache, pSW, pNW, -right);
if (CornerSegments > 0)
{
if (n && e) AddRoadCornerFiller(_Mesh, _Material, cache, pNE, right, forward);
if (n && w) AddRoadCornerFiller(_Mesh, _Material, cache, pNW, -right, forward);
if (s && e) AddRoadCornerFiller(_Mesh, _Material, cache, pSE, right, -forward);
if (s && w) AddRoadCornerFiller(_Mesh, _Material, cache, pSW, -right, -forward);
}
}
private void AddRoadExtension(PolygonMesh _Mesh, Material _Material, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _CornerA, Vector3 _CornerB, Vector3 _Direction)
{
Vector3 extA = _CornerA + _Direction * SidewalkWidth;
Vector3 extB = _CornerB + _Direction * SidewalkWidth;
var vCA = MeshUtility.GetOrAddVertex(_Mesh, _Cache, _CornerA);
var vCB = MeshUtility.GetOrAddVertex(_Mesh, _Cache, _CornerB);
var vExtA = MeshUtility.GetOrAddVertex(_Mesh, _Cache, extA);
var vExtB = MeshUtility.GetOrAddVertex(_Mesh, _Cache, extB);
MeshUtility.AddTexturedQuad(_Mesh, _Material, vCB, vExtB, vExtA, vCA,
new Vector2(_CornerB.x, _CornerB.y) / RoadTextureRepeat,
new Vector2(extB.x, extB.y) / RoadTextureRepeat,
new Vector2(extA.x, extA.y) / RoadTextureRepeat,
new Vector2(_CornerA.x, _CornerA.y) / RoadTextureRepeat);
}
private void AddRoadCornerFiller(PolygonMesh _Mesh, Material _Material, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _Corner, Vector3 _DirA, Vector3 _DirB)
{
Vector3 up = Vector3.Up;
float w = SidewalkWidth;
Vector3 arcCenter = _Corner + _DirA * w + _DirB * w;
Vector3 cross = Vector3.Cross(_DirA, _DirB);
bool flip = Vector3.Dot(cross, up) >= 0;
var vCorner = MeshUtility.GetOrAddVertex(_Mesh, _Cache, _Corner);
for (int i = 0; i < CornerSegments; i++)
{
float t0 = (float)i / CornerSegments;
float t1 = (float)(i + 1) / CornerSegments;
float angle0 = t0 * QuarterTurnRad;
float angle1 = t1 * QuarterTurnRad;
Vector3 roadEdge0 = arcCenter - _DirB * w * MathF.Cos(angle0) - _DirA * w * MathF.Sin(angle0);
Vector3 roadEdge1 = arcCenter - _DirB * w * MathF.Cos(angle1) - _DirA * w * MathF.Sin(angle1);
var vEdge0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, roadEdge0);
var vEdge1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, roadEdge1);
Vector2 uvCorner = new Vector2(_Corner.x, _Corner.y) / RoadTextureRepeat;
Vector2 uvEdge0 = new Vector2(roadEdge0.x, roadEdge0.y) / RoadTextureRepeat;
Vector2 uvEdge1 = new Vector2(roadEdge1.x, roadEdge1.y) / RoadTextureRepeat;
if (flip)
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vCorner, vEdge0, vEdge1, uvCorner, uvEdge0, uvEdge1);
else
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vCorner, vEdge1, vEdge0, uvCorner, uvEdge1, uvEdge0);
}
}
private void BuildRectangleSidewalk(PolygonMesh _Mesh, Material _Material)
{
var cache = new Dictionary<Vector3, HalfEdgeMesh.VertexHandle>();
bool n = RectangleExits.HasFlag(RectangleExit.North);
bool s = RectangleExits.HasFlag(RectangleExit.South);
bool e = RectangleExits.HasFlag(RectangleExit.East);
bool w = RectangleExits.HasFlag(RectangleExit.West);
Vector3 right = Vector3.Right;
Vector3 forward = Vector3.Forward;
float hw = Width * 0.5f;
float hl = Length * 0.5f;
Vector3 pSW = -right * hw - forward * hl;
Vector3 pNW = -right * hw + forward * hl;
Vector3 pNE = right * hw + forward * hl;
Vector3 pSE = right * hw - forward * hl;
if (!n) AddSidewalkStrip(_Mesh, _Material, cache, pNE, pNW, forward);
if (!s) AddSidewalkStrip(_Mesh, _Material, cache, pSW, pSE, -forward);
if (!e) AddSidewalkStrip(_Mesh, _Material, cache, pSE, pNE, right);
if (!w) AddSidewalkStrip(_Mesh, _Material, cache, pNW, pSW, -right);
if (CornerSegments > 0)
{
if (n && e) AddRoundedSidewalkCorner(_Mesh, _Material, cache, pNE, right, forward);
else AddCornerCap(_Mesh, _Material, cache, pNE, right, forward, e, n);
if (n && w) AddRoundedSidewalkCorner(_Mesh, _Material, cache, pNW, -right, forward);
else AddCornerCap(_Mesh, _Material, cache, pNW, -right, forward, w, n);
if (s && e) AddRoundedSidewalkCorner(_Mesh, _Material, cache, pSE, right, -forward);
else AddCornerCap(_Mesh, _Material, cache, pSE, right, -forward, e, s);
if (s && w) AddRoundedSidewalkCorner(_Mesh, _Material, cache, pSW, -right, -forward);
else AddCornerCap(_Mesh, _Material, cache, pSW, -right, -forward, w, s);
}
else
{
AddCornerCap(_Mesh, _Material, cache, pNE, right, forward, e, n);
AddCornerCap(_Mesh, _Material, cache, pNW, -right, forward, w, n);
AddCornerCap(_Mesh, _Material, cache, pSE, right, -forward, e, s);
AddCornerCap(_Mesh, _Material, cache, pSW, -right, -forward, w, s);
}
}
private void AddRoundedSidewalkCorner(PolygonMesh _Mesh, Material _Material, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _Corner, Vector3 _DirA, Vector3 _DirB)
{
Vector3 up = Vector3.Up;
float h = SidewalkHeight;
float w = SidewalkWidth;
Vector3 cross = Vector3.Cross(_DirA, _DirB);
bool flip = Vector3.Dot(cross, up) >= 0;
Vector3 arcCenter = _Corner + _DirA * w + _DirB * w;
float totalArcLength = w * QuarterTurnRad;
float hH = h / SidewalkTextureRepeat;
for (int i = 0; i < CornerSegments; i++)
{
float t0 = (float)i / CornerSegments;
float t1 = (float)(i + 1) / CornerSegments;
// At t=0 inner0==outer0, at t=1 inner1==outer1 — top face degenerates to triangle.
bool startDeg = (i == 0);
bool endDeg = (i == CornerSegments - 1);
float angle0 = float.DegreesToRadians(t0 * 90.0f);
float angle1 = float.DegreesToRadians(t1 * 90.0f);
Vector3 inner0 = arcCenter - _DirB * w * MathF.Cos(angle0) - _DirA * w * MathF.Sin(angle0);
Vector3 inner1 = arcCenter - _DirB * w * MathF.Cos(angle1) - _DirA * w * MathF.Sin(angle1);
Vector3 outer0, outer1;
if (t1 <= 0.5f)
{
outer0 = _Corner + _DirA * w + _DirB * w * (t0 * 2.0f);
outer1 = _Corner + _DirA * w + _DirB * w * (t1 * 2.0f);
}
else if (t0 >= 0.5f)
{
outer0 = _Corner + _DirA * w * (1.0f - (t0 - 0.5f) * 2.0f) + _DirB * w;
outer1 = _Corner + _DirA * w * (1.0f - (t1 - 0.5f) * 2.0f) + _DirB * w;
}
else
{
outer0 = _Corner + _DirA * w + _DirB * w * (t0 * 2.0f);
outer1 = _Corner + _DirA * w * (1.0f - (t1 - 0.5f) * 2.0f) + _DirB * w;
}
Vector3 topInner0 = inner0 + up * h;
Vector3 topInner1 = inner1 + up * h;
Vector3 topOuter0 = outer0 + up * h;
Vector3 topOuter1 = outer1 + up * h;
var vI0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, inner0);
var vI1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, inner1);
var vO0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, outer0);
var vO1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, outer1);
var vTI0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, topInner0);
var vTI1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, topInner1);
var vTO0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, topOuter0);
var vTO1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, topOuter1);
float v0 = (t0 * totalArcLength) / SidewalkTextureRepeat;
float v1 = (t1 * totalArcLength) / SidewalkTextureRepeat;
float distOuter0 = Vector3.DistanceBetween(inner0, outer0) / SidewalkTextureRepeat;
float distOuter1 = Vector3.DistanceBetween(inner1, outer1) / SidewalkTextureRepeat;
if (flip)
{
// Top face — triangle at arc endpoints where inner==outer
if (startDeg)
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTO0, vTO1, vTI1,
new Vector2(distOuter0, v0), new Vector2(distOuter1, v1), new Vector2(0, v1));
else if (endDeg)
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTO0, vTO1, vTI0,
new Vector2(distOuter0, v0), new Vector2(distOuter1, v1), new Vector2(0, v0));
else
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTO0, vTO1, vTI1, vTI0,
new Vector2(distOuter0, v0), new Vector2(distOuter1, v1), new Vector2(0, v1), new Vector2(0, v0));
// Inner face
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTI0, vTI1, vI1, vI0,
new Vector2(0, v0), new Vector2(0, v1), new Vector2(hH, v1), new Vector2(hH, v0));
// Outer face
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTO1, vTO0, vO0, vO1,
new Vector2(0, v1), new Vector2(0, v0), new Vector2(hH, v0), new Vector2(hH, v1));
}
else
{
// Top face — triangle at arc endpoints where inner==outer
if (startDeg)
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTI0, vTI1, vTO1,
new Vector2(0, v0), new Vector2(0, v1), new Vector2(distOuter1, v1));
else if (endDeg)
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTI0, vTI1, vTO0,
new Vector2(0, v0), new Vector2(0, v1), new Vector2(distOuter0, v0));
else
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTI0, vTI1, vTO1, vTO0,
new Vector2(0, v0), new Vector2(0, v1), new Vector2(distOuter1, v1), new Vector2(distOuter0, v0));
// Inner face
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTI1, vTI0, vI0, vI1,
new Vector2(0, v1), new Vector2(0, v0), new Vector2(hH, v0), new Vector2(hH, v1));
// Outer face
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTO0, vTO1, vO1, vO0,
new Vector2(0, v1), new Vector2(0, v0), new Vector2(hH, v0), new Vector2(hH, v1));
}
}
}
private void AddCornerCap(PolygonMesh _Mesh, Material _Material, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _CornerPos, Vector3 _DirA, Vector3 _DirB, bool _SideAIsExit, bool _SideBIsExit)
{
Vector3 up = Vector3.Up;
float h = SidewalkHeight;
float w = SidewalkWidth;
Vector3 pCenter = _CornerPos;
Vector3 pA = _CornerPos + _DirA * w;
Vector3 pB = _CornerPos + _DirB * w;
Vector3 pOuter = _CornerPos + _DirA * w + _DirB * w;
Vector3 tCenter = pCenter + up * h;
Vector3 tA = pA + up * h;
Vector3 tB = pB + up * h;
Vector3 tOuter = pOuter + up * h;
var vCenter = MeshUtility.GetOrAddVertex(_Mesh, _Cache, pCenter);
var vA = MeshUtility.GetOrAddVertex(_Mesh, _Cache, pA);
var vB = MeshUtility.GetOrAddVertex(_Mesh, _Cache, pB);
var vOuter = MeshUtility.GetOrAddVertex(_Mesh, _Cache, pOuter);
var vTC = MeshUtility.GetOrAddVertex(_Mesh, _Cache, tCenter);
var vTA = MeshUtility.GetOrAddVertex(_Mesh, _Cache, tA);
var vTB = MeshUtility.GetOrAddVertex(_Mesh, _Cache, tB);
var vTO = MeshUtility.GetOrAddVertex(_Mesh, _Cache, tOuter);
Vector3 cross = Vector3.Cross(_DirA, _DirB);
bool flip = Vector3.Dot(cross, up) >= 0;
float uW = SidewalkWidth / SidewalkTextureRepeat;
float hH = SidewalkHeight / SidewalkTextureRepeat;
Vector2 uvA = !_SideAIsExit ? new Vector2(uW, 0) : new Vector2(0, uW);
Vector2 uvB = !_SideBIsExit ? new Vector2(uW, 0) : new Vector2(0, uW);
if (flip)
{
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTO, vTC, vTA, new Vector2(uW, uW), new Vector2(0, 0), uvA);
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTO, vTB, vTC, new Vector2(uW, uW), uvB, new Vector2(0, 0));
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTA, vA, vOuter, vTO,
new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0), new Vector2(0, 0));
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTO, vOuter, vB, vTB,
new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0), new Vector2(0, 0));
}
else
{
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTO, vTA, vTC, new Vector2(uW, uW), uvA, new Vector2(0, 0));
MeshUtility.AddTexturedTriangle(_Mesh, _Material, vTO, vTC, vTB, new Vector2(uW, uW), new Vector2(0, 0), uvB);
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTO, vOuter, vA, vTA,
new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0), new Vector2(0, 0));
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTB, vB, vOuter, vTO,
new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0), new Vector2(0, 0));
}
if (_SideAIsExit)
{
if (flip)
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTA, vTC, vCenter, vA,
new Vector2(0, 0), new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0));
else
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTC, vTA, vA, vCenter,
new Vector2(0, 0), new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0));
}
if (_SideBIsExit)
{
if (flip)
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTC, vTB, vB, vCenter,
new Vector2(0, 0), new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0));
else
MeshUtility.AddTexturedQuad(_Mesh, _Material, vTB, vTC, vCenter, vB,
new Vector2(0, 0), new Vector2(0, uW), new Vector2(hH, uW), new Vector2(hH, 0));
}
}
private void AddSidewalkStrip(PolygonMesh _Mesh, Material _Material, Dictionary<Vector3, HalfEdgeMesh.VertexHandle> _Cache, Vector3 _Start, Vector3 _End, Vector3 _Outward)
{
Vector3 up = Vector3.Up;
Vector3 s0 = _Start;
Vector3 s1 = _End;
Vector3 o0 = s0 + _Outward * SidewalkWidth;
Vector3 o1 = s1 + _Outward * SidewalkWidth;
Vector3 t0 = s0 + up * SidewalkHeight;
Vector3 t1 = s1 + up * SidewalkHeight;
Vector3 ot0 = o0 + up * SidewalkHeight;
Vector3 ot1 = o1 + up * SidewalkHeight;
var vS0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, s0);
var vS1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, s1);
var vO0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, o0);
var vO1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, o1);
var vT0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, t0);
var vT1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, t1);
var vOT0 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, ot0);
var vOT1 = MeshUtility.GetOrAddVertex(_Mesh, _Cache, ot1);
float stripLen = (_End - _Start).Length;
float uWidth = SidewalkWidth / SidewalkTextureRepeat;
float vLen = stripLen / SidewalkTextureRepeat;
float hHeight = SidewalkHeight / SidewalkTextureRepeat;
// Top face
MeshUtility.AddTexturedQuad(_Mesh, _Material, vOT0, vOT1, vT1, vT0,
new Vector2(uWidth, 0), new Vector2(uWidth, vLen), new Vector2(0, vLen), new Vector2(0, 0));
// Inner face
MeshUtility.AddTexturedQuad(_Mesh, _Material, vT0, vT1, vS1, vS0,
new Vector2(0, 0), new Vector2(0, vLen), new Vector2(hHeight, vLen), new Vector2(hHeight, 0));
// Outer face
MeshUtility.AddTexturedQuad(_Mesh, _Material, vOT1, vOT0, vO0, vO1,
new Vector2(0, 0), new Vector2(0, vLen), new Vector2(hHeight, vLen), new Vector2(hHeight, 0));
}
private Transform GetRectangleExitLocalTransform(RectangleExit _Side, bool _IncludeSidewalk = false)
{
Vector3 pos = Vector3.Zero;
Rotation rot = Rotation.Identity;
switch (_Side)
{
case RectangleExit.North:
pos += Vector3.Forward * ((Length * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
break;
case RectangleExit.South:
pos -= Vector3.Forward * ((Length * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
rot *= Rotation.FromYaw(180);
break;
case RectangleExit.East:
pos += Vector3.Right * ((Width * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
rot *= Rotation.FromYaw(-90);
break;
case RectangleExit.West:
pos -= Vector3.Right * ((Width * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
rot *= Rotation.FromYaw(90);
break;
}
return new Transform { Position = pos, Rotation = rot };
}
private Transform GetRectangleExitTransform(RectangleExit _Side, bool _IncludeSidewalk = false)
{
Vector3 pos = WorldPosition;
Rotation rot = WorldRotation;
switch (_Side)
{
case RectangleExit.North:
pos += WorldRotation.Forward * ((Length * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
break;
case RectangleExit.South:
pos -= WorldRotation.Forward * ((Length * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
rot *= Rotation.FromYaw(180);
break;
case RectangleExit.East:
pos += WorldRotation.Right * ((Width * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
rot *= Rotation.FromYaw(-90);
break;
case RectangleExit.West:
pos -= WorldRotation.Right * ((Width * 0.5f) + (_IncludeSidewalk ? SidewalkWidth : 0.0f));
rot *= Rotation.FromYaw(90);
break;
}
return new Transform { Position = pos, Rotation = rot };
}
}