Editor/Services/ArchRowPlacement.cs

Editor service that computes placement of a street row and turns it into buildings. It divides a dragged frontage into bays (ArchRowBay), decides storeys and parapets using seeded noise, aligns bays to grid or road vergé, filters out blocked bays, and creates building instances when standing.

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

namespace Sunless.Architecture;

// One unit of a street row: a base-grid rectangle, and everything the seed decided about it.
public sealed class ArchRowBay
{
	public int Index { get; init; }
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }
	public int Storeys { get; init; }
	public bool Parapet { get; init; }

	public Vector2 Size => Max - Min;
}

// One resolve, read by the ghost, the commit and the report - a preview that divided the frontage again would
// draw a different row than the one it stands.
public sealed class ArchRowShape
{
	public List<ArchRowBay> Bays { get; init; } = new();
	public bool AlongX { get; init; }
	public float Frontage { get; init; }
	public float Depth { get; init; }
	public int RoadId { get; init; }
	public int Blocked { get; init; }
	public int Seed { get; init; }

	public bool IsUsable => Bays.Count > 0;
}

public sealed class ArchRowResult
{
	public ArchSiteAssembly Group { get; init; }
	public List<ArchBuilding> Buildings { get; init; } = new();
}

// A row is N party-walled units authored in one gesture: the drag names a frontage and a depth, ArchDivide cuts
// the frontage into bays, and each bay stands as a normal building. A curving street is absorbed as a STAIRCASE of
// rectangles rather than splayed plots - a splay's corners cannot sit on the base grid unless the road happens to
// run on it, and grid snapping outranks the kink.
public static class ArchRowPlacement
{
	const float Sway = 0.3f;
	const float LeastShare = 0.75f;
	const int LeastStoreys = 1;
	const int MostStoreys = 3;
	const int WidthSalt = 5;
	const int StoreySalt = 17;
	const int ParapetSalt = 37;

	// How near the drag has to run to a road before the row takes that road's verge as its frontage.
	public static float Verge( ArchRoadPart road ) => road.Reach() * 1.5f;

	// The drag START, not the whole rectangle: seeding off both corners reshuffles every bay the author has
	// already reviewed as the drag grows.
	public static int Seeded( Vector2 start )
	{
		return unchecked( (int)start.x * 73856093 ^ (int)start.y * 19349663 );
	}

	public static ArchRowShape Resolve(
		ArchPlan plan,
		ArchKit kit,
		ArchArchetype archetype,
		int level,
		Vector2 from,
		Vector2 to,
		ArchRoadPart road,
		ArchCurve centreline,
		bool parapets )
	{
		if ( plan is null || kit is null || archetype is null )
		{
			return new ArchRowShape();
		}

		var grid = new ArchGridService();
		var unit = MathF.Max( grid.BaseSize, archetype.MinimumPlot.x );
		var least = MathF.Max( grid.BaseSize, grid.Base( unit * LeastShare ) );
		var seed = Seeded( grid.Base( from ) );

		var drafted = road is null
			? Straight( grid, from, to, unit, least, seed, parapets )
			: Verged( grid, road, centreline ?? road.Curve(), from, to, unit, least, seed, parapets );

		return Vacant( plan, kit, level, drafted );
	}

	public static ArchRowResult Stand(
		ArchPlan plan,
		ArchKit kit,
		ArchRowShape row,
		ArchArchetype archetype,
		ArchSectionRules rules,
		int level,
		float baseHeight,
		RoofStyle? style,
		RidgeRun ridge )
	{
		if ( plan is null || kit is null || rules is null || row is null || !row.IsUsable )
		{
			return null;
		}

		var standing = ArchArchetypeRules.Standing( rules, kit );
		var storey = standing + kit.FloorThickness;
		var floor = rules.Floor ?? true;
		var gutters = rules.Gutters ?? true;
		var stood = new List<ArchBuilding>();

		foreach ( var bay in row.Bays )
		{
			var building = new ArchBuilding
			{
				Id = plan.AllocateId(),
				Name = $"{archetype?.Title ?? "Unit"}{plan.Buildings.Count + 1}",
				Archetype = archetype?.Name ?? "",
				GuttersEnabled = gutters && !bay.Parapet
			};

			var shell = ArchBuild.Shell(
				plan, building, kit, level, baseHeight, bay.Min, bay.Max,
				floor, style, building.GuttersEnabled, ridge );

			if ( shell is null )
			{
				continue;
			}

			ArchArchetypeRules.Apply( shell, rules, kit );
			Capped( shell.Roof, bay );

			plan.Units.Add( building );
			Stacked( plan, kit, building, bay, rules, level, baseHeight, storey, standing, floor, style, ridge );

			stood.Add( building );
		}

		if ( stood.Count == 0 )
		{
			return null;
		}

		return new ArchRowResult
		{
			Group = ArchLayerGroups.Create( plan, ArchLayerGroups.Named( plan ), ArchAssemblyKind.Group, stood.Select( unit => unit.Id ) ),
			Buildings = stood
		};
	}

