Editor/Platform/ArchPlatform.cs

Editor utility for creating and managing architecture platforms. Computes terrain grade under a footprint, validates size, handles joining existing platforms, builds ArchPlatformPart instances, and files them into an ArchPlan.

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

namespace Sunless.Architecture;


public static class ArchPlatform
{
	public const float MinSize = 24f;

	// LOWEST point under the footprint: average or centre leaves a downhill corner hanging.
	public static float Grade( Scene scene, Vector2 min, Vector2 max, float datum )
	{
		var corners = new[]
		{
			min, new Vector2( max.x, min.y ), max, new Vector2( min.x, max.y ), (min + max) * 0.5f
		};

		var lowest = float.MaxValue;

		foreach ( var corner in corners )
		{
			if ( ArchTerrain.Sample( scene, corner, out var height ) )
			{
				lowest = MathF.Min( lowest, height );
			}
		}

		return lowest == float.MaxValue ? datum : lowest;
	}

	public static ArchPlatformPart Add(
		ArchPlan plan,
		ArchBuilding building,
		int level,
		float grade,
		Vector2 min,
		Vector2 max,
		ArchPlatformPart draft )
	{
		return Add( plan, building, level, grade, ArchFootprint.Rect( min, max ), draft );
	}

	public static ArchPlatformPart Add(
		ArchPlan plan,
		ArchBuilding building,
		int level,
		float grade,
		IReadOnlyList<Vector2> outline,
		ArchPlatformPart draft )
	{
		return Add( plan, building, level, grade, grade + MathF.Max( 4f, draft.Rise ), outline, draft );
	}

	// The band the DRAG resolved, rather than the draft's own rise. The ghost promises the height the author
	// pulled out on screen, and a placement quietly reading the seed's instead stood every plinth at the
	// default however far the preview was dragged.
	public static ArchPlatformPart Add(
		ArchPlan plan,
		ArchBuilding building,
		int level,
		float grade,
		float top,
		IReadOnlyList<Vector2> outline,
		ArchPlatformPart draft )
	{
		if ( building is null || outline is not { Count: >= 3 } )
		{
			return null;
		}

		ArchFootprint.Bounds( outline, out var min, out var max );

		if ( max.x - min.x < MinSize || max.y - min.y < MinSize )
		{
			return null;
		}

		var head = MathF.Max( grade + 4f, top );

		// A drag touching one already there extends it: one coping, no seam.
		if ( Joining( plan, building, level, head, outline, draft ) is { } joining )
		{
			joining.Reshape( ArchFootprint.Union( new[] { (IReadOnlyList<Vector2>)joining.Outline(), outline } ).Single() );

			return joining;
		}

		var kinds = ArchKinds.Load();

		var platform = new ArchPlatformPart
		{
			Id = plan.AllocateId(),
			Name = $"{(draft.Ramp ? "Ramp" : "Platform")}{plan.CountFiled( ArchKind.Platform, building, kinds ) + 1}",
			Level = level,
			GradeHeight = grade,
			TopHeight = head,
			Ramp = draft.Ramp,
			RampFall = draft.RampFall,
			RampYaw = draft.RampYaw,
			Guardrail = draft.Guardrail,
			GuardHeight = draft.GuardHeight,
			Coping = draft.Coping,
			CopingHeight = draft.CopingHeight,
			CopingWidth = draft.CopingWidth,
			CopingOversail = draft.CopingOversail,
			CopingInside = draft.CopingInside
		};

		platform.Reshape( outline.ToList() );

		plan.File( ArchKind.Platform, building, platform, kinds );

		return platform;
	}

	// A flat deck at the same head, in the same group. A ramp is never one of them either way round: two decks
	// climbing different ways merged under one outline is neither of them.
	static ArchPlatformPart Joining(
		ArchPlan plan,
		ArchBuilding building,
		int level,
		float top,
		IReadOnlyList<Vector2> outline,
		ArchPlatformPart draft )
	{
		if ( draft.Ramp )
		{
			return null;
		}

		var kinds = ArchKinds.Load();
		var members = ArchLayerGroups.Holding( plan, building.Id ) is { } group
			? ArchLayerGroups.Flatten( plan, group )
			: null;
		var owners = members is null
			? new[] { building }
			: plan.Buildings.Where( candidate => members.Contains( candidate.Id ) );

		return owners
			.SelectMany( owner => plan.Filed<ArchPlatformPart>( owner, kinds ) )
			.FirstOrDefault( other => !other.Ramp
				&& other.Level == level
				&& MathF.Abs( other.TopHeight - top ) < 1f
				&& ArchFootprint.Union( new[] { (IReadOnlyList<Vector2>)other.Outline(), outline } ).Count == 1 );
	}
}