Editor/Cabinet/ArchCabinetFittingRequest.cs
using Sandbox;
namespace Sunless.Architecture;
// Where on the author's own prefab the mount takes hold, in the PREFAB's local space. A picked vertex gives a
// point and nothing else; a picked edge gives its midpoint and the direction it runs in.
public readonly record struct ArchCabinetAnchor( Vector3 Point, Vector3 Along ) {
public static ArchCabinetAnchor None => default;
public bool Directed => Along.Length > 0.001f;
}
// ANYTHING AN AUTHOR AIMS A PREFAB WITH. A front, a whole carcass and a run's own override all answer the same five
// questions, so the aiming viewport and the landing solve are written once and none of them knows which it is
// holding.
public interface IArchAimedPrefab {
string Prefab { get; }
Vector3 Anchor { get; }
Vector3 AnchorAlong { get; }
Angles AnchorTurn { get; }
bool Scales { get; }
bool Standing => !string.IsNullOrWhiteSpace( Prefab );
bool Anchored => Anchor != Vector3.Zero || AnchorAlong != Vector3.Zero;
bool OnAnEdge => AnchorAlong.Length > 0.01f;
}
// The five answers as a VALUE, for a preset that keeps them as flat fields rather than as an object of its own -
// an opening's leaf art. Folded into a build key, so value semantics are the point.
public readonly record struct ArchAimedPrefab(
string Prefab,
Vector3 Anchor,
Vector3 AnchorAlong,
Angles AnchorTurn,
bool Scales ) : IArchAimedPrefab {
public bool Standing => !string.IsNullOrWhiteSpace( Prefab );
public bool Anchored => Anchor != Vector3.Zero || AnchorAlong != Vector3.Zero;
public bool OnAnEdge => AnchorAlong.Length > 0.01f;
public ulong Fold( ulong hash ) {
hash = ArchHash.Fold( hash, Prefab );
hash = ArchHash.Fold( hash, Anchor );
hash = ArchHash.Fold( hash, AnchorAlong );
hash = ArchHash.Fold( hash, new Vector3( AnchorTurn.pitch, AnchorTurn.yaw, AnchorTurn.roll ) );
return ArchHash.Fold( hash, Scales );
}
}
// A prefab to hang on one mount of one module. The tool draws nothing for it - art the author made beats anything
// extruded - so this is the whole instruction, and the scene pass is the only thing that reads it.
public sealed class ArchFittingRequest {
public string Name { get; init; }
public string Prefab { get; init; }
// The mount's own frame: origin at the top of the opening, at the carcass face, at the run-start end, with
// Forward out of the face, Left along the run and Up up.
public Transform Mount { get; init; }
public Vector3 Space { get; init; }
// Off is the honest default: a front modelled at a real size stands at that size, and squashing one into a
// module it does not fit is a thing the author has to ask for.
public bool Fits { get; init; }
// Written onto every renderer the instance carries, so one row of art re-finishes without a second prefab.
// Empty leaves whatever the prefab was authored with.
public string MaterialGroup { get; init; }
public ArchCabinetAnchor Anchor { get; init; }
// What the author turned the art by in the designer, which is the only answer a VERTEX pick can be given: a
// point carries no direction, so nothing else can tell a front modelled face-on from one modelled edge-on.
public Angles Turn { get; init; }
// The anchor point lands on the mount origin and the prefab stands in the mount's frame - its own x out of the
// cabinet face, y along the run, z up. Solving the position AFTER the scale is what keeps the picked point
// still whether the prefab was fitted or left alone.
public Transform Standing( BBox bounds ) {
var rotation = Turned();
var scale = Fits ? Filled( bounds ) : Vector3.One;
return new Transform( Mount.Position - rotation * (Anchor.Point * scale), rotation, scale );
}
public ulong Fold( ulong hash ) {
hash = ArchHash.Fold( hash, Name );
hash = ArchHash.Fold( hash, Prefab );
hash = ArchHash.Fold( hash, Mount.Position );
hash = ArchHash.Fold( hash, Mount.Rotation.Forward );
hash = ArchHash.Fold( hash, Mount.Rotation.Up );
hash = ArchHash.Fold( hash, Space );
hash = ArchHash.Fold( hash, Fits );
hash = ArchHash.Fold( hash, MaterialGroup );
hash = ArchHash.Fold( hash, Anchor.Point );
hash = ArchHash.Fold( hash, Anchor.Along );
return ArchHash.Fold( hash, new Vector3( Turn.pitch, Turn.yaw, Turn.roll ) );
}
// A picked edge squares the prefab up by the SHORTEST turn onto the mount axis it already points nearest, so
// art modelled straight is left exactly as it is and art a few degrees out is corrected rather than spun. A
// vertex carries no direction at all, so it leaves the prefab standing as the mount's frame puts it.
Rotation Turned() {
var nudged = Mount.Rotation * Turn.ToRotation();
if ( !Anchor.Directed ) {
return nudged;
}
var pointing = nudged * Anchor.Along.Normal;
var nearest = Nearest( pointing );
return pointing.Dot( nearest ) > 0.9999f
? nudged
: Rotation.FromToRotation( pointing, nearest ) * nudged;
}
Vector3 Nearest( Vector3 pointing ) {
var axes = new[]
{
Mount.Rotation.Forward, -Mount.Rotation.Forward,
Mount.Rotation.Left, -Mount.Rotation.Left,
Mount.Rotation.Up, -Mount.Rotation.Up
};
var best = axes[0];
foreach ( var axis in axes ) {
if ( pointing.Dot( axis ) > pointing.Dot( best ) ) {
best = axis;
}
}
return best;
}
Vector3 Filled( BBox bounds ) {
var size = bounds.Size;
return new Vector3( Fitted( size.x, Space.x ), Fitted( size.y, Space.y ), Fitted( size.z, Space.z ) );
}
static float Fitted( float modelled, float room ) {
return modelled > 0.01f && room > 0.01f ? room / modelled : 1f;
}
}