Editor/Output/ArchSolid.cs

A readonly struct representing a single convex collision or render solid used by the architecture system. It supports three concrete forms (Box, Cylinder, Hull), factory constructors for each form, a usability check to skip tiny solids, and a Fold method to combine the solid into a rolling hash.

Native Interop
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

public enum ArchSolidForm
{
	Box,
	Cylinder,
	Hull
}

// One convex piece of a part, in the part node's own space. A generator emits solids because it KNOWS them - a
// column is a box, a voussoir is a prism, a board is a beam - so nothing here has to be recovered from triangles.
// Every form the engine can take as a single shape and no more: a box, a lathed cylinder, or a point cloud whose
// convex hull is the shape. A rotated anything is Hull, because a hull carries its own orientation in its points
// and a box shape does not.
public readonly struct ArchSolid
{
	public ArchSolidForm Form { get; init; }
	public Vector3 Centre { get; init; }
	public Vector3 Size { get; init; }
	public float Radius { get; init; }
	public float Height { get; init; }
	public int Slices { get; init; }
	public IReadOnlyList<Vector3> Points { get; init; }

	public static ArchSolid Box( Vector3 mins, Vector3 maxs )
	{
		var lo = Vector3.Min( mins, maxs );
		var hi = Vector3.Max( mins, maxs );

		return new ArchSolid
		{
			Form = ArchSolidForm.Box,
			Centre = (lo + hi) * 0.5f,
			Size = hi - lo
		};
	}

	public static ArchSolid Cylinder( Vector3 centre, float radius, float height, int slices )
	{
		return new ArchSolid
		{
			Form = ArchSolidForm.Cylinder,
			Centre = centre,
			Radius = radius,
			Height = height,
			Slices = slices
		};
	}

	public static ArchSolid Hull( IReadOnlyList<Vector3> points )
	{
		return new ArchSolid { Form = ArchSolidForm.Hull, Points = points };
	}

	// A shape smaller than this in every direction is not worth a solver body - it is a beading or a lip, and the
	// piece it sits on already carries the collision the player meets.
	const float Least = 1.5f;

	public bool IsUsable
	{
		get
		{
			if ( Form == ArchSolidForm.Box )
			{
				return Size.x > Least && Size.y > Least && Size.z > Least;
			}

			if ( Form == ArchSolidForm.Cylinder )
			{
				return Radius > Least * 0.5f && Height > Least;
			}

			return Points is { Count: >= 4 };
		}
	}

	public ulong Fold( ulong hash )
	{
		hash = ArchHash.Fold( hash, (int)Form );

		if ( Form == ArchSolidForm.Hull )
		{
			foreach ( var point in Points )
			{
				hash = ArchHash.Fold( hash, point );
			}

			return hash;
		}

		hash = ArchHash.Fold( hash, Centre );
		hash = ArchHash.Fold( hash, Size );
		hash = ArchHash.Fold( hash, Radius );
		hash = ArchHash.Fold( hash, Height );

		return ArchHash.Fold( hash, Slices );
	}
}