Editor/Platform/ArchPlatformGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchPlatformGen {
public static void Build(
ArchMesh canvas,
ArchPlatformPart platform,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
ArchStyle style ) {
var shape = Resolve( platform, building, plan, kit );
if ( shape.Faces.Count == 0 ) {
return;
}
var chain = new[] { platform.Palette, building.Palette };
var skirt = style.Brush( ArchSurface.Foundation, chain );
var brushes = new Dictionary<ArchCarveSide, ArchBrush> {
[ArchCarveSide.Top] = style.Brush( ArchSurface.Deck, chain ),
[ArchCarveSide.Sill] = style.Brush( ArchSurface.StairTread, chain ),
[ArchCarveSide.Jamb] = style.Brush( ArchSurface.StairRiser, chain )
};
if ( platform.Hung ) {
brushes[ArchCarveSide.Bottom] = style.Brush( ArchSurface.Soffit, chain );
}
Skin( canvas, shape, Base( platform, kit ), platform.Hung, brushes, skirt );
using ( canvas.Part( ArchPieces.Coping ) ) {
Coping( canvas, shape, platform, building, kit, style.Brush( ArchSurface.WallCap, chain ) );
}
using ( canvas.Part( ArchPieces.Guardrail ) ) {
foreach ( var guard in Guardrails( platform, building, plan, kit ) ) {
ArchBarrierGen.Build( canvas, guard.Shape, guard.Spec, kit, ArchBarrierBrushes.Of( style, chain ), guard.Doubled() );
}
}
}
public static List<ArchGuardRun> Guardrails( ArchPlatformPart platform, ArchBuilding building, ArchPlan plan, ArchKit kit ) {
var guards = new List<ArchGuardRun>();
if ( !platform.Guardrail || ArchRamp.Rakes( platform ) ) {
return guards;
}
var seat = platform.TopHeight;
var mouths = Mouths( platform, building, kit );
var notches = Notches( platform, building, plan, kit, seat );
bool Abuts( Vector2 point, Vector2 outward ) {
var probe = point + outward * ArchProbe.Step;
return mouths.Any( mouth => ArchFootprint.Contains( mouth, probe ) );
}
var region = notches.Count == 0
? new List<List<Vector2>> { platform.Outline() }
: ArchFootprint.Subtract( new List<List<Vector2>> { platform.Outline() }, notches );
foreach ( var loop in ArchFootprint.Outer( region ) ) {
foreach ( var run in ArchBrokenRun.Of( ArchFootprint.Wind( loop ), Abuts ).Level( seat ).Resolve() ) {
var spec = ArchGuards.Spec( MathF.Max( 12f, platform.GuardHeight ), kit, run, platform.GuardStyle );
var curve = ArchCurve.Polyline( run.Raised(), run.Closed );
guards.Add( new ArchGuardRun {
Shape = ArchBarrierShape.Resolve( curve, spec, 0f, 0f, kit ),
Spec = spec,
Closed = run.Closed,
Length = curve.Length
} );
}
}
return guards;
}
static List<List<Vector2>> Notches( ArchPlatformPart platform, ArchBuilding building, ArchPlan plan, ArchKit kit, float seat ) {
var notches = new List<List<Vector2>>();
foreach ( var stair in ArchLayerGate.Enabled( platform.Stairs ) ) {
if ( stair.Core is not { } core ) {
continue;
}
foreach ( var lane in ArchStairShape.Standing( stair, core ) ) {
notches.Add( lane.Loop( core ) );
}
}
foreach ( var cut in ArchCut.Over( plan, platform.Level, platform.Outline(), building?.Id ?? 0, ArchCutAffects.Platforms, platform.Id ) ) {
foreach ( var volume in ArchCut.Resolve( cut, kit ).Where( volume => ArchCut.Reaches( volume, seat - 1f, seat ) ) ) {
notches.Add( volume.Footprint.ToList() );
}
}
return notches;
}
static List<List<Vector2>> Mouths( ArchPlatformPart platform, ArchBuilding building, ArchKit kit ) {
var mouths = new List<List<Vector2>>();
foreach ( var stair in ArchLayerGate.Enabled( platform.Stairs ) ) {
foreach ( var mouth in ArchHostCarvings.Mouths( platform, stair, building, kit ) ) {
mouths.Add( mouth );
}
}
return mouths;
}
// Sunk below grade so the slab edge doesn't show daylight
public static float Base( ArchPlatformPart platform, ArchKit kit ) {
return platform.Hung ? platform.GradeHeight : platform.GradeHeight - ArchGround.Embedment( kit );
}
public static ArchCarveShape Resolve( ArchPlatformPart platform, ArchBuilding building, ArchPlan plan, ArchKit kit ) {
var outline = platform.Outline();
var baseHeight = Base( platform, kit );
var cuts = ArchCut.Over( plan, platform.Level, outline, building?.Id ?? 0, ArchCutAffects.Platforms, platform.Id )
.Where( cut => ArchCut.Resolve( cut, kit ).Any( volume => ArchCut.Reaches( volume, baseHeight, platform.TopHeight ) ) )
.ToList();
var carve = Body( platform, outline, baseHeight ).In( Yaw( platform, cuts ) );
foreach ( var stair in ArchLayerGate.Enabled( platform.Stairs ) ) {
foreach ( var volume in ArchHostCarvings.Volumes( platform, stair, building, kit ) ) {
carve.Less( volume );
}
}
foreach ( var cut in cuts ) {
foreach ( var volume in ArchCut.Resolve( cut, kit ) ) {
carve.Less( volume );
}
}
return carve.Resolve();
}
static ArchCarve Body( ArchPlatformPart platform, IReadOnlyList<Vector2> outline, float baseHeight ) {
return ArchRamp.Rakes( platform )
? ArchCarve.Wedge( outline, baseHeight, ArchRamp.Deck( platform ) )
: ArchCarve.Prism( outline, baseHeight, platform.TopHeight );
}
static float Yaw( ArchPlatformPart platform, IReadOnlyList<ArchCutPart> cuts ) {
var stepped = cuts.FirstOrDefault( cut => cut.Profile == CutProfile.Steps && cut.HasContent );
if ( stepped is not null ) {
return stepped.Segments[0].Yaw;
}
var flight = ArchLayerGate.Enabled( platform.Stairs ).FirstOrDefault( stair => MathF.Abs( stair.Yaw % 90f ) > 0.01f );
return flight?.Yaw ?? 0f;
}
static void Skin(
ArchMesh canvas,
ArchCarveShape shape,
float underside,
bool hung,
Dictionary<ArchCarveSide, ArchBrush> brushes,
ArchBrush skirt ) {
using var welding = canvas.Welding();
foreach ( var face in shape.Faces ) {
if ( !hung && face.Side == ArchCarveSide.Bottom && face.Lowest - underside < 0.05f ) {
continue;
}
canvas.Polygon( face.Points, brushes.TryGetValue( face.Side, out var brush ) ? brush : skirt );
}
}
static void Coping( ArchMesh canvas, ArchCarveShape shape, ArchPlatformPart platform, ArchBuilding building, ArchKit kit, ArchBrush brush ) {
if ( !platform.Coping || platform.CopingHeight < 0.05f || ArchRamp.Rakes( platform ) ) {
return;
}
var top = platform.TopHeight;
// Bitten into the deck so it laps correctly
var band = ArchBandSection.Between(
-MathF.Max( 1f, platform.CopingWidth ),
MathF.Max( 0f, platform.CopingOversail ),
top - ArchLap.Bite( kit ),
top + platform.CopingHeight );
var mouths = ApproachMouths( platform, building, kit );
foreach ( var loop in Coped( shape.Outline( top ), platform ) ) {
foreach ( var run in Parted( loop, mouths, kit ) ) {
run.Level( top ).Emit( canvas, band, brush );
}
}
}
// Coping breaks over approaches that land on this deck
static List<IReadOnlyList<Vector2>> ApproachMouths( ArchPlatformPart platform, ArchBuilding building, ArchKit kit ) {
var mouths = new List<IReadOnlyList<Vector2>>();
var bite = ArchLap.Bite( kit );
// Past the course both ways to avoid coplanar pairs at the podium edge
var inward = MathF.Max( 1f, platform.CopingWidth ) + bite;
var garden = MathF.Max( 0f, platform.CopingOversail ) + bite;
foreach ( var room in building?.Rooms ?? Enumerable.Empty<ArchRoom>() ) {
foreach ( var approach in room.Approaches ) {
if ( approach.PlatformId != platform.Id || !ArchLayerGate.On( approach ) ) {
continue;
}
mouths.Add( ArchApproachGen.Mouth( approach, kit, garden, inward, 0f ).Outline() );
}
}
return mouths;
}
// Open runs parted at mouths, not re-routed around them
static IEnumerable<ArchRun> Parted( List<Vector2> loop, List<IReadOnlyList<Vector2>> mouths, ArchKit kit ) {
if ( mouths.Count == 0 ) {
yield return ArchRun.Around( loop );
yield break;
}
foreach ( var stretch in ArchRegion.Open( loop, mouths ) ) {
if ( stretch.Count < 2 ) {
continue;
}
if ( (stretch[0] - stretch[^1]).Length < 0.05f ) {
yield return ArchRun.Around( stretch.Take( stretch.Count - 1 ).ToList() );
continue;
}
yield return ArchRun.Along( Retracted( stretch, ArchLap.Bite( kit ) ) );
}
}
// Open ends retract a bite inside the solid to avoid z-fighting
static List<Vector2> Retracted( IReadOnlyList<Vector2> stretch, float bite ) {
var ends = stretch.ToList();
ends[0] = Inside( ends[0], ends[1], bite );
ends[^1] = Inside( ends[^1], ends[^2], bite );
return ends;
}
static Vector2 Inside( Vector2 end, Vector2 towards, float bite ) {
var span = towards - end;
var length = span.Length;
return length < bite * 3f ? end : end + span / length * bite;
}
static IEnumerable<List<Vector2>> Coped( List<List<Vector2>> region, ArchPlatformPart platform ) {
return platform.CopingInside ? region : ArchFootprint.Outer( region );
}
}