Editor/Carve/ArchCarveVolume.cs
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 };
// Where a ray meets this plane. A raked plane and a ray are both linear, so the crossing solves in closed
// form; every deck a cursor can come to rest on is measured through here so a roof and a ramp cannot answer
// the same ray two different ways.
public bool Crosses( Ray ray, out float reach, out Vector3 hit ) {
reach = 0f;
hit = default;
var closing = ray.Forward.z - Vector2.Dot( Fall, new Vector2( ray.Forward.x, ray.Forward.y ) );
if ( MathF.Abs( closing ) < 0.0001f ) {
return false;
}
reach = (At( new Vector2( ray.Position.x, ray.Position.y ) ) - ray.Position.z) / closing;
if ( reach < 0f ) {
return false;
}
hit = ray.Position + ray.Forward * reach;
return true;
}
}
// 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 );
}