	static void Stacked(
		ArchPlan plan,
		ArchKit kit,
		ArchBuilding building,
		ArchRowBay bay,
		ArchSectionRules rules,
		int level,
		float baseHeight,
		float storey,
		float standing,
		bool floor,
		RoofStyle? style,
		RidgeRun ridge )
	{
		for ( var above = 1; above < bay.Storeys; above++ )
		{
			var section = ArchBuild.Storey(
				plan, building, kit, level + above, baseHeight + storey * above, bay.Min, bay.Max,
				floor, style, building.GuttersEnabled, ridge );

			if ( section is null )
			{
				return;
			}

			ArchArchetypeRules.Apply( section, rules, kit );

			// A lifted deck keeps the storey Storey measured, so the row restates the one it authored.
			section.Room.WallHeight = standing;

			if ( section.Roof is { } lifted )
			{
				lifted.BaseHeight = section.Room.BaseHeight + standing;
			}

			Capped( section.Roof, bay );
		}
	}

	// A parapet stands on the wall line, so the eave trims that would hang off it go with it.
	static void Capped( ArchRoofPart roof, ArchRowBay bay )
	{
		if ( roof is null || !bay.Parapet )
		{
			return;
		}

		roof.Parapet = true;
		roof.Overhang = 0f;
		roof.Fascia = false;
		roof.Soffit = false;
		roof.Gutters = false;
	}

	static ArchRowShape Straight( ArchGridService grid, Vector2 from, Vector2 to, float unit, float least, int seed, bool parapets )
	{
		var lo = grid.Base( Vector2.Min( from, to ) );
		var hi = grid.Base( Vector2.Max( from, to ) );
		var span = hi - lo;
		var alongX = span.x >= span.y;
		var frontage = alongX ? span.x : span.y;
		var depth = alongX ? span.y : span.x;

		if ( frontage < least || depth < grid.BaseSize )
		{
			return new ArchRowShape { AlongX = alongX, Frontage = frontage, Depth = depth, Seed = seed };
		}

		var division = ArchDivide.AtLeast( frontage, unit );
		var stations = Stations( division, seed, least );
		var start = alongX ? lo.x : lo.y;
		var edges = Rising( stations.Select( station => start + grid.Base( station ) ).ToList() );
		var near = alongX ? lo.y : lo.x;
		var bays = new List<ArchRowBay>();

		for ( var index = 0; index < edges.Count - 1; index++ )
		{
			if ( edges[index + 1] - edges[index] < grid.BaseSize )
			{
				continue;
			}

			bays.Add( Bay( index, seed, alongX, edges[index], edges[index + 1], near, near + depth, parapets ) );
		}

		return new ArchRowShape { Bays = bays, AlongX = alongX, Frontage = frontage, Depth = depth, Seed = seed };
	}

	static ArchRowShape Verged(
		ArchGridService grid,
		ArchRoadPart road,
		ArchCurve curve,
		Vector2 from,
		Vector2 to,
		float unit,
		float least,
		int seed,
		bool parapets )
	{
		if ( curve is null || !curve.IsUsable
			|| !curve.Nearest( from, out var head, out var headGap )
			|| !curve.Nearest( to, out var tail, out var tailGap ) )
		{
			return Straight( grid, from, to, unit, least, seed, parapets );
		}

		var furthest = headGap >= tailGap ? head : tail;
		var pull = (headGap >= tailGap ? from : to) - furthest.Flat;
		var right = Vector2.Dot( pull, Across( furthest ) ) >= 0f;
		var reach = road.Reach( right );
		var near = MathF.Min( head.Distance, tail.Distance );
		var far = MathF.Max( head.Distance, tail.Distance );
		var run = far - near;
		var depth = MathF.Max( grid.BaseSize, grid.Base( MathF.Max( headGap, tailGap ) - reach ) );

		if ( run < least || !curve.Sample( near, out var opening ) || !curve.Sample( far, out var closing ) )
		{
			return new ArchRowShape { Frontage = run, Depth = depth, RoadId = road.Id, Seed = seed };
		}

		var chord = closing.Flat - opening.Flat;
		var alongX = MathF.Abs( chord.x ) >= MathF.Abs( chord.y );
		var forward = (alongX ? chord.x : chord.y) >= 0f;
		var outward = (alongX ? pull.y : pull.x) < 0f ? -1f : 1f;
		var division = ArchDivide.AtLeast( run, unit );
		var stations = Stations( division, seed, least );

		float Stationed( float offset ) => forward ? near + offset : far - offset;

		var edges = Rising( stations.Select( station => grid.Base( Edge( curve, Stationed( station ), alongX ) ) ).ToList() );
		var bays = new List<ArchRowBay>();

		for ( var index = 0; index < edges.Count - 1; index++ )
		{
			if ( edges[index + 1] - edges[index] < grid.BaseSize )
			{
				continue;
			}

			if ( !curve.Sample( Stationed( (stations[index] + stations[index + 1]) * 0.5f ), out var frame ) )
			{
				continue;
			}

			var kerb = frame.Flat + Across( frame ) * (right ? reach : -reach);
			var verge = grid.Base( alongX ? kerb.y : kerb.x );

			bays.Add( Bay( index, seed, alongX, edges[index], edges[index + 1], verge, verge + outward * depth, parapets ) );
		}

		return new ArchRowShape { Bays = bays, AlongX = alongX, Frontage = run, Depth = depth, RoadId = road.Id, Seed = seed };
	}

