Editor-side generator for a building storey. It builds floors, ceilings, trims, cornices and boards into an ArchMesh based on plan, kit, style and stored cuts, computing holes, wells and recessed volumes and emitting geometry (slabs, bands, planks, cornices, displaced damage panels).
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 );
// A stored hole stands only while the layer that opened it does, so disabling a flight fills its
// stairwell back in without the plan losing the well it would restore.
var cutouts = ArchCut.Stored( building, group.Level ).ToList();
// Every wall the building has: a cut edge is covered by whichever storey's wall stands over it.
var walls = building.Rooms.SelectMany( room => room.Walls ).ToList();
// The ceiling and the floor above share one plane, so they take the same holes.
var above = ArchCut.Stored( building, group.Level + 1 ).ToList();
if ( lead.HasFloor )
{
var top = lead.BaseHeight;
// Derived every rebuild, never stored: deleting the cut fills the slab back in.
cutouts.AddRange( ArchCut.Holes( plan, group.Level, kit, top - thickness, top, building.Id, ArchCutAffects.Floors, lead.Id ) );
Solid( canvas, group.Region, 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 );
}
// A BODY, not a plate: it hangs the authored depth under the plate, and a cut that stops inside that
// depth leaves a coffer rather than a hole. Only the cuts that go all the way through are wells, and
// only a well has an edge to dress.
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 - the roof deck, or the floor of the storey above. Level
// with it, the ceiling's top and that underside are one plane, which is four faces of z-fight along
// the whole room. The soffit is untouched, and that is what the cornice is measured from.
var head = plate + ArchContact.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 + ArchContact.Bite( kit )
: MathF.Min( head, soffit + MathF.Max( 0.5f, cut.DamageDepth ) );
recesses.Add( ArchCarveVolume.Over( volume.Footprint, soffit - ArchContact.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 );
}
// A passage's footprint runs on to the host wall's INNER FACE so its floor meets that room's own slab at the
// threshold, where the skirting covers the seam. The ceiling body has no such cover, and it is lapped up into
// whatever stands over the plate, so taken the whole way its cap lands on that wall's plaster ABOVE the mouth
// head - one plane, two faces, a flicker over every doorway. Held a bite short at each MOUTH, the way the
// cornice and the deck over it already are. Only the mouths give: the sides carry the passage's own walls.
static List<List<Vector2>> Body( ArchFloorGroup ceiling, ArchKit kit )
{
if ( !ceiling.Lead.Spans )
{
return ceiling.Region;
}
ArchFootprint.Bounds( Footprint( ceiling.Lead ), out var min, out var max );
var bite = ArchContact.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 );
}
// A spanning room is a passage, and a passage has walls on its sides and nothing across its mouths, so its
// cornice runs the sides only - the same answer the deck over it uses, and the run axis comes off the link
// rather than being read a second way here.
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 );
}
// The whole-storey region carries a lane unbroken through doorways and party walls.
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
};
// The footprint is the wall's CENTRELINE, so boards run through a doorway with no gap and no step.
ArchPlanks.Fill( canvas, group.Region, cutouts, spec, brush );
}
// Its own clockwise loops are holes; the outer loops alone fill a ring solid.
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 );
}
}
// What a body loses to the cuts standing in it, split by whether they go all the way through: one
// resolution both the room's ceiling and the roof's own read, so a coffer is a coffer under either.
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 the BOUNDARY OF THE UNION of the holes: consecutive pieces share edges, so banding per hole doubles them.
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 = ArchContact.Bite( kit );
var proud = ArchContact.Proud( kit );
var depth = MathF.Max( 0f, kit.WellTrimDepth );
if ( cutouts is null || depth < 0.05f )
{
return;
}
foreach ( var loop in Boundary( cutouts ) )
{
// CCW loop: void left, slab right - a centroid read breaks on an L.
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;
// The board is folded onto the bisector at each corner it actually reaches, so the run round
// a hole is one continuous ring - butted ends left a wedge open at every turn.
var opening = ArchBandGen.Fold( Reaching( loop, index, -1, unit ), unit, slab );
var closing = ArchBandGen.Fold( unit, Reaching( loop, index, 1, unit ), slab );
// CLIPPED, not sampled at the middle: an edge that starts over floor and runs out past it
// is dressed for the part that cuts and bare for the rest. Judging it by its midpoint ran
// the whole board out into the air, which is the trim standing off the end of the building.
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;
// A cut edge buried in a wall has no exposed slab edge to trim - the wall already
// covers it - and the boards come out as a band up the plaster instead of round a hole.
if ( ArchProbe.Against( walls, kit, middle, outward ) )
{
continue;
}
// No shared face: the nosing overhangs, the facing stops under it.
// Only the nosing hands over: it lies at the floor line with nothing under it.
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 );
}
}
}
}
// Whether the hole actually CUTS here - is there slab on this point's slab side at all? A hole
// reaching past the floor it opens has edges out in the air, and dressing those rings a well on four
// sides when only the two inside the building were ever cut. No region means the caller is not scoped
// to one, and everything is taken as cut, which is how it behaved before.
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 ) );
}
// The stretches of one edge that are worth dressing, as parameters along it: split where the edge
// crosses the floor's own boundary, keep the runs whose slab side is standing on slab. The offset is
// a translation, so a parameter on the probe line is the same parameter on the edge.
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 the zero-length edges a traced boundary leaves behind, so a corner folds against the direction
// the board next door actually runs on rather than against nothing.
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 );
}
}