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.
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 );
}
}