Editor/InteriorLayoutBuilder/RoomLayoutGeometryBuilder.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace ReusableRoomLayout;
public sealed partial class RoomLayoutGeometryBuilder
{
public const string GeneratedRootName = "Generated Interior Layout";
private const string LegacyGeneratedRootName = "Generated Room Layout";
private const float TrimSurfaceEmbed = 0.25f;
public static IReadOnlyList<GameObject> FindGeneratedRoots( Scene scene )
{
return scene.GetAllObjects( true )
.Where( gameObject => gameObject.Name == GeneratedRootName || gameObject.Name == LegacyGeneratedRootName )
.ToArray();
}
public GameObject Build( Scene scene, RoomLayoutDocument document )
{
var root = CreateGroup( scene, null, GeneratedRootName );
foreach ( var floor in document.UsedFloors() )
{
BuildFloor( scene, root, document, floor );
}
return root;
}
private void BuildFloor( Scene scene, GameObject root, RoomLayoutDocument document, int floor )
{
var rooms = document.Rooms
.Where( room => document.FloorFor( room ) == floor )
.OrderBy( room => room.Id )
.ToArray();
var corridors = document.Corridors
.Where( corridor => document.FloorFor( corridor ) == floor && document.CorridorDoorsAreOnFloor( corridor ) )
.OrderBy( corridor => corridor.Id )
.ToArray();
if ( rooms.Length == 0 && corridors.Length == 0 )
{
return;
}
var floorRoot = CreateGroup( scene, root, GeneratedFloorGroupName( floor ) );
floorRoot.LocalPosition = new Vector3( 0.0f, 0.0f, document.Settings.EffectiveFloorSpacing * floor );
var floors = CreateGroup( scene, floorRoot, "Generated Floors" );
var walls = CreateGroup( scene, floorRoot, "Generated Walls" );
var frames = CreateGroup( scene, floorRoot, "Generated Frames" );
var floorSlabs = new List<RoomLayoutFloorSlab>();
var roofSlabs = new List<RoomLayoutFloorSlab>();
var thresholdSlabs = new List<RoomLayoutThresholdSlab>();
var solidSlabs = new List<RoomLayoutSolidSlab>();
foreach ( var room in rooms )
{
BuildRoomFloors( document.Settings, floorSlabs, roofSlabs, room );
BuildRoomWalls( scene, frames, floorSlabs, solidSlabs, document, room );
}
foreach ( var corridor in corridors )
{
BuildCorridor( scene, frames, document, corridor, floorSlabs, roofSlabs, thresholdSlabs, solidSlabs );
}
var cutoutRects = document.FloorCutouts
.Where( cutout => document.FloorFor( cutout ) == floor )
.Select( cutout => cutout.Bounds )
.ToArray();
var clippedFloorSlabs = ClipFloorSlabs( floorSlabs, cutoutRects );
var clippedThresholdSlabs = ClipThresholdSlabs( thresholdSlabs, cutoutRects );
BuildSolidGeometry( scene, walls, document.Settings, solidSlabs );
BuildFloorGeometry( scene, floors, document.Settings, clippedFloorSlabs );
var exposedRoofSlabs = document.Settings.RoofEnabled
? ExposedRoofSlabs( roofSlabs, HigherFloorCoverageRects( document, floor ) )
: Array.Empty<RoomLayoutFloorSlab>();
if ( exposedRoofSlabs.Count > 0 )
{
var roofs = CreateGroup( scene, floorRoot, "Generated Roofs" );
BuildRoofGeometry( scene, roofs, document.Settings, exposedRoofSlabs );
}
BuildThresholdGeometry( scene, floors, document.Settings, clippedThresholdSlabs );
}
private static string GeneratedFloorGroupName( int floor )
{
return floor >= 0
? $"Generated Floor {floor:00}"
: $"Generated Basement {Math.Abs( floor ):00}";
}
private static void BuildRoomFloors(
RoomLayoutSettings settings,
ICollection<RoomLayoutFloorSlab> floorSlabs,
ICollection<RoomLayoutFloorSlab> roofSlabs,
RoomLayoutRoom room )
{
AddStructuralFloorSlab(
floorSlabs,
$"{RoomName( room )} Floor",
room.Bounds,
settings,
RoomMaterialPath( settings, room, RoomLayoutSurface.Floor ),
RoomMaterialScale( settings, room, RoomLayoutSurface.Floor ) );
if ( settings.RoofEnabled )
{
AddStructuralRoofSlab(
roofSlabs,
$"{RoomName( room )} Roof",
room.Bounds,
settings,
RoomMaterialPath( settings, room, RoomLayoutSurface.Roof ),
RoomMaterialScale( settings, room, RoomLayoutSurface.Roof ) );
}
}
private static void BuildRoomWalls(
Scene scene,
GameObject frameParent,
ICollection<RoomLayoutFloorSlab> floorSlabs,
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutDocument document,
RoomLayoutRoom room )
{
foreach ( var side in Enum.GetValues<RoomLayoutWallSide>() )
{
var doors = document.Doors
.Where( door => door.RoomId == room.Id && door.Side == side )
.OrderBy( door => door.Offset )
.ToArray();
var windows = document.Windows
.Where( window => window.RoomId == room.Id && window.Side == side )
.OrderBy( window => window.Offset )
.ToArray();
var openings = WallOpeningsForSide( document, room, side, doors );
BuildWallSide( scene, frameParent, floorSlabs, solidSlabs, document, room, side, doors, windows, openings );
}
BuildRoomCorners( solidSlabs, document, room );
}
private static void BuildWallSide(
Scene scene,
GameObject frameParent,
ICollection<RoomLayoutFloorSlab> floorSlabs,
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutDocument document,
RoomLayoutRoom room,
RoomLayoutWallSide side,
IReadOnlyList<RoomLayoutDoor> doors,
IReadOnlyList<RoomLayoutWindow> windows,
IReadOnlyList<RoomLayoutWallOpening> openings )
{
var settings = document.Settings;
var length = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
? room.Bounds.Width
: room.Bounds.Height;
var spans = OpenWallSpans( settings, length, openings );
var wallIndex = 0;
foreach ( var span in spans )
{
if ( span.y - span.x < 1.0f )
{
continue;
}
wallIndex = BuildWallSpanWithWindows( scene, frameParent, solidSlabs, settings, room, side, windows, span.x, span.y, wallIndex );
}
if ( settings.BaseboardsEnabled )
{
var baseboardIndex = 0;
foreach ( var span in spans )
{
var extendStart = span.x <= 0.5f || IsCorridorConnectedDoorOpeningEdge( length, openings, span.x );
var extendEnd = span.y >= length - 0.5f || IsCorridorConnectedDoorOpeningEdge( length, openings, span.y );
baseboardIndex = BuildRoomBaseboard( solidSlabs, settings, room, side, span.x, span.y, extendStart, extendEnd, baseboardIndex );
}
}
foreach ( var door in doors )
{
if ( !IsDoorConnectedToCorridor( document, door.Id ) )
{
BuildStandaloneDoorFloor( floorSlabs, settings, room, side, door, length );
wallIndex = BuildStandaloneDoorHeader( solidSlabs, settings, room, side, door, length, wallIndex );
BuildDoorFrame( scene, frameParent, settings, room, door );
}
}
}
private static bool IsDoorConnectedToCorridor( RoomLayoutDocument document, int doorId )
{
return document.Corridors.Any( corridor => corridor.StartDoorId == doorId || corridor.EndDoorId == doorId );
}
private static bool IsCorridorConnectedDoorOpeningEdge(
float length,
IReadOnlyList<RoomLayoutWallOpening> openings,
float offset )
{
foreach ( var opening in openings )
{
if ( !opening.IsConnectedToCorridor )
{
continue;
}
var openStart = Math.Clamp( opening.Start, 0.0f, length );
var openEnd = Math.Clamp( opening.End, 0.0f, length );
if ( MathF.Abs( offset - openStart ) <= 0.5f || MathF.Abs( offset - openEnd ) <= 0.5f )
{
return true;
}
}
return false;
}
private static List<Vector2> OpenWallSpans( RoomLayoutSettings settings, float length, IReadOnlyList<RoomLayoutWallOpening> openings )
{
var spans = new List<Vector2>();
var cursor = 0.0f;
var minimumWallRun = MinimumWallRunLength( settings );
foreach ( var opening in openings.OrderBy( opening => opening.Start ) )
{
var openStart = Math.Clamp( opening.Start, 0.0f, length );
var openEnd = Math.Clamp( opening.End, 0.0f, length );
SnapOpeningToWallEnds( length, minimumWallRun, ref openStart, ref openEnd );
if ( openEnd - openStart < 1.0f )
{
continue;
}
if ( openStart - cursor >= minimumWallRun )
{
spans.Add( new Vector2( cursor, openStart ) );
}
cursor = MathF.Max( cursor, openEnd );
}
if ( length - cursor >= minimumWallRun )
{
spans.Add( new Vector2( cursor, length ) );
}
return spans;
}
private static IReadOnlyList<RoomLayoutWallOpening> WallOpeningsForSide(
RoomLayoutDocument document,
RoomLayoutRoom room,
RoomLayoutWallSide side,
IReadOnlyList<RoomLayoutDoor> ownDoors )
{
var openings = new List<RoomLayoutWallOpening>();
foreach ( var door in ownDoors )
{
var halfWidth = DoorOpeningWidth( document, door ) * 0.5f;
openings.Add( new RoomLayoutWallOpening(
door.Offset - halfWidth,
door.Offset + halfWidth,
IsDoorConnectedToCorridor( document, door.Id ) ) );
}
foreach ( var otherRoom in document.Rooms )
{
if ( otherRoom.Id == room.Id ||
document.FloorFor( otherRoom ) != document.FloorFor( room ) ||
!TryGetSharedWall( room, side, otherRoom, out var otherSide, out var sharedStart, out var sharedEnd ) )
{
continue;
}
foreach ( var door in document.Doors.Where( door => door.RoomId == otherRoom.Id && door.Side == otherSide ) )
{
var offset = SharedWallDoorOffset( room, side, otherRoom, door.Offset );
var halfWidth = DoorOpeningWidth( document, door ) * 0.5f;
var start = MathF.Max( sharedStart, offset - halfWidth );
var end = MathF.Min( sharedEnd, offset + halfWidth );
if ( end - start < 1.0f )
{
continue;
}
openings.Add( new RoomLayoutWallOpening(
start,
end,
IsDoorConnectedToCorridor( document, door.Id ) ) );
}
}
return openings;
}
private static bool TryGetSharedWall(
RoomLayoutRoom room,
RoomLayoutWallSide side,
RoomLayoutRoom otherRoom,
out RoomLayoutWallSide otherSide,
out float sharedStart,
out float sharedEnd )
{
otherSide = OppositeSide( side );
sharedStart = 0.0f;
sharedEnd = 0.0f;
var bounds = room.Bounds;
var otherBounds = otherRoom.Bounds;
var sharesWallLine = side switch
{
RoomLayoutWallSide.North => (bounds.Y + bounds.Height).AlmostEqual( otherBounds.Y ),
RoomLayoutWallSide.South => bounds.Y.AlmostEqual( otherBounds.Y + otherBounds.Height ),
RoomLayoutWallSide.East => (bounds.X + bounds.Width).AlmostEqual( otherBounds.X ),
_ => bounds.X.AlmostEqual( otherBounds.X + otherBounds.Width )
};
if ( !sharesWallLine )
{
return false;
}
var roomMin = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? bounds.X : bounds.Y;
var roomMax = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? bounds.X + bounds.Width : bounds.Y + bounds.Height;
var otherMin = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? otherBounds.X : otherBounds.Y;
var otherMax = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South ? otherBounds.X + otherBounds.Width : otherBounds.Y + otherBounds.Height;
sharedStart = MathF.Max( roomMin, otherMin ) - roomMin;
sharedEnd = MathF.Min( roomMax, otherMax ) - roomMin;
return sharedEnd - sharedStart >= 1.0f;
}
private static float SharedWallDoorOffset( RoomLayoutRoom room, RoomLayoutWallSide side, RoomLayoutRoom otherRoom, float otherOffset )
{
return side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
? otherRoom.Bounds.X + otherOffset - room.Bounds.X
: otherRoom.Bounds.Y + otherOffset - room.Bounds.Y;
}
private static RoomLayoutWallSide OppositeSide( RoomLayoutWallSide side )
{
return side switch
{
RoomLayoutWallSide.North => RoomLayoutWallSide.South,
RoomLayoutWallSide.South => RoomLayoutWallSide.North,
RoomLayoutWallSide.East => RoomLayoutWallSide.West,
_ => RoomLayoutWallSide.East
};
}
private static float DoorOpeningWidth( RoomLayoutDocument document, RoomLayoutDoor door )
{
var connectedWidth = 0.0f;
foreach ( var corridor in document.Corridors )
{
if ( corridor.StartDoorId != door.Id && corridor.EndDoorId != door.Id )
{
continue;
}
connectedWidth = MathF.Max( connectedWidth, CorridorClearWidth( document, corridor ) );
}
return connectedWidth > 0.0f
? MathF.Min( door.Width, connectedWidth )
: door.Width;
}
private static DoorOpeningData DoorOpeningSpan( RoomLayoutSettings settings, RoomLayoutDoor door, float wallLength )
{
var minimumWallRun = MinimumWallRunLength( settings );
var halfDoor = door.Width * 0.5f;
var start = Math.Clamp( door.Offset - halfDoor, 0.0f, wallLength );
var end = Math.Clamp( door.Offset + halfDoor, 0.0f, wallLength );
SnapOpeningToWallEnds( wallLength, minimumWallRun, ref start, ref end );
return new DoorOpeningData( start, end, end - start );
}
private static void SnapOpeningToWallEnds( float wallLength, float minimumWallRun, ref float start, ref float end )
{
if ( start < minimumWallRun )
{
start = 0.0f;
}
if ( wallLength - end < minimumWallRun )
{
end = wallLength;
}
}
private static float MinimumWallRunLength( RoomLayoutSettings settings )
{
return MathF.Max( 1.0f, settings.WallThickness );
}
private static int BuildWallSpanWithWindows(
Scene scene,
GameObject frameParent,
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWallSide side,
IReadOnlyList<RoomLayoutWindow> windows,
float start,
float end,
int index )
{
var cursor = start;
var cursorTouchesOpening = false;
var minimumWallRun = MinimumWallRunLength( settings );
foreach ( var window in windows )
{
var halfWidth = window.Width * 0.5f;
var openStart = Math.Clamp( window.Offset - halfWidth, start, end );
var openEnd = Math.Clamp( window.Offset + halfWidth, start, end );
if ( openEnd - openStart < 1.0f )
{
continue;
}
if ( openStart - start < minimumWallRun )
{
openStart = start;
}
if ( end - openEnd < minimumWallRun )
{
openEnd = end;
}
if ( openStart - cursor >= minimumWallRun )
{
index = BuildWallModules( solidSlabs, settings, room, side, cursor, openStart, index, cursorTouchesOpening, true );
}
index = BuildWindowWallOpening(
scene,
frameParent,
solidSlabs,
settings,
room,
side,
window,
MathF.Max( cursor, openStart ),
openEnd,
index );
cursor = MathF.Max( cursor, openEnd );
cursorTouchesOpening = true;
}
if ( end - cursor >= minimumWallRun )
{
index = BuildWallModules( solidSlabs, settings, room, side, cursor, end, index, cursorTouchesOpening, false );
}
return index;
}
private static int BuildWindowWallOpening(
Scene scene,
GameObject frameParent,
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWallSide side,
RoomLayoutWindow window,
float start,
float end,
int index )
{
if ( end - start < 1.0f )
{
return index;
}
var sillHeight = EffectiveWindowSillHeight( settings, window );
var openingHeight = EffectiveWindowHeight( settings, window, sillHeight );
var windowTop = MathF.Min( settings.WallHeight, sillHeight + openingHeight );
if ( sillHeight > 0.5f )
{
index = BuildWallBodySection( solidSlabs, settings, room, side, start, end, 0.0f, sillHeight, false, index, true, true );
}
if ( settings.WallHeight - windowTop > 0.5f )
{
index = BuildWallBodySection( solidSlabs, settings, room, side, start, end, windowTop, settings.WallHeight, true, index, true, true );
}
BuildWindowFrame( scene, frameParent, settings, room, window, start, end, sillHeight, openingHeight );
return index;
}
private static int BuildWallModules(
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWallSide side,
float start,
float end,
int index,
bool revealStartOuter = false,
bool revealEndOuter = false )
{
return BuildWallBodySection( solidSlabs, settings, room, side, start, end, 0.0f, settings.WallHeight, true, index, revealStartOuter, revealEndOuter );
}
private static int BuildStandaloneDoorHeader(
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWallSide side,
RoomLayoutDoor door,
float wallLength,
int index )
{
var span = DoorOpeningSpan( settings, door, wallLength );
if ( span.OpeningWidth < 1.0f )
{
return index;
}
var openingTop = EffectiveDoorHeight( settings, door );
if ( settings.WallHeight - openingTop < 0.5f )
{
return index;
}
return BuildWallBodySection(
solidSlabs,
settings,
room,
side,
span.Start,
span.End,
openingTop,
settings.WallHeight,
true,
index,
true,
true );
}
private static void BuildStandaloneDoorFloor(
ICollection<RoomLayoutFloorSlab> floorSlabs,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWallSide side,
RoomLayoutDoor door,
float wallLength )
{
var span = DoorOpeningSpan( settings, door, wallLength );
if ( span.OpeningWidth < 1.0f )
{
return;
}
var box = WallBox(
room.Bounds,
side,
span.Start + span.OpeningWidth * 0.5f,
span.OpeningWidth,
settings.WallThickness,
settings.FloorThickness,
0.0f );
var rect = new RoomLayoutRect(
box.Center.x - box.Size.x * 0.5f,
box.Center.y - box.Size.y * 0.5f,
box.Size.x,
box.Size.y );
AddFloorSlab(
floorSlabs,
$"{RoomName( room )} Door {door.Id:00} Floor",
rect,
RoomMaterialPath( settings, room, RoomLayoutSurface.Floor ),
RoomMaterialScale( settings, room, RoomLayoutSurface.Floor ) );
}
private static int BuildWallBodySection(
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWallSide side,
float start,
float end,
float zMin,
float zMax,
bool includeTopCap,
int index,
bool revealStartOuter = false,
bool revealEndOuter = false )
{
var length = end - start;
var height = zMax - zMin;
if ( length < 1.0f || height < 0.5f )
{
return index;
}
var centerAlong = start + length * 0.5f;
var outerWallMaterialPath = RoomOuterWallMaterialPath( settings, room );
var innerWallMaterialPath = RoomInnerWallMaterialPath( settings, room );
var outerWallMaterialScale = RoomOuterWallMaterialScale( settings, room );
var innerWallMaterialScale = RoomInnerWallMaterialScale( settings, room );
var capMaterialPath = RoomMaterialPath( settings, room, RoomLayoutSurface.Cap );
var capMaterialScale = RoomMaterialScale( settings, room, RoomLayoutSurface.Cap );
var body = WallBox( room.Bounds, side, centerAlong, length, settings.WallThickness, height, zMin + height * 0.5f );
AddRoomWallSolidSlab(
solidSlabs,
$"{RoomName( room )} Wall {side} {index:00}",
body.Center,
body.Size,
side,
outerWallMaterialPath,
innerWallMaterialPath,
outerWallMaterialScale,
innerWallMaterialScale,
revealStartOuter,
revealEndOuter );
if ( !includeTopCap )
{
return index + 1;
}
var cap = WallBox(
room.Bounds,
side,
centerAlong,
length,
settings.WallThickness,
settings.WallCapHeight,
settings.WallHeight + settings.WallCapHeight * 0.5f );
AddSolidSlab(
solidSlabs,
$"{RoomName( room )} Wall {side} {index:00} Cap",
cap.Center,
cap.Size,
RoomLayoutSurface.Cap,
materialPath: capMaterialPath,
textureWorldSize: capMaterialScale );
return index + 1;
}
private static void AddRoomWallSolidSlab(
ICollection<RoomLayoutSolidSlab> solidSlabs,
string name,
Vector3 center,
Vector3 size,
RoomLayoutWallSide side,
string outerMaterialPath,
string innerMaterialPath,
float outerMaterialScale,
float innerMaterialScale,
bool revealStartOuter,
bool revealEndOuter )
{
AddSolidSlab(
solidSlabs,
name,
center,
size,
RoomLayoutSurface.Wall,
materialPath: outerMaterialPath,
textureWorldSize: outerMaterialScale,
northMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.North, outerMaterialPath, innerMaterialPath ),
southMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.South, outerMaterialPath, innerMaterialPath ),
eastMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.East, outerMaterialPath, innerMaterialPath ),
westMaterialPath: RoomWallFaceMaterialPath( side, revealStartOuter, revealEndOuter, CardinalDirection.West, outerMaterialPath, innerMaterialPath ),
northTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.North, outerMaterialScale, innerMaterialScale ),
southTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.South, outerMaterialScale, innerMaterialScale ),
eastTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.East, outerMaterialScale, innerMaterialScale ),
westTextureWorldSize: RoomWallFaceMaterialScale( side, revealStartOuter, revealEndOuter, CardinalDirection.West, outerMaterialScale, innerMaterialScale ) );
}
private static string RoomWallFaceMaterialPath(
RoomLayoutWallSide side,
bool revealStartOuter,
bool revealEndOuter,
CardinalDirection face,
string outerMaterialPath,
string innerMaterialPath )
{
return IsRoomWallOuterFace( side, revealStartOuter, revealEndOuter, face )
? outerMaterialPath
: innerMaterialPath;
}
private static float RoomWallFaceMaterialScale(
RoomLayoutWallSide side,
bool revealStartOuter,
bool revealEndOuter,
CardinalDirection face,
float outerMaterialScale,
float innerMaterialScale )
{
return IsRoomWallOuterFace( side, revealStartOuter, revealEndOuter, face )
? outerMaterialScale
: innerMaterialScale;
}
private static bool IsRoomWallOuterFace(
RoomLayoutWallSide side,
bool revealStartOuter,
bool revealEndOuter,
CardinalDirection face )
{
return side switch
{
RoomLayoutWallSide.North => face switch
{
CardinalDirection.North => true,
CardinalDirection.West => revealStartOuter,
CardinalDirection.East => revealEndOuter,
_ => false
},
RoomLayoutWallSide.South => face switch
{
CardinalDirection.South => true,
CardinalDirection.West => revealStartOuter,
CardinalDirection.East => revealEndOuter,
_ => false
},
RoomLayoutWallSide.East => face switch
{
CardinalDirection.East => true,
CardinalDirection.South => revealStartOuter,
CardinalDirection.North => revealEndOuter,
_ => false
},
_ => face switch
{
CardinalDirection.West => true,
CardinalDirection.South => revealStartOuter,
CardinalDirection.North => revealEndOuter,
_ => false
}
};
}
private static void BuildDoorFrame(
Scene scene,
GameObject parent,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutDoor door )
{
var length = door.Side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
? room.Bounds.Width
: room.Bounds.Height;
var span = DoorOpeningSpan( settings, door, length );
if ( span.OpeningWidth < 1.0f )
{
return;
}
var jamb = Math.Clamp( settings.DoorFrameThickness, 1.0f, MathF.Max( 1.0f, span.OpeningWidth * 0.45f ) );
var height = EffectiveDoorHeight( settings, door );
if ( height < 0.5f )
{
return;
}
var left = WallBox( room.Bounds, door.Side, span.Start + jamb * 0.5f, jamb, settings.WallThickness, height, height * 0.5f );
var right = WallBox( room.Bounds, door.Side, span.End - jamb * 0.5f, jamb, settings.WallThickness, height, height * 0.5f );
var railHeight = MathF.Min( height, Math.Clamp( settings.DoorFrameThickness, 1.0f, MathF.Max( 1.0f, height * 0.45f ) ) );
var header = WallBox( room.Bounds, door.Side, span.Start + span.OpeningWidth * 0.5f, span.OpeningWidth, settings.WallThickness, railHeight, height - railHeight * 0.5f );
var materialPath = FirstMaterialPath( door.DoorFrameMaterialPath, settings.DoorFrameMaterialPath );
var textureWorldSize = MaterialScaleOrFallback( door.DoorFrameMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.DoorFrame ) );
CreateBox( scene, parent, $"{RoomName( room )} Door {door.Id:00} Jamb A", left.Center, left.Size, settings, RoomLayoutSurface.DoorFrame, materialPath, textureWorldSize );
CreateBox( scene, parent, $"{RoomName( room )} Door {door.Id:00} Jamb B", right.Center, right.Size, settings, RoomLayoutSurface.DoorFrame, materialPath, textureWorldSize );
CreateBox( scene, parent, $"{RoomName( room )} Door {door.Id:00} Header", header.Center, header.Size, settings, RoomLayoutSurface.DoorFrame, materialPath, textureWorldSize );
}
private static void BuildWindowFrame(
Scene scene,
GameObject parent,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWindow window,
float start,
float end,
float sillHeight,
float openingHeight )
{
var width = end - start;
var jamb = Math.Clamp( settings.WindowFrameThickness, 0.1f, MathF.Max( 0.1f, width * 0.25f ) );
var railHeight = Math.Clamp( settings.WindowFrameThickness, 0.1f, MathF.Max( 0.1f, openingHeight * 0.25f ) );
var depth = TrimDepth( settings, settings.WindowFrameThickness );
var center = (start + end) * 0.5f;
var verticalCenter = sillHeight + openingHeight * 0.5f;
var sillCenter = sillHeight + railHeight * 0.5f;
var headerCenter = sillHeight + MathF.Max( railHeight * 0.5f, openingHeight - railHeight * 0.5f );
var materialPath = FirstMaterialPath( window.WindowFrameMaterialPath, settings.WindowFrameMaterialPath );
var textureWorldSize = MaterialScaleOrFallback( window.WindowFrameMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.WindowFrame ) );
var left = InteriorTrimBox( room.Bounds, window.Side, start + jamb * 0.5f, jamb, depth, openingHeight, verticalCenter );
var right = InteriorTrimBox( room.Bounds, window.Side, end - jamb * 0.5f, jamb, depth, openingHeight, verticalCenter );
var sill = InteriorTrimBox( room.Bounds, window.Side, center, width, depth, railHeight, sillCenter );
var header = InteriorTrimBox( room.Bounds, window.Side, center, width, depth, railHeight, headerCenter );
CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Jamb A", left.Center, left.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Jamb B", right.Center, right.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Sill", sill.Center, sill.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
CreateBox( scene, parent, $"{RoomName( room )} Window {window.Id:00} Header", header.Center, header.Size, settings, RoomLayoutSurface.WindowFrame, materialPath, textureWorldSize );
}
private static float EffectiveDoorHeight( RoomLayoutSettings settings, RoomLayoutDoor door )
{
var height = door.Height > 0.0f ? door.Height : settings.DoorHeight;
return Math.Clamp( height, 0.0f, settings.WallHeight );
}
private static float EffectiveWindowSillHeight( RoomLayoutSettings settings, RoomLayoutWindow window )
{
var sillHeight = window.SillHeight >= 0.0f ? window.SillHeight : settings.WindowSillHeight;
return Math.Clamp( sillHeight, 0.0f, MathF.Max( 0.0f, settings.WallHeight - 1.0f ) );
}
private static float EffectiveWindowHeight( RoomLayoutSettings settings, RoomLayoutWindow window, float sillHeight )
{
var height = window.Height > 0.0f ? window.Height : settings.WindowHeight;
return Math.Clamp( height, 1.0f, MathF.Max( 1.0f, settings.WallHeight - sillHeight ) );
}
private static int BuildRoomBaseboard(
ICollection<RoomLayoutSolidSlab> solidSlabs,
RoomLayoutSettings settings,
RoomLayoutRoom room,
RoomLayoutWallSide side,
float start,
float end,
bool extendStart,
bool extendEnd,
int index )
{
var length = end - start;
if ( length < 1.0f || settings.BaseboardHeight < 0.5f || settings.BaseboardDepth < 0.5f )
{
return index;
}
var startOverlap = extendStart ? BaseboardRunOverlap( settings ) : 0.0f;
var endOverlap = extendEnd ? BaseboardRunOverlap( settings ) : 0.0f;
var centerAlong = (start - startOverlap + end + endOverlap) * 0.5f;
length += startOverlap + endOverlap;
var materialPath = RoomMaterialPath( settings, room, RoomLayoutSurface.Baseboard );
var textureWorldSize = RoomMaterialScale( settings, room, RoomLayoutSurface.Baseboard );
var data = BaseboardBox( room.Bounds, side, centerAlong, length, settings.BaseboardDepth, settings.BaseboardHeight );
AddSolidSlab(
solidSlabs,
$"{RoomName( room )} Baseboard {side} {index:00}",
data.Center,
data.Size,
RoomLayoutSurface.Baseboard,
hasCollider: false,
materialPath: materialPath,
textureWorldSize: textureWorldSize );
return index + 1;
}
private static WallBoxData WallBox(
RoomLayoutRect bounds,
RoomLayoutWallSide side,
float centerAlong,
float length,
float thickness,
float height,
float centerZ )
{
return side switch
{
RoomLayoutWallSide.North => new WallBoxData(
new Vector3( bounds.X + centerAlong, bounds.Y + bounds.Height + thickness * 0.5f, centerZ ),
new Vector3( length, thickness, height ) ),
RoomLayoutWallSide.South => new WallBoxData(
new Vector3( bounds.X + centerAlong, bounds.Y - thickness * 0.5f, centerZ ),
new Vector3( length, thickness, height ) ),
RoomLayoutWallSide.East => new WallBoxData(
new Vector3( bounds.X + bounds.Width + thickness * 0.5f, bounds.Y + centerAlong, centerZ ),
new Vector3( thickness, length, height ) ),
_ => new WallBoxData(
new Vector3( bounds.X - thickness * 0.5f, bounds.Y + centerAlong, centerZ ),
new Vector3( thickness, length, height ) )
};
}
private static WallBoxData BaseboardBox(
RoomLayoutRect bounds,
RoomLayoutWallSide side,
float centerAlong,
float length,
float depth,
float height )
{
var centerZ = height * 0.5f;
var embed = TrimEmbedForDepth( depth );
return side switch
{
RoomLayoutWallSide.North => new WallBoxData(
new Vector3( bounds.X + centerAlong, bounds.Y + bounds.Height - depth * 0.5f + embed, centerZ ),
new Vector3( length, depth, height ) ),
RoomLayoutWallSide.South => new WallBoxData(
new Vector3( bounds.X + centerAlong, bounds.Y + depth * 0.5f - embed, centerZ ),
new Vector3( length, depth, height ) ),
RoomLayoutWallSide.East => new WallBoxData(
new Vector3( bounds.X + bounds.Width - depth * 0.5f + embed, bounds.Y + centerAlong, centerZ ),
new Vector3( depth, length, height ) ),
_ => new WallBoxData(
new Vector3( bounds.X + depth * 0.5f - embed, bounds.Y + centerAlong, centerZ ),
new Vector3( depth, length, height ) )
};
}
private static WallBoxData InteriorTrimBox(
RoomLayoutRect bounds,
RoomLayoutWallSide side,
float centerAlong,
float length,
float depth,
float height,
float centerZ )
{
var embed = TrimEmbedForDepth( depth );
return side switch
{
RoomLayoutWallSide.North => new WallBoxData(
new Vector3( bounds.X + centerAlong, bounds.Y + bounds.Height - depth * 0.5f + embed, centerZ ),
new Vector3( length, depth, height ) ),
RoomLayoutWallSide.South => new WallBoxData(
new Vector3( bounds.X + centerAlong, bounds.Y + depth * 0.5f - embed, centerZ ),
new Vector3( length, depth, height ) ),
RoomLayoutWallSide.East => new WallBoxData(
new Vector3( bounds.X + bounds.Width - depth * 0.5f + embed, bounds.Y + centerAlong, centerZ ),
new Vector3( depth, length, height ) ),
_ => new WallBoxData(
new Vector3( bounds.X + depth * 0.5f - embed, bounds.Y + centerAlong, centerZ ),
new Vector3( depth, length, height ) )
};
}
private static float TrimDepth( RoomLayoutSettings settings, float preferred )
{
return Math.Clamp( preferred, 0.5f, MathF.Max( 0.5f, settings.WallThickness ) );
}
private static float TrimEmbedForDepth( float depth )
{
return MathF.Min( TrimSurfaceEmbed, depth * 0.25f );
}
private static float BaseboardRunOverlap( RoomLayoutSettings settings )
{
if ( settings.BaseboardDepth < 0.5f )
{
return 0.0f;
}
var overlap = settings.BaseboardDepth - TrimEmbedForDepth( settings.BaseboardDepth );
return Math.Clamp( overlap, 0.0f, MathF.Max( 0.0f, settings.WallThickness ) );
}
private static GameObject CreateGroup( Scene scene, GameObject parent, string name )
{
var group = scene.CreateObject( true );
group.Name = name;
if ( parent is not null )
{
group.SetParent( parent, false );
}
return group;
}
private static string RoomName( RoomLayoutRoom room )
{
return string.IsNullOrWhiteSpace( room.Name ) ? $"Room {room.Id:00}" : room.Name;
}
private readonly record struct DoorOpeningData( float Start, float End, float OpeningWidth );
private readonly record struct RoomLayoutWallOpening( float Start, float End, bool IsConnectedToCorridor );
private readonly record struct WallBoxData( Vector3 Center, Vector3 Size );
}