Utility that resolves span geometry for architectural editor tools. It determines which pier or wall an authored span end anchors to, computes seat/pier heights and insets, decides springing/top/thickness for span runs, and returns resolved ArchSpanRun and ArchSpanPier structures used by generators and mesh code.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// What one end of a span stands on, measured with the run's own direction known: where it is, how high its pier
// reaches, how far into that pier the solid runs, and how wide the pier is across the run.
public readonly record struct ArchSpanSeat( Vector2 At, float Head, float Inset, float Across );
// Where one end actually stands and how high the thing under it reaches, so a gesture can mark the pier at the
// height the span will meet it rather than at the storey floor.
public readonly record struct ArchSpanPier( Vector2 At, float Head, bool Standing );
// The ONE resolve. The ghost, the generator, the extents and the report all read it, or the arch the author drags
// is not the arch the mesh comes out as.
public static class ArchSpanShape
{
// How near a pier the gesture has to land to name it rather than a bare point.
public const float Grab = 96f;
public static ArchSpanPier Pier( ArchPlan plan, ArchKit kit, ArchRoom room, ArchSpanEnd end )
{
if ( room is null || end is null )
{
return default;
}
var standing = Standing( plan, end );
var at = To( standing, end );
return new ArchSpanPier( at, Seat( plan, kit, room, standing, end, at, Vector2.Right ).Head, standing is not null );
}
public static bool Resolve( ArchPlan plan, ArchKit kit, ArchRoom room, ArchSpanPart span, out ArchSpanRun run )
{
run = default;
if ( room is null || span?.From is null || span.To is null )
{
return false;
}
var from = Standing( plan, span.From );
var to = Standing( plan, span.To );
var start = To( from, span.From );
var end = To( to, span.To );
var direction = end - start;
if ( direction.Length < 1f )
{
return false;
}
var unit = direction.Normal;
var seatFrom = Seat( plan, kit, room, from, span.From, start, unit );
var seatTo = Seat( plan, kit, room, to, span.To, end, unit );
var top = MathF.Min( seatFrom.Head, seatTo.Head ) - MathF.Max( 0f, span.Drop );
var springing = Springs( room, span, top );
var across = MathF.Max( seatFrom.Across, seatTo.Across );
run = new ArchSpanRun
{
From = start,
To = end,
Springing = springing,
Top = top,
Thickness = Across( span, across ),
InsetFrom = seatFrom.Inset,
InsetTo = seatTo.Inset,
Segments = span.Segments,
Ring = span.Ring,
Form = span.Form
};
return top - springing > 1f;
}
// Taking the piers' own section means taking it SHY, or the span's two sides land exactly on the column's two
// sides and both pairs flicker. Piers that offer no section at all - a span run wall to wall - fall back to a
// section of their own rather than to a shy nothing.
static float Across( ArchSpanPart span, float piers )
{
if ( span.Thickness > 0.5f )
{
return span.Thickness;
}
return piers > 1f ? MathF.Max( 1f, piers - ArchContact.Shy ) : 8f;
}
// An authored springing is a height above the floor, so two spans set the same way start level whatever their
// piers reach; left at zero the form's rise decides and the band hangs off the head instead.
public static float Springs( ArchRoom room, ArchSpanPart span, float top )
{
var wanted = span.Springing > 0.5f ? room.BaseHeight + span.Springing : top - span.Band;
return MathF.Max( room.BaseHeight + 8f, MathF.Min( wanted, top - 2f ) );
}
// A pier's own head, so a span between two columns of different heights springs off the shorter one.
static ArchSpanSeat Seat( ArchPlan plan, ArchKit kit, ArchRoom room, object standing, ArchSpanEnd end, Vector2 at, Vector2 unit )
{
var bite = ArchContact.Bite( kit );
var ceiling = room.BaseHeight + ArchFloorGen.WallHeight( room, kit );
if ( standing is ArchPillarPart pillar )
{
var half = pillar.Half;
var local = Rotation.FromYaw( -pillar.Yaw ) * new Vector3( unit.x, unit.y, 0f );
var along = MathF.Abs( local.x ) * half.x + MathF.Abs( local.y ) * half.y;
var side = MathF.Abs( local.y ) * half.x + MathF.Abs( local.x ) * half.y;
return new ArchSpanSeat( at, pillar.BaseHeight + ArchPillarGen.Height( pillar, room, kit, plan ), MathF.Max( 0f, along - bite ), side * 2f );
}
if ( standing is ArchWall wall )
{
var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
return new ArchSpanSeat( at, room.BaseHeight + ArchWallSection.Height( wall, room, kit ), MathF.Max( 0f, thickness * 0.5f - bite ), 0f );
}
return new ArchSpanSeat( at, ceiling, 0f, 0f );
}
// Re-read every build rather than stored, which is the whole reason an end names a part instead of a point.
static Vector2 To( object standing, ArchSpanEnd end )
{
// A grid that lost the column this end named - its counts pulled in - falls back to where the gesture
// landed rather than to the origin, which is what FirstOrDefault would have handed back.
if ( standing is ArchPillarPart pillar )
{
return ArchPillarGen.Columns( pillar )
.Where( candidate => candidate.X == end.ColumnX && candidate.Y == end.ColumnY )
.Select( candidate => new Vector2( candidate.At.x, candidate.At.y ) )
.DefaultIfEmpty( end.At )
.First();
}
if ( standing is ArchWall wall )
{
return Nearest( wall, end.At );
}
return end.At;
}
public static object Standing( ArchPlan plan, ArchSpanEnd end )
{
if ( plan is null || end is null || end.PartId == 0 )
{
return null;
}
foreach ( var room in plan.Buildings.SelectMany( building => building.Rooms ) )
{
if ( room.Pillars.FirstOrDefault( pillar => pillar.Id == end.PartId ) is { } pillar )
{
return pillar;
}
if ( room.Walls.FirstOrDefault( wall => wall.Id == end.PartId ) is { } wall )
{
return wall;
}
}
return null;
}
// What the gesture landed on: a column of any grid on the storey first, because a corner an author aimed at is
// a coordinate they placed where a wall face is only a line to lie along.
public static ArchSpanEnd Anchored( ArchPlan plan, ArchRoom room, Vector2 point )
{
var end = new ArchSpanEnd { At = point };
var closest = Grab;
foreach ( var pillar in room.Pillars.Where( pillar => pillar.Placement != PillarPlacement.Pilaster ) )
{
foreach ( var column in ArchPillarGen.Columns( pillar ) )
{
var at = new Vector2( column.At.x, column.At.y );
var reach = (at - point).Length;
if ( reach >= closest )
{
continue;
}
closest = reach;
end = new ArchSpanEnd { PartId = pillar.Id, ColumnX = column.X, ColumnY = column.Y, At = at };
}
}
if ( end.PartId != 0 )
{
return end;
}
foreach ( var wall in room.Walls )
{
var at = Nearest( wall, point );
var reach = (at - point).Length;
if ( reach >= closest )
{
continue;
}
closest = reach;
end = new ArchSpanEnd { PartId = wall.Id, At = at };
}
return end;
}
static Vector2 Nearest( ArchWall wall, Vector2 point )
{
var length = wall.Length;
if ( length < 0.001f )
{
return wall.Start;
}
return wall.PointAt( Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length ) );
}
}