Editor/Carve/ArchCarveShape.cs

Editor-side types describing a carved architectural shape. Defines enum for face sides, ArchCarveFace holding four corner points and derived accessors, ArchCarveCell representing a grid cell with bounds and end planes, and ArchCarveShape collecting faces and cells with helpers to query sides and compute outline loops.

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

namespace Sunless.Architecture;

// One question, one name: a stepped cut's Sills are its treads, its Jambs its risers.
public enum ArchCarveSide
{
	Face,
	Top,
	Bottom,
	Jamb,
	Sill,
	Head
}

// Wound as ArchMesh.Box winds the same side - only an angled clip makes a cap an n-gon.
public sealed class ArchCarveFace
{
	public List<Vector3> Points { get; init; }
	public ArchCarveSide Side { get; init; }

	public Vector3 A => Points[0];
	public Vector3 B => Points[1];
	public Vector3 C => Points[2];
	public Vector3 D => Points[3];

	public float Lowest => Points.Min( point => point.z );
}

public sealed class ArchCarveCell
{
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }
	public Vector2 Centre { get; init; }
	public IReadOnlyList<Vector2> Loop { get; init; }
	// Band at the cell's centre, faces per corner - on parallel planes the two agree exactly.
	public float From { get; init; }
	public float To { get; init; }
	// The surfaces that put those two ends where they are, which is NOT the same as the solid's own floor and
	// ceiling: a bite out of the middle leaves ends belonging to the cut, and a raked bite leaves raked ends.
	// Each holds the invariant that evaluating it at Centre gives back the scalar beside it.
	public ArchCarvePlane Foot { get; init; }
	public ArchCarvePlane Head { get; init; }

	public bool Rakes => Foot.Rakes || Head.Rakes;
}

public sealed class ArchCarveShape
{
	// A seam against a solid neighbour appears in NOBODY's list - that is what keeps the result manifold.
	public List<ArchCarveFace> Faces { get; } = new();
	public List<ArchCarveCell> Cells { get; } = new();

	internal ArchCarveGrid Grid { get; set; }

	internal float Frame { get; set; }

	public IEnumerable<ArchCarveFace> Reveals => Faces.Where( face => face.Side is ArchCarveSide.Jamb or ArchCarveSide.Sill or ArchCarveSide.Head );

	public IEnumerable<ArchCarveFace> Sides( ArchCarveSide side ) => Faces.Where( face => face.Side == side );

	// Walked off the arrangement, never off the pieces' bounding rects - a trapezoid has none worth squaring off.
	public List<List<Vector2>> Outline( float height )
	{
		if ( Grid is null )
		{
			return new List<List<Vector2>>();
		}

		return ArchCarveCells.Boundary( Grid.Pieces, height )
			.Select( loop => loop.Select( point => ArchCarveFrame.Turn( point, Frame ) ).ToList() )
			.ToList();
	}
}