Editor-side immutable value types describing carving volumes and planes used by the level/architecture editor. Defines ArchCarvePlane (a planar height with origin and fall), ArchCarveBreak (ruin/break metadata), and ArchCarveVolume (a footprint with floor/ceiling planes and helper constructors for common volume shapes).
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// The fall is what makes a raked deck a carveable solid - not a lofted quad.
public readonly struct ArchCarvePlane
{
public float Datum { get; init; }
public Vector2 Origin { get; init; }
public Vector2 Fall { get; init; }
public static ArchCarvePlane Level( float height ) => new() { Datum = height };
public static ArchCarvePlane Through( Vector2 origin, float datum, Vector2 fall )
{
return new ArchCarvePlane { Origin = origin, Datum = datum, Fall = fall };
}
public bool Rakes => Fall.Length > 0.0001f;
public float At( Vector2 point ) => Datum + Vector2.Dot( Fall, point - Origin );
public ArchCarvePlane Raised( float by ) => new() { Datum = Datum + by, Origin = Origin, Fall = Fall };
}
// A ruined edge, carried by the volume that takes the bite. The seed is the cut's own id, so the same shaft
// hands back the same ruin after a hotload and the build cache is not defeated by a vertex that moved.
public readonly struct ArchCarveBreak
{
public int Seed { get; init; }
public float Jitter { get; init; }
public bool Breaks => Seed != 0 && Jitter > ArchGridService.FinestSize;
}
// Both sides of a carve - the solid and the cut - are this same volume.
public readonly struct ArchCarveVolume
{
public IReadOnlyList<Vector2> Footprint { get; init; }
public ArchCarvePlane Floor { get; init; }
public ArchCarvePlane Ceiling { get; init; }
public ArchCarveBreak Break { get; init; }
public static ArchCarveVolume Over( IReadOnlyList<Vector2> footprint, float from, float to )
{
return new ArchCarveVolume
{
Footprint = footprint,
Floor = ArchCarvePlane.Level( MathF.Min( from, to ) ),
Ceiling = ArchCarvePlane.Level( MathF.Max( from, to ) )
};
}
public ArchCarveVolume Breaking( ArchCarveBreak breaking )
{
return new ArchCarveVolume { Footprint = Footprint, Floor = Floor, Ceiling = Ceiling, Break = breaking };
}
// A level base under a raked top - the wedge a ramp is. Not Raked(): that is two parallel planes a constant
// thickness apart, which is a sloping slab and not something whose foot meets the ground it stands on.
public static ArchCarveVolume Under( IReadOnlyList<Vector2> footprint, float from, ArchCarvePlane top )
{
return new ArchCarveVolume { Footprint = footprint, Floor = ArchCarvePlane.Level( from ), Ceiling = top };
}
// The same wedge upside down, and the shape a SUBTRACTED ramp is: nothing taken out at the head, the whole
// band gone at the foot, so what is left under it keeps the raked floor as its own top.
public static ArchCarveVolume Above( IReadOnlyList<Vector2> footprint, ArchCarvePlane floor, float to )
{
return new ArchCarveVolume { Footprint = footprint, Floor = floor, Ceiling = ArchCarvePlane.Level( to ) };
}
// The two faces are parallel, which is what keeps the interval algebra one-dimensional.
public static ArchCarveVolume Raked( IReadOnlyList<Vector2> footprint, ArchCarvePlane plane, float thickness )
{
var depth = MathF.Max( 0.05f, thickness );
return new ArchCarveVolume { Footprint = footprint, Floor = plane, Ceiling = plane.Raised( depth ) };
}
public bool Rakes => Floor.Rakes || Ceiling.Rakes;
public bool Covers( Vector2 point ) => ArchFootprint.Encloses( new[] { Footprint }, point );
}