Editor/Pillar/ArchPillarSnap.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Seats a column against a snap target rather than centring on it — direction comes from the author's aim.
public static class ArchPillarSnap {
public static Vector2 Seat( ArchPlan plan, ArchKit kit, ArchCursor cursor, ArchPillarPart section ) {
if ( section is null || cursor.Snap == ArchSnapKind.None ) {
return cursor.Plan;
}
// Under a soffit the capital must land flush; elsewhere the widest course must not overhang.
var overhead = cursor.Face == ArchCursorFace.Ceiling || cursor.SnapAbove;
var reach = overhead ? section.Heading : section.Seated;
// Walls snap on centreline (need half-thickness offset); outlines are already the outer face.
var skin = cursor.SnappedTo is { } wall ? ArchWallSection.Thickness( wall, kit ) * 0.5f : 0f;
var inside = Inside( plan, kit, cursor );
var step = MathF.Max( 1f, MathF.Max( reach.x, reach.y ) );
if ( cursor.Snap == ArchSnapKind.Corner ) {
var toward = Inward( inside, cursor.Plan, step, cursor.Free );
return cursor.Plan + new Vector2( toward.x * (reach.x - skin), toward.y * (reach.y - skin) );
}
var heading = cursor.SnapHeading.Length > 0.001f ? cursor.SnapHeading : cursor.SnappedTo?.Direction ?? Vector2.Zero;
if ( heading.Length < 0.001f ) {
return cursor.Plan;
}
var normal = new Vector2( -heading.y, heading.x );
var across = MathF.Abs( normal.x ) > MathF.Abs( normal.y ) ? reach.x : reach.y;
var side = ArchFootprint.Encloses( inside, cursor.Plan + normal * step ) ? 1f
: ArchFootprint.Encloses( inside, cursor.Plan - normal * step ) ? -1f
: Toward( Vector2.Dot( cursor.Free - cursor.Plan, normal ) );
return cursor.Plan + normal * side * (across - skin);
}
// Which diagonal is "inside" comes from the plan; falls back to the author's aim if nothing encloses.
static Vector2 Inward( IReadOnlyList<List<Vector2>> inside, Vector2 corner, float step, Vector2 free ) {
foreach ( var toward in Diagonals ) {
if ( ArchFootprint.Encloses( inside, corner + toward * step ) ) {
return toward;
}
}
return new Vector2( Toward( free.x - corner.x ), Toward( free.y - corner.y ) );
}
static readonly Vector2[] Diagonals =
{
new( 1f, 1f ),
new( -1f, 1f ),
new( -1f, -1f ),
new( 1f, -1f )
};
// Under an overhang use that slab's outline; otherwise union all storeys (stacked plots cancel in Encloses).
static List<List<Vector2>> Inside( ArchPlan plan, ArchKit kit, ArchCursor cursor ) {
if ( cursor.Surface is { Count: >= 3 } surface ) {
return new List<List<Vector2>> { surface.ToList() };
}
var rooms = plan?.AllRooms() ?? Enumerable.Empty<ArchRoom>();
return ArchFootprint.Union( ArchRegion.Shell( ArchRegion.Footprints( rooms ), kit.WallThickness ) );
}
// A free point sitting exactly on the snap says nothing, so the section stays where the grid put it.
static float Toward( float offset ) => MathF.Abs( offset ) < 0.01f ? 0f : MathF.Sign( offset );
}