	static ArchRowBay Bay( int index, int seed, bool alongX, float opening, float closing, float verge, float back, bool parapets )
	{
		var near = MathF.Min( verge, back );
		var far = MathF.Max( verge, back );

		return new ArchRowBay
		{
			Index = index,
			Min = alongX ? new Vector2( opening, near ) : new Vector2( near, opening ),
			Max = alongX ? new Vector2( closing, far ) : new Vector2( far, closing ),
			Storeys = Storeyed( seed, index ),
			Parapet = parapets && ArchBarrierShape.Noise( seed, index, ParapetSalt ) < 0.5f
		};
	}

	static int Storeyed( int seed, int index )
	{
		var reach = MostStoreys - LeastStoreys + 1;

		return Math.Clamp( LeastStoreys + (int)MathF.Floor( ArchBarrierShape.Noise( seed, index, StoreySalt ) * reach ), LeastStoreys, MostStoreys );
	}

	// The party wall between two units is ONE node used by both, so a jittered width can never open a gap.
	static List<float> Stations( ArchDivision division, int seed, float least )
	{
		var stations = new List<float>();

		for ( var index = 0; index <= division.Count; index++ )
		{
			var station = division.At( index );

			if ( index > 0 && index < division.Count )
			{
				station += ArchBarrierShape.Signed( seed, index, WidthSalt ) * division.Step * Sway;
			}

			stations.Add( station );
		}

		for ( var index = 1; index < stations.Count - 1; index++ )
		{
			var earliest = stations[index - 1] + least;
			var latest = MathF.Max( earliest, division.Span - least * (stations.Count - 1 - index) );

			stations[index] = Math.Clamp( stations[index], earliest, latest );
		}

		return stations;
	}

	// A street that doubles back would hand the same edge out twice; the bay between them collapses and drops.
	static List<float> Rising( List<float> edges )
	{
		for ( var index = 1; index < edges.Count; index++ )
		{
			edges[index] = MathF.Max( edges[index], edges[index - 1] );
		}

		return edges;
	}

	static float Edge( ArchCurve curve, float distance, bool alongX )
	{
		if ( !curve.Sample( distance, out var frame ) )
		{
			return 0f;
		}

		return alongX ? frame.Flat.x : frame.Flat.y;
	}

	static Vector2 Across( ArchFrame frame )
	{
		var across = new Vector2( frame.Across.x, frame.Across.y );

		return across.Length < 0.01f ? new Vector2( 0f, 1f ) : across.Normal;
	}

	// A trimmed bay loses the party wall it shares and steps out of the staircase, so occupied ground drops the
	// whole bay rather than shrinking it.
	static ArchRowShape Vacant( ArchPlan plan, ArchKit kit, int level, ArchRowShape drafted )
	{
		if ( !drafted.IsUsable )
		{
			return drafted;
		}

		var boundary = new ArchBoundaryPlacementService( plan, kit );
		var standing = new List<ArchRowBay>();
		var blocked = 0;

		foreach ( var bay in drafted.Bays )
		{
			var placement = boundary.Outside( level, bay.Min, bay.Max );

			if ( !placement.IsUsable || placement.Min != bay.Min || placement.Max != bay.Max )
			{
				blocked++;
				continue;
			}

			standing.Add( bay );
		}

		return new ArchRowShape
		{
			Bays = standing,
			AlongX = drafted.AlongX,
			Frontage = drafted.Frontage,
			Depth = drafted.Depth,
			RoadId = drafted.RoadId,
			Blocked = blocked,
			Seed = drafted.Seed
		};
	}
}