Editor/Layers/ArchLayerShape.cs

Utility class that resolves geometric shape and height information for architectural plan items. It finds the authored part by id within an ArchPlan, asks the matching ArchKind for loop outlines and heights, and filters out loops with fewer than three corners.

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

namespace Sunless.Architecture;

// What a layer's shape IS, asked in one place. The stack builds on itself: a child nested under a
// layer can take that layer's outline as its own, so a trim dropped inside a circular cut runs round
// the circle instead of being drawn round it by hand. Every kind answers from the same resolve its
// generator reads, never from a second derivation - a followed shape that disagreed with the shape it
// followed would be worse than no inheritance at all.
public static class ArchLayerShape
{
	public static IReadOnlyList<List<Vector2>> Loops( ArchPlan plan, int itemId )
	{
		var kinds = ArchKinds.Load();

		return Loops( Find( plan, itemId ), kinds );
	}

	public static float HeightOf( ArchPlan plan, int itemId, float fallback )
	{
		var kinds = ArchKinds.Load();

		return Height( Find( plan, itemId ), fallback, kinds );
	}

	public static IReadOnlyList<List<Vector2>> Loops( object payload )
	{
		return Loops( payload, ArchKinds.Load() );
	}

	// Fewer than three corners is not a shape anything can follow, and the manifests disagree about saying so -
	// beams, roofs, rooms and porches refuse one themselves, cuts, platforms and trims hand it back - so the
	// refusal is stated once here rather than trusted to each of them.
	public static IReadOnlyList<List<Vector2>> Loops( object payload, ArchKinds kinds )
	{
		var loops = kinds.Claiming( payload )?.Loops( payload ) ?? Array.Empty<List<Vector2>>();

		return loops.Where( loop => loop.Count >= 3 ).ToList();
	}

	// The height a follower sits at, so it lands on its host rather than on the storey's work plane.
	public static float Height( object payload, float fallback )
	{
		return Height( payload, fallback, ArchKinds.Load() );
	}

	public static float Height( object payload, float fallback, ArchKinds kinds )
	{
		return kinds.Claiming( payload )?.Height( payload, fallback ) ?? fallback;
	}

	// One lookup for "which authored part is this id", so a follower and its host never resolve the same id two
	// different ways. Named outright rather than walked through the store: these are the plan document's own types
	// and they stay in core, and the store's walk reaches a part only through the host that files it - which a
	// bool standing on a bare building does not yet have.
	public static object Find( ArchPlan plan, int itemId )
	{
		if ( itemId == 0 || plan is null )
		{
			return null;
		}

		if ( plan.FindCut( itemId ) is { } cut )
		{
			return cut;
		}

		if ( plan.AllRooms().FirstOrDefault( room => room.Id == itemId ) is { } room )
		{
			return room;
		}

		if ( plan.Buildings.SelectMany( building => building.Platforms ).FirstOrDefault( platform => platform.Id == itemId ) is { } platform )
		{
			return platform;
		}

		if ( plan.FindBeam( itemId ) is { } beam )
		{
			return beam;
		}

		if ( plan.Buildings.SelectMany( building => building.Roofs ).FirstOrDefault( roof => roof.Id == itemId ) is { } roof )
		{
			return roof;
		}

		if ( plan.AllRooms().SelectMany( entry => entry.Porches ).FirstOrDefault( porch => porch.Id == itemId ) is { } porch )
		{
			return porch;
		}

		return plan.AllRooms()
			.SelectMany( entry => entry.Trims.Concat( entry.Porches.SelectMany( standing => standing.Trims ) ) )
			.FirstOrDefault( trim => trim.Id == itemId );
	}
}