Editor/Layers/ArchLayerShape.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// A layer's shape — children can inherit the outline (e.g. a trim inside a circular cut follows the circle).
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 );
	}
}