Editor/Platform/ArchPlatformGen.cs

Generator for platform geometry and appearance. It resolves the carved shape of a platform (deck, ramp, or plinth), computes notches and mouths for stairs and cuts, builds guardrail runs, applies surface brushes (deck, tread, riser, soffit, skirt), and emits coping and guardrail parts into an ArchMesh.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// One carve: steps are what a flight took OUT, the coping walks the outline left.
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, 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() );
			}
		}
	}

	// Edge protection is not a second barrier engine: the deck says where its edge is and hands posts,
	// bays and rails to ArchBarrierShape/ArchBarrierGen, exactly as a roof edge does. The edge is the
	// authored outline less the same notches the body subtracts - each carved flight's box and every
	// cut open at the deck plane - and it parts at each flight's mouth, where the walk leaves the deck.
	public static List<ArchGuardRun> Guardrails( ArchPlatformPart platform, ArchBuilding building, ArchPlan plan, ArchKit kit )
	{
		var guards = new List<ArchGuardRun>();

		// A run is level by contract; a raked deck has no one height to stand a rail along.
		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 );
				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;
	}

	// What is missing from the deck at its own plane: each carved flight's whole box, and every cut
	// whose void spans the surface - the same authored operations the body subtracts, asked in plan.
	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;
	}

	// Where a carved flight arrives on the deck: the strip over its top going, grown a probe step so an
	// edge midpoint lands inside. The guard parts there - a rail across the walk-off is a fence.
	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;
	}

	// One resolution for generator, ghost and report; deleting a flight fills the platform back in.
	// Sunk below grade: a slab ending on the terrain shows daylight unless dead level.
	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();
	}

	// A ramp's deck is a raked plane over the same level base a plinth stands on, so a wedge and a pad are one
	// solid resolved two ways rather than two generators.
	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 );
	}

	// A stepped cut is one volume per tread, so it decides the pad's yaw.
	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;
	}

	// Sills are treads, jambs risers - why the side is classified in the service.
	static void Skin(
		ArchMesh canvas,
		ArchCarveShape shape,
		float underside,
		bool hung,
		Dictionary<ArchCarveSide, ArchBrush> brushes,
		ArchBrush skirt )
	{
		// One solid emits no interior seams, so notch cheeks share the tread's edge.
		using var welding = canvas.Welding();

		foreach ( var face in shape.Faces )
		{
			// The ground-side underside is never seen; a face nobody sees the bake still merges.
			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, ArchKit kit, ArchBrush brush )
	{
		// A run is level by contract - anything that rakes is a swept profile - so a raked deck has no one
		// height to walk a band round and wears none.
		if ( !platform.Coping || platform.CopingHeight < 0.05f || ArchRamp.Rakes( platform ) )
		{
			return;
		}

		var top = platform.TopHeight;

		// Bitten into the deck: sitting on it puts three faces on every edge.
		var band = ArchBandSection.Between(
			-MathF.Max( 1f, platform.CopingWidth ),
			MathF.Max( 0f, platform.CopingOversail ),
			top - ArchContact.Bite( kit ),
			top + platform.CopingHeight );

		foreach ( var loop in Coped( shape.Outline( top ), platform ) )
		{
			ArchRun.Around( loop ).Level( top ).Emit( canvas, band, brush );
		}
	}

	// A hole comes back wound the other way with the material still on its LEFT, so the outward side of a rim
	// points into the void exactly as the outside edge's does - the same band reads inward with no sign flipped
	// anywhere, which is what lets the inner edge be finished identically to the outer one.
	static IEnumerable<List<Vector2>> Coped( List<List<Vector2>> region, ArchPlatformPart platform )
	{
		return platform.CopingInside ? region : ArchFootprint.Outer( region );
	}
}