Code/StairsTesting.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;
using HalfEdgeMesh;
namespace StairsTool;
[Title("Stairs"), Category("Construction"), Icon("stairs")]
public class StairsTesting : Component
{
private float _width = 100.0f;
private float _height = 200.0f;
private float _depth = 300.0f;
private int _stepCount = 10;
private bool _doubleSided = true;
private StairShape _stairShape = StairShape.Straight;
private float _curveAngle = 90.0f;
private float _landingHeight = 0.5f;
private LandingDirection _landingDirection = LandingDirection.Right;
private Material _material = null;
private string _materialPath = "materials/dev/dev_gray.vmat";
private int _materialRotation = 0;
private bool _showMeshCollider = false;
private bool _alwaysCenterMesh = true;
[Property]
public float Width
{
get => _width;
set
{
if (_width == value) return;
_width = value;
RegenerateMesh();
}
}
[Property]
public float Height
{
get => _height;
set
{
if (_height == value) return;
_height = value;
RegenerateMesh();
}
}
[Property]
public float Depth
{
get => _depth;
set
{
if (_depth == value) return;
_depth = value;
RegenerateMesh();
}
}
[Property, Range(2, 64)]
public int StepCount
{
get => _stepCount;
set
{
if (_stepCount == value) return;
_stepCount = value;
RegenerateMesh();
}
}
//TODO: split the vertices into top/side&bottom to allow for only top/all sides option to save on polycount
//[Property]
public bool DoubleSided = true;
/*{
get => _doubleSided;
set
{
if (_doubleSided == value) return;
_doubleSided = value;
RegenerateMesh();
}
}*/
[Property]
public StairShape ShapeType
{
get => _stairShape;
set
{
if (_stairShape == value) return;
_stairShape = value;
RegenerateMesh();
}
}
[ShowIf("ShapeType",StairShape.Curved)]
[Property, Range(1, 360)]
public float CurveAngle
{
get => _curveAngle;
set
{
if (_curveAngle == value) return;
_curveAngle = value;
if (_stairShape == StairShape.Curved)
RegenerateMesh();
}
}
[ShowIf("ShapeType",StairShape.Landing)]
[Property, Range(0.1f, 0.9f)]
public float LandingHeightRatio
{
get => _landingHeight;
set
{
if (_landingHeight == value) return;
_landingHeight = Math.Clamp(value, 0.1f, 0.9f);
if (_stairShape == StairShape.Landing)
RegenerateMesh();
}
}
[ShowIf("ShapeType",StairShape.Landing)]
[Property]
public LandingDirection LandingDir
{
get => _landingDirection;
set
{
if (_landingDirection == value) return;
_landingDirection = value;
if (_stairShape == StairShape.Landing)
RegenerateMesh();
}
}
[Property, ResourceType("material")]
public Material Material
{
get => _material;
set
{
if (_material == value) return;
_material = value;
RegenerateMesh();
}
}
//TODO: Add a property to set the material rotation from the editor
//[Property, Range(0, 360), Title("Material Rotation"), Description("Rotation angle for textures on stair faces in degrees")]
public float MaterialRotation
{
get => _materialRotation;
set
{
if (_materialRotation == value) return;
_materialRotation = (int)value;
RegenerateMesh();
}
}
[Property, Description("Show or hide the mesh collider in the editor"), Category("Advanced")]
public bool ShowMeshCollider
{
get => _showMeshCollider;
set
{
if (_showMeshCollider == value) return;
_showMeshCollider = value;
UpdateMeshColliderVisibility();
}
}
[Property, Description("Show or hide the mesh collider in the editor"), Category("Advanced")]
public bool AlwaysCenterMesh
{
get => _alwaysCenterMesh;
set
{
if (_alwaysCenterMesh == value) return;
_alwaysCenterMesh = value;
UpdateMeshColliderVisibility();
}
}
private PolygonMesh _mesh;
// Get the mesh component, creating it if it doesn't exist
private MeshComponent MeshComponent => GameObject?.Components.GetOrCreate<MeshComponent>();
protected override void OnAwake()
{
base.OnAwake();
RegenerateMesh();
}
protected override void OnEnabled()
{
base.OnEnabled();
RegenerateMesh();
UpdateMeshColliderVisibility();
}
private void RegenerateMesh()
{
if (!GameObject.IsValid)
return;
try
{
_mesh = new PolygonMesh();
switch (_stairShape)
{
case StairShape.Straight:
GenerateStraightStairMesh(_mesh);
break;
case StairShape.Curved:
GenerateCurvedStairMesh(_mesh);
break;
case StairShape.Landing:
GenerateLandingStairMesh(_mesh);
break;
default:
GenerateStraightStairMesh(_mesh);
break;
}
Material material = _material;
if (material == null)
{
Log.Warning($"Material is null, trying default material...");
material = Material.Load("materials/default.vmat");
if (material == null)
{
Log.Error("Failed to load materials");
return;
}
}
foreach (var face in _mesh.FaceHandles)
{
_mesh.SetFaceMaterial(face, material);
_mesh.TextureAlignToGrid(new Transform().WithPosition(Vector3.Zero).WithRotation(Rotation.FromRoll(_materialRotation)));
}
if (_alwaysCenterMesh){
CenterMesh();
}
var meshComponent = MeshComponent;
if (meshComponent != null)
{
meshComponent.Mesh = _mesh;
Log.Info("Stair mesh generated successfully");
// Update the mesh collider visibility after generating the mesh
UpdateMeshColliderVisibility();
}
else
{
Log.Error("Failed to create MeshComponent");
}
}
catch (Exception ex)
{
Log.Error($"Exception in RegenerateMesh: {ex.Message}");
Log.Error(ex.StackTrace);
}
}
[Property, Button("Center Mesh"), Description("Centers the mesh at the origin"), Category("Advanced")]
public void CenterMesh()
{
if (_mesh == null || !GameObject.IsValid)
return;
try
{
// Calculate the bounds of the mesh
BBox bounds = _mesh.CalculateBounds();
// Calculate the center offset
Vector3 centerOffset = bounds.Center;
// Apply this offset to all vertices
foreach (var vertexHandle in _mesh.VertexHandles)
{
Vector3 currentPos = _mesh.GetVertexPosition(vertexHandle);
_mesh.SetVertexPosition(vertexHandle, currentPos - centerOffset);
}
// Update the mesh component
MeshComponent.Mesh = _mesh;
Log.Info("Mesh successfully centered at origin");
}
catch (Exception ex)
{
Log.Error($"Failed to center mesh: {ex.Message}");
Log.Error(ex.StackTrace);
}
}
private void GenerateStraightStairMesh(PolygonMesh mesh)
{
if (mesh == null) return;
float stepHeight = Height / StepCount;
float stepDepth = Depth / StepCount;
// Create vertices for the full staircase outline for enclosing panels if needed
var leftSideVertices = new List<VertexHandle>();
var rightSideVertices = new List<VertexHandle>();
var bottomVertices = new List<VertexHandle>();
var topVertices = new List<VertexHandle>();
var backVertices = new List<VertexHandle>();
// Store corner vertices for the complete staircase
VertexHandle backBottomLeft = default;
VertexHandle backBottomRight = default;
VertexHandle backTopLeft = default;
VertexHandle backTopRight = default;
// Generate each step individually, making sure every step is fully enclosed
for (var i = 0; i < StepCount; i++)
{
float z = stepHeight * i;
float x = stepDepth * i;
float nextZ = stepHeight * (i + 1);
float nextX = stepDepth * (i + 1);
// Add vertices for this step
var v1 = mesh.AddVertex(new Vector3(x, -Width/2, z)); // Bottom left front
var v2 = mesh.AddVertex(new Vector3(x, Width/2, z)); // Bottom right front
var v3 = mesh.AddVertex(new Vector3(x, -Width/2, nextZ)); // Top left front
var v4 = mesh.AddVertex(new Vector3(x, Width/2, nextZ)); // Top right front
var v5 = mesh.AddVertex(new Vector3(nextX, -Width/2, nextZ)); // Top left back
var v6 = mesh.AddVertex(new Vector3(nextX, Width/2, nextZ)); // Top right back
var v7 = mesh.AddVertex(new Vector3(nextX, -Width/2, z)); // Bottom left back
var v8 = mesh.AddVertex(new Vector3(nextX, Width/2, z)); // Bottom right back
// Store references to full staircase bounds if needed for outer panels
if (i == 0)
{
bottomVertices.Add(v1);
bottomVertices.Add(v2);
bottomVertices.Add(v8);
bottomVertices.Add(v7);
leftSideVertices.Add(v1);
leftSideVertices.Add(v3);
rightSideVertices.Add(v2);
rightSideVertices.Add(v4);
// Store back bottom corners
backBottomLeft = v7;
backBottomRight = v8;
}
if (i == StepCount - 1)
{
// Add top vertices for the top enclosing face
topVertices.Add(v3);
topVertices.Add(v5);
topVertices.Add(v6);
topVertices.Add(v4);
// Store back top corners
backTopLeft = v5;
backTopRight = v6;
leftSideVertices.Add(v5);
leftSideVertices.Add(v7);
rightSideVertices.Add(v6);
rightSideVertices.Add(v8);
}
else if (i == 0)
{
leftSideVertices.Add(v3);
rightSideVertices.Add(v4);
}
// Each step gets ALL faces with correct winding order to face outward
// 1. Front face (vertical part of step) - facing front
mesh.AddFace(v1, v3, v4, v2);
// 2. Top face (horizontal part of step) - facing up
mesh.AddFace(v3, v5, v6, v4);
// 3. Back face of step - facing back
mesh.AddFace(v5, v7, v8, v6);
// 4. Left side face of step - facing left
mesh.AddFace(v1, v3, v5, v7);
// 5. Right side face of step - facing right
mesh.AddFace(v2, v4, v6, v8);
// 6. Bottom face of step - facing down
mesh.AddFace(v1, v2, v8, v7);
// If double sided is enabled, add reverse winding faces for inside visibility
if (_doubleSided)
{
// 1. Front face (reverse)
mesh.AddFace(v2, v4, v3, v1);
// 2. Top face (reverse)
mesh.AddFace(v4, v6, v5, v3);
// 3. Back face (reverse)
mesh.AddFace(v6, v8, v7, v5);
// 4. Left side face (reverse)
mesh.AddFace(v7, v5, v3, v1);
// 5. Right side face (reverse)
mesh.AddFace(v8, v6, v4, v2);
// 6. Bottom face (reverse)
mesh.AddFace(v7, v8, v2, v1);
}
}
// If double sided is enabled, also add the full enclosure panels with correct winding
if (_doubleSided)
{
// Full left side panel with correct winding to face outward
if (leftSideVertices.Count >= 4)
{
var leftPanelVertices = new List<VertexHandle>();
// Reverse the left side vertices to make them face outward
for (int i = leftSideVertices.Count - 1; i >= 0; i--)
{
leftPanelVertices.Add(leftSideVertices[i]);
}
// Add full left side panel face (facing left)
mesh.AddFace(leftPanelVertices.ToArray());
// Add reverse-wound face for double-sided rendering
mesh.AddFace(leftSideVertices.ToArray());
}
// Full right side panel with correct winding to face outward
if (rightSideVertices.Count >= 4)
{
// Add right side panel face (facing right)
mesh.AddFace(rightSideVertices.ToArray());
// Add reverse-wound face for double-sided rendering
var rightPanelVertices = new List<VertexHandle>();
for (int i = rightSideVertices.Count - 1; i >= 0; i--)
{
rightPanelVertices.Add(rightSideVertices[i]);
}
mesh.AddFace(rightPanelVertices.ToArray());
}
// Add top enclosing face with correct winding to face upward
if (topVertices.Count > 0)
{
// Add top face facing upward
mesh.AddFace(topVertices.ToArray());
// Add reverse-wound face for double-sided rendering
var topVerticesReversed = new List<VertexHandle>(topVertices);
topVerticesReversed.Reverse();
mesh.AddFace(topVerticesReversed.ToArray());
}
// Add back panel with correct winding to face backward
if (backBottomLeft.IsValid && backBottomRight.IsValid && backTopRight.IsValid && backTopLeft.IsValid)
{
// Add back face with correct winding to face backward
backVertices.Add(backBottomLeft);
backVertices.Add(backTopLeft);
backVertices.Add(backTopRight);
backVertices.Add(backBottomRight);
mesh.AddFace(backVertices.ToArray());
// Add reverse-wound face for double-sided rendering
var backVerticesReversed = new List<VertexHandle>(backVertices);
backVerticesReversed.Reverse();
mesh.AddFace(backVerticesReversed.ToArray());
}
}
// Apply texture alignment to all faces
//mesh.TextureAlignToGrid(Transform.Zero);
}
private void GenerateCurvedStairMesh(PolygonMesh mesh)
{
if (mesh == null) return;
float stepHeight = Height / StepCount;
float totalAngle = MathF.PI * _curveAngle / 180.0f; // Convert degrees to radians
float anglePerStep = totalAngle / StepCount;
// Calculate the radius based on the desired depth (outer perimeter)
float outerRadius = Depth / totalAngle;
float innerRadius = outerRadius - Width;
// Create vertices for the full staircase outline
var innerSideVertices = new List<VertexHandle>();
var outerSideVertices = new List<VertexHandle>();
var bottomVertices = new List<VertexHandle>();
var topVertices = new List<VertexHandle>();
// Store the first and last steps' vertices for enclosing the sides
VertexHandle[] firstStepVertices = new VertexHandle[4];
VertexHandle[] lastStepVertices = new VertexHandle[4];
// Generate each step individually
for (var i = 0; i < StepCount; i++)
{
float z = stepHeight * i;
float nextZ = stepHeight * (i + 1);
float angle = anglePerStep * i;
float nextAngle = anglePerStep * (i + 1);
// Calculate inner and outer points at current and next angle
float sinAngle = MathF.Sin(angle);
float cosAngle = MathF.Cos(angle);
float sinNextAngle = MathF.Sin(nextAngle);
float cosNextAngle = MathF.Cos(nextAngle);
Vector3 innerPoint = new Vector3(innerRadius * sinAngle, innerRadius * cosAngle, z);
Vector3 outerPoint = new Vector3(outerRadius * sinAngle, outerRadius * cosAngle, z);
Vector3 innerNextPoint = new Vector3(innerRadius * sinNextAngle, innerRadius * cosNextAngle, z);
Vector3 outerNextPoint = new Vector3(outerRadius * sinNextAngle, outerRadius * cosNextAngle, z);
Vector3 innerPointTop = new Vector3(innerRadius * sinAngle, innerRadius * cosAngle, nextZ);
Vector3 outerPointTop = new Vector3(outerRadius * sinAngle, outerRadius * cosAngle, nextZ);
Vector3 innerNextPointTop = new Vector3(innerRadius * sinNextAngle, innerRadius * cosNextAngle, nextZ);
Vector3 outerNextPointTop = new Vector3(outerRadius * sinNextAngle, outerRadius * cosNextAngle, nextZ);
// Add vertices for this step
var v1 = mesh.AddVertex(innerPoint); // Bottom inner
var v2 = mesh.AddVertex(outerPoint); // Bottom outer
var v3 = mesh.AddVertex(innerPointTop); // Top inner
var v4 = mesh.AddVertex(outerPointTop); // Top outer
var v5 = mesh.AddVertex(innerNextPointTop); // Top next inner
var v6 = mesh.AddVertex(outerNextPointTop); // Top next outer
var v7 = mesh.AddVertex(innerNextPoint); // Bottom next inner
var v8 = mesh.AddVertex(outerNextPoint); // Bottom next outer
// Store first and last steps' vertices for side enclosure
if (i == 0)
{
firstStepVertices[0] = v1; // Bottom inner
firstStepVertices[1] = v2; // Bottom outer
firstStepVertices[2] = v3; // Top inner
firstStepVertices[3] = v4; // Top outer
// Add to side vertices lists
innerSideVertices.Add(v1);
innerSideVertices.Add(v3);
outerSideVertices.Add(v2);
outerSideVertices.Add(v4);
// Add to bottom vertices
bottomVertices.Add(v1);
bottomVertices.Add(v2);
bottomVertices.Add(v8);
bottomVertices.Add(v7);
}
if (i == StepCount - 1)
{
lastStepVertices[0] = v5; // Top next inner
lastStepVertices[1] = v6; // Top next outer
lastStepVertices[2] = v7; // Bottom next inner
lastStepVertices[3] = v8; // Bottom next outer
// Add to side vertices lists
innerSideVertices.Add(v5);
innerSideVertices.Add(v7);
outerSideVertices.Add(v6);
outerSideVertices.Add(v8);
// Add top vertices for the top enclosing face
topVertices.Add(v3);
topVertices.Add(v5);
topVertices.Add(v6);
topVertices.Add(v4);
}
// Front vertical face of step
mesh.AddFace(v1, v3, v4, v2);
// Top horizontal face of step
mesh.AddFace(v3, v5, v6, v4);
// Back vertical face of step
mesh.AddFace(v5, v7, v8, v6);
// Inner side face of step
mesh.AddFace(v1, v3, v5, v7);
// Outer side face of step
mesh.AddFace(v2, v4, v6, v8);
// Bottom face of step
mesh.AddFace(v1, v2, v8, v7);
// If double sided is enabled, add reverse winding faces
if (_doubleSided)
{
// Front face (reverse)
mesh.AddFace(v2, v4, v3, v1);
// Top face (reverse)
mesh.AddFace(v4, v6, v5, v3);
// Back face (reverse)
mesh.AddFace(v6, v8, v7, v5);
// Inner side face (reverse)
mesh.AddFace(v7, v5, v3, v1);
// Outer side face (reverse)
mesh.AddFace(v8, v6, v4, v2);
// Bottom face (reverse)
mesh.AddFace(v7, v8, v2, v1);
}
}
// Add enclosing faces for start and end of staircase if double sided
if (_doubleSided && firstStepVertices[0].IsValid && lastStepVertices[0].IsValid)
{
// Start face (facing toward start of stairs)
var startFace = new List<VertexHandle>
{
firstStepVertices[0], // Bottom inner
firstStepVertices[2], // Top inner
firstStepVertices[3], // Top outer
firstStepVertices[1] // Bottom outer
};
mesh.AddFace(startFace.ToArray());
// Reverse winding for double-sided rendering
var startFaceReversed = new List<VertexHandle>(startFace);
startFaceReversed.Reverse();
mesh.AddFace(startFaceReversed.ToArray());
// End face (facing toward end of stairs)
var endFace = new List<VertexHandle>
{
lastStepVertices[2], // Bottom next inner
lastStepVertices[0], // Top next inner
lastStepVertices[1], // Top next outer
lastStepVertices[3] // Bottom next outer
};
mesh.AddFace(endFace.ToArray());
// Reverse winding for double-sided rendering
var endFaceReversed = new List<VertexHandle>(endFace);
endFaceReversed.Reverse();
mesh.AddFace(endFaceReversed.ToArray());
// Add top enclosing face
if (topVertices.Count > 0)
{
mesh.AddFace(topVertices.ToArray());
var topVerticesReversed = new List<VertexHandle>(topVertices);
topVerticesReversed.Reverse();
mesh.AddFace(topVerticesReversed.ToArray());
}
}
// Apply texture alignment to all faces
//mesh.TextureAlignToGrid(Transform.Zero);
}
private void GenerateLandingStairMesh(PolygonMesh mesh)
{
if (mesh == null) return;
// Calculate the landing height position (relative to total height)
float landingHeightPos = Height * _landingHeight;
// Calculate step counts and dimensions for each flight
int firstFlightSteps = (int)(StepCount * _landingHeight);
int secondFlightSteps = StepCount - firstFlightSteps;
// Ensure we have at least one step in each flight
if (firstFlightSteps < 1) firstFlightSteps = 1;
if (secondFlightSteps < 1) secondFlightSteps = 1;
float firstFlightHeight = landingHeightPos;
float secondFlightHeight = Height - landingHeightPos;
float firstStepHeight = firstFlightHeight / firstFlightSteps;
float secondStepHeight = secondFlightHeight / secondFlightSteps;
// First flight uses half the depth for straight and U shape
float firstFlightDepth = Depth / 2;
// Second flight uses half the depth for straight and U shape
float secondFlightDepth = Depth / 2;
float firstStepDepth = firstFlightDepth / firstFlightSteps;
float secondStepDepth = secondFlightDepth / secondFlightSteps;
// Landing dimensions
// For U-shape, we need to adjust the landing depth
float landingDepth = /*_landingDirection == LandingDirection.U ? Width :*/ Width;
// Generate the first flight of stairs (going up)
GenerateStairFlight(
mesh,
0,
0,
firstFlightSteps,
firstStepHeight,
firstStepDepth,
Width,
false);
// Generate the landing platform
// Use landing top height (landing height position + step height) for the second flight starting position
float landingTopHeight = landingHeightPos + firstStepHeight;
// Create landing connection points based on direction
var landingTopFrontLeft = new Vector3(0, 0, 0);
var landingTopFrontRight = new Vector3(0, 0, 0);
switch (_landingDirection)
{
case LandingDirection.Straight:
// For straight, connection points are at the back of the landing
landingTopFrontLeft = new Vector3(firstFlightDepth + landingDepth, -Width / 2, landingTopHeight);
landingTopFrontRight = new Vector3(firstFlightDepth + landingDepth, Width / 2, landingTopHeight);
break;
case LandingDirection.Left:
// For left turn, connection points are at the left side of the landing
landingTopFrontLeft = new Vector3(firstFlightDepth, Width / 2, landingTopHeight);
landingTopFrontRight = new Vector3(firstFlightDepth + Width, Width / 2, landingTopHeight);
break;
case LandingDirection.Right:
// For right turn, connection points are at the right side of the landing
landingTopFrontLeft = new Vector3(firstFlightDepth, -Width / 2, landingTopHeight);
landingTopFrontRight = new Vector3(firstFlightDepth + Width, -Width / 2, landingTopHeight);
break;
/*case LandingDirection.U:
// For U shape, connection points are at the front of the landing (same side as first flight)
landingTopFrontLeft = new Vector3(0, -Width / 2, landingTopHeight);
landingTopFrontRight = new Vector3(0, Width / 2, landingTopHeight);
break;*/
}
GenerateLandingPlatform(
mesh,
firstFlightDepth, // x position at end of first flight
landingHeightPos, // z position at landing height
landingDepth, // depth of landing platform
Width, // width of landing platform
firstStepHeight); // height of a step (for consistency)
// Position the second flight based on the landing direction
// Adjusted to be from perspective of someone at the bottom looking up
switch (_landingDirection)
{
case LandingDirection.Straight:
// For straight, continue in the same direction (along X axis)
GenerateStairFlight(
mesh,
firstFlightDepth + landingDepth, // Start at the edge of the landing
landingTopHeight, // Start at the top of landing platform
secondFlightSteps,
secondStepHeight,
secondStepDepth,
Width,
false,
false,
landingTopFrontLeft,
landingTopFrontRight); // Connect to landing front vertices
break;
case LandingDirection.Left:
// For left turn, place stairs on the left side (-Y) of the landing
// From perspective of someone at bottom looking up
GenerateStairFlight(
mesh,
firstFlightDepth, // Start at the beginning of the landing
landingTopHeight, // Start at the top of landing platform
secondFlightSteps,
secondStepHeight,
secondStepDepth,
Width,
true, // true = perpendicular to first flight (along Y axis)
false,
landingTopFrontLeft,
landingTopFrontRight); // Connect to landing front vertices
break;
case LandingDirection.Right:
// For right turn, place stairs on the right side (+Y) of the landing
// From perspective of someone at bottom looking up
GenerateStairFlight(
mesh,
firstFlightDepth, // Start at the beginning of the landing
landingTopHeight, // Start at the top of landing platform
secondFlightSteps,
secondStepHeight,
secondStepDepth,
Width,
true, // true = perpendicular to first flight (along Y axis)
true,
landingTopFrontLeft,
landingTopFrontRight); // Connect to landing front vertices
break;
/*case LandingDirection.U:
// For U-shaped stairs, place the second flight going back in the opposite direction
// from the first flight (back along negative X axis)
GenerateStairFlight(
mesh,
0, // Start at x=0 (same side as first flight)
landingTopHeight, // Start at the top of landing platform
secondFlightSteps,
secondStepHeight,
secondStepDepth,
Width,
false, // false = same axis as first flight (along X axis)
true, // true = opposite direction as first flight
landingTopFrontLeft,
landingTopFrontRight); // Connect to landing front vertices
break;*/
}
}
private void GenerateStairFlight(PolygonMesh mesh, float startX, float startZ, int steps, float stepHeight, float stepDepth, float width, bool perpendicular, bool negativeDirection = false, Vector3? landingConnectLeft = null, Vector3? landingConnectRight = null)
{
// Lists to store vertices for enclosing panels
var leftSideVertices = new List<VertexHandle>();
var rightSideVertices = new List<VertexHandle>();
var bottomFaceVertices = new List<VertexHandle>();
var topFaceVertices = new List<VertexHandle>();
var backFaceVertices = new List<VertexHandle>();
for (var i = 0; i < steps; i++)
{
float z = startZ + (stepHeight * i);
float nextZ = startZ + (stepHeight * (i + 1));
Vector3 v1, v2, v3, v4, v5, v6, v7, v8;
if (!perpendicular)
{
// Standard direction (along X axis)
float x = startX + (stepDepth * i);
float nextX = startX + (stepDepth * (i + 1));
v1 = new Vector3(x, -width/2, z); // Bottom left front
v2 = new Vector3(x, width/2, z); // Bottom right front
v3 = new Vector3(x, -width/2, nextZ); // Top left front
v4 = new Vector3(x, width/2, nextZ); // Top right front
v5 = new Vector3(nextX, -width/2, nextZ); // Top left back
v6 = new Vector3(nextX, width/2, nextZ); // Top right back
v7 = new Vector3(nextX, -Width/2, z); // Bottom left back
v8 = new Vector3(nextX, Width/2, z); // Bottom right back
}
else
{
// Perpendicular direction (along Y axis)
float direction = negativeDirection ? -1 : 1; // Determine if we're going in positive or negative Y direction
if (negativeDirection)
{
// Going in negative Y direction (RIGHT turn)
float y = -width/2 - (stepDepth * i);
float nextY = -width/2 - (stepDepth * (i + 1));
v1 = new Vector3(startX, y, z); // Bottom left front
v2 = new Vector3(startX + width, y, z); // Bottom right front
v3 = new Vector3(startX, y, nextZ); // Top left front
v4 = new Vector3(startX + width, y, nextZ);// Top right front
v5 = new Vector3(startX, nextY, nextZ); // Top left back
v6 = new Vector3(startX + width, nextY, nextZ); // Top right back
v7 = new Vector3(startX, nextY, z); // Bottom left back
v8 = new Vector3(startX + width, nextY, z); // Bottom right back
}
else
{
// Going in positive Y direction (LEFT turn)
float y = width/2 + (stepDepth * i);
float nextY = width/2 + (stepDepth * (i + 1));
v1 = new Vector3(startX, y, z); // Bottom left front
v2 = new Vector3(startX + width, y, z); // Bottom right front
v3 = new Vector3(startX, y, nextZ); // Top left front
v4 = new Vector3(startX + width, y, nextZ);// Top right front
v5 = new Vector3(startX, nextY, nextZ); // Top left back
v6 = new Vector3(startX + width, nextY, nextZ); // Top right back
v7 = new Vector3(startX, nextY, z); // Bottom left back
v8 = new Vector3(startX + width, nextY, z); // Bottom right back
}
}
// For the first step of the second flight, connect to the landing if landing vertices are provided
if (i == 0 && landingConnectLeft.HasValue && landingConnectRight.HasValue)
{
// Use the landing's vertices for the front bottom points of the first step
v1 = landingConnectLeft.Value; // Bottom left front (connects to landing)
v2 = landingConnectRight.Value; // Bottom right front (connects to landing)
}
var vh1 = mesh.AddVertex(v1);
var vh2 = mesh.AddVertex(v2);
var vh3 = mesh.AddVertex(v3);
var vh4 = mesh.AddVertex(v4);
var vh5 = mesh.AddVertex(v5);
var vh6 = mesh.AddVertex(v6);
var vh7 = mesh.AddVertex(v7);
var vh8 = mesh.AddVertex(v8);
// Store references for enclosure panels
if (i == 0)
{
leftSideVertices.Add(vh1);
leftSideVertices.Add(vh3);
rightSideVertices.Add(vh2);
rightSideVertices.Add(vh4);
bottomFaceVertices.Add(vh1);
bottomFaceVertices.Add(vh2);
bottomFaceVertices.Add(vh8);
bottomFaceVertices.Add(vh7);
}
if (i == steps - 1)
{
// Add vertices for top face of last step
topFaceVertices.Add(vh3);
topFaceVertices.Add(vh4);
topFaceVertices.Add(vh6);
topFaceVertices.Add(vh5);
// Add back vertices
backFaceVertices.Add(vh5);
backFaceVertices.Add(vh6);
backFaceVertices.Add(vh8);
backFaceVertices.Add(vh7);
// Complete left side vertices
leftSideVertices.Add(vh5);
leftSideVertices.Add(vh7);
// Complete right side vertices
rightSideVertices.Add(vh6);
rightSideVertices.Add(vh8);
}
if (perpendicular && !negativeDirection)
{
// For LEFT landing direction, we need to flip some of the face windings
// because the stairs are going in the positive Y axis direction
// Front face (vertical part of step) - facing front
mesh.AddFace(vh2, vh4, vh3, vh1);
// Top face (horizontal part of step) - facing up
mesh.AddFace(vh4, vh6, vh5, vh3);
// Back face of step - facing back (away from first flight)
mesh.AddFace(vh6, vh8, vh7, vh5);
// Left side face of step (which is actually the outer side in this orientation)
mesh.AddFace(vh2, vh4, vh6, vh8);
// Right side face of step (which is actually the inner side in this orientation)
mesh.AddFace(vh7, vh5, vh3, vh1);
// Bottom face of step - facing down
mesh.AddFace(vh7, vh8, vh2, vh1);
// If double sided is enabled, add reverse winding faces for inside visibility
if (_doubleSided)
{
// 1. Front face (reverse)
mesh.AddFace(vh1, vh3, vh4, vh2);
// 2. Top face (reverse)
mesh.AddFace(vh3, vh5, vh6, vh4);
// 3. Back face (reverse)
mesh.AddFace(vh5, vh7, vh8, vh6);
// 4. Left side face (reverse) (outer side)
mesh.AddFace(vh8, vh6, vh4, vh2);
// 5. Right side face (reverse) (inner side)
mesh.AddFace(vh1, vh3, vh5, vh7);
// 6. Bottom face (reverse)
mesh.AddFace(vh1, vh2, vh8, vh7);
}
}
else
{
// Standard winding order for both straight stairs and RIGHT landing direction
// Front face (vertical part of step) - facing front
mesh.AddFace(vh1, vh3, vh4, vh2);
// Top face (horizontal part of step) - facing up
mesh.AddFace(vh3, vh5, vh6, vh4);
// Back face of step - facing back
mesh.AddFace(vh5, vh7, vh8, vh6);
// Left side face of step - facing left
mesh.AddFace(vh1, vh3, vh5, vh7);
// Right side face of step - facing right
mesh.AddFace(vh2, vh4, vh6, vh8);
// Bottom face of step - facing down
mesh.AddFace(vh1, vh2, vh8, vh7);
// If double sided is enabled, add reverse winding faces for inside visibility
if (_doubleSided)
{
// 1. Front face (reverse)
mesh.AddFace(vh2, vh4, vh3, vh1);
// 2. Top face (reverse)
mesh.AddFace(vh4, vh6, vh5, vh3);
// 3. Back face (reverse)
mesh.AddFace(vh6, vh8, vh7, vh5);
// 4. Left side face (reverse)
mesh.AddFace(vh7, vh5, vh3, vh1);
// 5. Right side face (reverse)
mesh.AddFace(vh8, vh6, vh4, vh2);
// 6. Bottom face (reverse)
mesh.AddFace(vh7, vh8, vh2, vh1);
}
}
}
}
private void GenerateLandingPlatform(PolygonMesh mesh, float x, float z, float depth, float width, float height)
{
// Create a simple cube for the landing platform
// Define the 8 vertices of the landing platform
var v1 = new Vector3(x, -width/2, z); // Bottom left front
var v2 = new Vector3(x, width/2, z); // Bottom right front
var v3 = new Vector3(x, -width/2, z + height); // Top left front
var v4 = new Vector3(x, width/2, z + height); // Top right front
var v5 = new Vector3(x + depth, -width/2, z + height); // Top left back
var v6 = new Vector3(x + depth, width/2, z + height); // Top right back
var v7 = new Vector3(x + depth, -width/2, z); // Bottom left back
var v8 = new Vector3(x + depth, width/2, z); // Bottom right back
// Add vertices to the mesh
var vh1 = mesh.AddVertex(v1);
var vh2 = mesh.AddVertex(v2);
var vh3 = mesh.AddVertex(v3);
var vh4 = mesh.AddVertex(v4);
var vh5 = mesh.AddVertex(v5);
var vh6 = mesh.AddVertex(v6);
var vh7 = mesh.AddVertex(v7);
var vh8 = mesh.AddVertex(v8);
// Add faces with correct winding order to face outward
// Front face
mesh.AddFace(vh1, vh3, vh4, vh2);
// Top face
mesh.AddFace(vh3, vh5, vh6, vh4);
// Back face
mesh.AddFace(vh5, vh7, vh8, vh6);
// Left side face
mesh.AddFace(vh1, vh7, vh5, vh3);
// Right side face
mesh.AddFace(vh2, vh4, vh6, vh8);
// Bottom face
mesh.AddFace(vh1, vh2, vh8, vh7);
// Always add reverse winding faces for the landing platform to avoid see-through issues
// This ensures you won't see through the platform from any angle
// Front face (reverse)
mesh.AddFace(vh2, vh4, vh3, vh1);
// Top face (reverse)
mesh.AddFace(vh4, vh6, vh5, vh3);
// Back face (reverse)
mesh.AddFace(vh6, vh8, vh7, vh5);
// Left side face (reverse) - fixed winding order
mesh.AddFace(vh3, vh5, vh7, vh1);
// Right side face (reverse)
mesh.AddFace(vh8, vh6, vh4, vh2);
// Bottom face (reverse)
mesh.AddFace(vh7, vh8, vh2, vh1);
if (_doubleSided)
{
}
}
private void UpdateMeshColliderVisibility()
{
if (!GameObject.IsValid)
return;
Collider collider = GameObject.Components.Get<Collider>();
if (collider != null)
{
collider.Flags = _showMeshCollider ? ComponentFlags.None : ComponentFlags.Hidden;
}
}
}