Editor/Floor/ArchFloorGen.Storey.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchFloorGen {
public static void Storey( ArchMesh canvas, ArchFloorGroup group, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style ) {
var lead = group.Lead;
var chain = new[] { lead.Palette, building.Palette };
var thickness = kit.FloorThickness;
var trim = style.Brush( ArchSurface.Trim, chain );
var cutouts = ArchCut.Stored( building, group.Level ).ToList();
var walls = building.Rooms.SelectMany( room => room.Walls ).ToList();
var above = ArchCut.Stored( building, group.Level + 1 ).ToList();
if ( lead.HasFloor ) {
var top = lead.BaseHeight;
var structure = SeatedShell( group.Region, kit );
cutouts.AddRange( ArchCut.Holes( plan, group.Level, kit, top - thickness, top, building.Id, ArchCutAffects.Floors, lead.Id ) );
Solid( canvas, structure, cutouts, top - thickness, top, style.Brush( ArchSurface.Floor, chain ) );
WellTrim( canvas, cutouts, top - thickness, top, kit, trim, ArchWellEdges.Trimmed( building, kit, cutouts, top ), walls, group.Region );
if ( lead.FloorBoards ) {
Boards( canvas, group, cutouts, kit, top, style.Brush( ArchSurface.Deck, chain ) );
}
}
foreach ( var ceiling in Ceilings( group, building, kit, style ).Where( Builds( building ) ) ) {
Ceiling( canvas, ceiling, building, plan, kit, style, above, walls );
}
}
static Func<ArchFloorGroup, bool> Builds( ArchBuilding building ) {
return group => group.Lead.HasCeiling && !CeilingHeldAbove( building, group.Lead );
}
static void Ceiling(
ArchMesh canvas,
ArchFloorGroup ceiling,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
ArchStyle style,
List<ArchFloorCutout> above,
List<ArchWall> walls ) {
var lead = ceiling.Lead;
var chain = new[] { lead.Palette, building.Palette };
var plate = lead.BaseHeight + ceiling.WallHeight;
var soffit = plate - CeilingDepth( lead, kit );
var trim = style.Brush( ArchSurface.Trim, chain );
// Lapped into whatever stands over the plate to avoid z-fighting
var head = plate + ArchLap.Bite( kit );
var (wells, recesses) = Bites( plan, ceiling.Level, kit, soffit, head, building.Id, ArchCutAffects.Ceilings, lead.Id );
var body = Body( ceiling, kit );
var ceilingBrush = style.Brush( ArchSurface.Ceiling, chain );
var damage = ArchCut.Damage( plan, ceiling.Level, kit, soffit, head, building.Id, ArchCutAffects.Ceilings, lead.Id )
.Where( found => found.Cut.ResolvedDamage == ArchDamageKind.SurfaceSpall || ArchDamage.Panels( found.Cut.ResolvedDamage ) )
.ToList();
wells.AddRange( above );
foreach ( var (cut, volume) in damage ) {
var damageTop = ArchDamage.Panels( cut.ResolvedDamage )
? head + ArchLap.Bite( kit )
: MathF.Min( head, soffit + MathF.Max( 0.5f, cut.DamageDepth ) );
recesses.Add( ArchCarveVolume.Over( volume.Footprint, soffit - ArchLap.Bite( kit ), damageTop ) );
}
Solid( canvas, body, wells, soffit, head, ceilingBrush, recesses );
foreach ( var (cut, volume) in damage ) {
if ( ArchDamage.Displaced( cut, volume ) ) {
ArchDamage.DisplacedPanel( canvas, cut, volume, soffit, ceilingBrush );
}
}
WellTrim( canvas, wells, soffit, head, kit, trim, ArchWellEdges.Trimmed( building, kit, above, head ), walls, body );
Cornice( canvas, ceiling, wells, kit, soffit, trim );
}
// Passage ceiling body held a bite short at each mouth to avoid z-fighting with the host wall
static List<List<Vector2>> Body( ArchFloorGroup ceiling, ArchKit kit ) {
if ( !ceiling.Lead.Spans ) {
return SeatedShell( ceiling.Region, kit );
}
ArchFootprint.Bounds( Footprint( ceiling.Lead ), out var min, out var max );
var bite = ArchLap.Bite( kit );
var over = new Vector2( kit.WallThickness, kit.WallThickness );
var mouths = ArchAsks.WalkwayRunsAlongX( ceiling.Lead )
? new[]
{
ArchFootprint.Rect( min - over, new Vector2( min.x + bite, max.y + over.y ) ),
ArchFootprint.Rect( new Vector2( max.x - bite, min.y - over.y ), max + over )
}
: new[]
{
ArchFootprint.Rect( min - over, new Vector2( max.x + over.x, min.y + bite ) ),
ArchFootprint.Rect( new Vector2( min.x - over.x, max.y - bite ), max + over )
};
return ArchFootprint.Subtract( ceiling.Region, mouths );
}
static List<List<Vector2>> SeatedShell( IReadOnlyList<List<Vector2>> region, ArchKit kit ) {
return ArchRegion.Shell( region, MathF.Max( 0f, kit.WallThickness - ArchLap.Bite( kit ) * 2f ) );
}
static void Cornice( ArchMesh canvas, ArchFloorGroup ceiling, List<ArchFloorCutout> wells, ArchKit kit, float soffit, ArchBrush trim ) {
if ( !ceiling.Lead.Spans ) {
ArchCornice.Around( canvas, ceiling.Region, wells, kit, soffit, trim );
return;
}
ArchCornice.AlongPassage( canvas, ceiling.Region, ArchAsks.WalkwayRunsAlongX( ceiling.Lead ), wells, kit, soffit, trim );
}
static void Boards( ArchMesh canvas, ArchFloorGroup group, List<ArchFloorCutout> cutouts, ArchKit kit, float floor, ArchBrush brush ) {
var spec = new ArchPlankSpec {
Width = kit.FloorBoardWidth,
Gap = kit.FloorBoardGap,
Thickness = kit.FloorBoardThickness,
Length = kit.FloorBoardLength,
Top = floor + kit.FloorBoardThickness,
Yaw = group.Lead.FloorBoardYaw
};
ArchPlanks.Fill( canvas, group.Region, cutouts, spec, brush );
}
public static void Solid(
ArchMesh canvas,
IReadOnlyList<List<Vector2>> region,
IReadOnlyList<ArchFloorCutout> cutouts,
float bottom,
float top,
ArchBrush brush,
IReadOnlyList<ArchCarveVolume> recesses = null ) {
var holes = Holes( region );
if ( cutouts is { Count: > 0 } ) {
holes.AddRange( cutouts );
}
foreach ( var loop in ArchFootprint.Outer( region ) ) {
Slab( canvas, loop, holes, bottom, top, brush, recesses );
}
}
// Splits cuts into full-through wells vs partial recesses (coffers)
public static (List<ArchFloorCutout> Wells, List<ArchCarveVolume> Recesses) Bites(
ArchPlan plan,
int level,
ArchKit kit,
float bottom,
float top,
int hostId,
ArchCutAffects target,
int standing = 0 ) {
var wells = new List<ArchFloorCutout>();
var recesses = new List<ArchCarveVolume>();
foreach ( var volume in ArchCut.Volumes( plan, level, kit, bottom, top, hostId, target, standing ) ) {
if ( !ArchCut.Pierces( volume, bottom, top ) ) {
recesses.Add( volume );
continue;
}
var well = new ArchFloorCutout { Level = level, Break = volume.Break };
well.Reshape( volume.Footprint.ToList() );
wells.Add( well );
}
return (wells, recesses);
}
static List<ArchFloorCutout> Holes( IReadOnlyList<List<Vector2>> region ) {
var holes = new List<ArchFloorCutout>();
foreach ( var loop in region.Where( loop => loop.Count >= 3 && ArchFootprint.SignedArea( loop ) < 0f ) ) {
var hole = new ArchFloorCutout();
hole.Reshape( loop );
holes.Add( hole );
}
return holes;
}
// Runs along the union boundary of holes to avoid doubled edges
public static void WellTrim(
ArchMesh canvas,
IReadOnlyList<ArchFloorCutout> cutouts,
float bottom,
float top,
ArchKit kit,
ArchBrush brush,
Func<Vector2, bool> handedOver = null,
IReadOnlyList<ArchWall> walls = null,
IReadOnlyList<IReadOnlyList<Vector2>> region = null ) {
var bite = ArchLap.Bite( kit );
var proud = ArchLap.Proud( kit );
var depth = MathF.Max( 0f, kit.WellTrimDepth );
if ( cutouts is null || depth < 0.05f ) {
return;
}
foreach ( var loop in Boundary( cutouts ) ) {
var slab = ArchFootprint.FloorSide( loop );
for ( var index = 0; index < loop.Count; index++ ) {
var a = loop[index];
var b = loop[(index + 1) % loop.Count];
var run = b - a;
if ( run.Length < 0.5f ) {
continue;
}
var unit = run.Normal;
var outward = new Vector2( -unit.y, unit.x ) * slab;
// Folded onto bisector at corners for continuous ring
var opening = ArchBandGen.Fold( Reaching( loop, index, -1, unit ), unit, slab );
var closing = ArchBandGen.Fold( unit, Reaching( loop, index, 1, unit ), slab );
// Clipped to the slab boundary, not sampled at midpoint
foreach ( var (from, to) in Dressed( region, a, b, outward, bite ) ) {
var start = Vector2.Lerp( a, b, from );
var finish = Vector2.Lerp( a, b, to );
var middle = (start + finish) * 0.5f;
var foldFrom = from < 0.001f ? opening : 0f;
var foldTo = to > 0.999f ? closing : 0f;
if ( ArchProbe.Against( walls, kit, middle, outward ) ) {
continue;
}
if ( handedOver?.Invoke( middle ) != true ) {
Board( canvas, start, finish, outward, -proud, depth, top - bite, top + proud, brush, foldFrom, foldTo );
}
Board( canvas, start, finish, outward, -proud * 0.5f, bite, bottom, top - bite, brush, foldFrom, foldTo );
}
}
}
}
public static bool Cutting( IReadOnlyList<IReadOnlyList<Vector2>> region, Vector2 at, Vector2 outward, float bite ) {
return region is null || ArchFootprint.Encloses( region, at + outward * MathF.Max( bite, 0.05f ) );
}
public static IEnumerable<(float From, float To)> Dressed(
IReadOnlyList<IReadOnlyList<Vector2>> region,
Vector2 a,
Vector2 b,
Vector2 outward,
float bite ) {
if ( region is null ) {
yield return (0f, 1f);
yield break;
}
var step = outward * MathF.Max( bite, 0.05f );
var cuts = new List<float> { 0f, 1f };
foreach ( var loop in region ) {
cuts.AddRange( ArchFootprint.Crossings( loop, a + step, b + step ) );
}
cuts.Sort();
for ( var index = 0; index + 1 < cuts.Count; index++ ) {
var from = cuts[index];
var to = cuts[index + 1];
if ( to - from > 0.001f && Cutting( region, Vector2.Lerp( a, b, (from + to) * 0.5f ), outward, bite ) ) {
yield return (from, to);
}
}
}
static IEnumerable<List<Vector2>> Boundary( IReadOnlyList<ArchFloorCutout> cutouts ) {
return ArchFootprint.Union( cutouts.Select( cutout => cutout.Outline() ) );
}
// Skips zero-length edges from traced boundary
static Vector2 Reaching( IReadOnlyList<Vector2> loop, int from, int step, Vector2 fallback ) {
for ( var walked = 1; walked <= loop.Count; walked++ ) {
var at = ((from + step * walked) % loop.Count + loop.Count) % loop.Count;
var run = loop[(at + 1) % loop.Count] - loop[at];
if ( run.Length > 0.5f ) {
return run.Normal;
}
}
return fallback;
}
static void Board( ArchMesh canvas, Vector2 a, Vector2 b, Vector2 outward, float near, float far, float bottom, float top, ArchBrush brush, float foldFrom = 0f, float foldTo = 0f ) {
if ( top - bottom < 0.05f || far - near < 0.05f ) {
return;
}
var unit = (b - a).Normal;
Vector3 At( Vector2 point, float offset, float height, float fold ) {
var along = unit * (fold * offset);
var across = outward * offset;
return new Vector3( point.x + along.x + across.x, point.y + along.y + across.y, height );
}
var lower = new List<Vector3>
{
At( a, near, bottom, foldFrom ),
At( b, near, bottom, -foldTo ),
At( b, far, bottom, -foldTo ),
At( a, far, bottom, foldFrom )
};
var upper = new List<Vector3>();
foreach ( var point in lower ) {
upper.Add( point.WithZ( top ) );
}
canvas.Prism( lower, upper, brush );
}
}