Editor-side generator for arch spans. It computes geometry for beams, haunches, coves and arched voussoirs and emits prisms into an ArchMesh using provided ArchBrush, splitting sections into near/far faces and welding where needed.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// One resolved span, whoever asked for it. The ends are the piers' centres and the insets are their half-sections
// along the run, so the solid runs INTO each pier rather than up to its face.
public readonly record struct ArchSpanRun
{
public Vector2 From { get; init; }
public Vector2 To { get; init; }
public float Springing { get; init; }
public float Top { get; init; }
public float Thickness { get; init; }
public float InsetFrom { get; init; }
public float InsetTo { get; init; }
public int Segments { get; init; }
public float Ring { get; init; }
public PillarSpan Form { get; init; }
}
// The ONE emitter for every span in the tool: the arcade a pillar grid stands and the arch an author picks two
// columns for are the same solid. A second copy of the voussoir sweep is the defect this exists to prevent.
public static class ArchSpanGen
{
public static void Build( ArchMesh canvas, ArchSpanRun run, ArchBrush brush )
{
var direction = run.To - run.From;
if ( run.Form == PillarSpan.None || direction.Length < 1f )
{
return;
}
var unit = direction.Normal;
var start = run.From + unit * run.InsetFrom;
var end = run.To - unit * run.InsetTo;
var chord = Vector2.Dot( end - start, unit );
var side = new Vector2( -unit.y, unit.x ) * MathF.Max( 1f, run.Thickness ) * 0.5f;
if ( run.Form == PillarSpan.Haunch )
{
// A cove needs only room for itself at each end, so it survives a gap a whole arch could not span.
Haunch( canvas, start, end, unit, side, run, brush );
return;
}
if ( chord < 4f )
{
return;
}
if ( run.Form == PillarSpan.Beam )
{
Beam( canvas, start, end, side, run.Springing, run.Top, brush );
return;
}
Arch( canvas, start, end, unit, side, chord, run, brush );
}
static void Beam( ArchMesh canvas, Vector2 start, Vector2 end, Vector2 side, float springing, float top, ArchBrush brush )
{
var bottom = new List<Vector3>
{
new( start.x + side.x, start.y + side.y, springing ),
new( end.x + side.x, end.y + side.y, springing ),
new( end.x - side.x, end.y - side.y, springing ),
new( start.x - side.x, start.y - side.y, springing )
};
var upper = new List<Vector3>();
foreach ( var point in bottom )
{
upper.Add( point.WithZ( top ) );
}
canvas.Prism( bottom, upper, brush );
}
// The corner where a pier meets what it carries, filled up to a concave quarter-round. Emitted as a fan off
// that corner rather than as one polygon: a concave section gets no hull, so it would come out of the mesh
// with no solid to stand on.
static void Haunch( ArchMesh canvas, Vector2 start, Vector2 end, Vector2 unit, Vector2 side, ArchSpanRun run, ArchBrush brush )
{
var rise = MathF.Max( 2f, run.Top - run.Springing );
var reach = Vector2.Dot( end - start, unit );
if ( reach < 1f )
{
return;
}
// Never past halfway, or two coves on a short span grow through each other.
var cove = MathF.Min( rise, reach * 0.5f );
Cove( canvas, start, unit, side, run, cove, brush );
Cove( canvas, end, -unit, side, run, cove, brush );
}
static void Cove( ArchMesh canvas, Vector2 face, Vector2 unit, Vector2 side, ArchSpanRun run, float cove, ArchBrush brush )
{
var segments = Math.Max( 3, run.Segments );
var top = run.Top;
var eye = face + unit * cove;
var corner = new Vector3( face.x, face.y, top );
using var welding = canvas.Welding();
for ( var index = 0; index < segments; index++ )
{
var near = Turn( face, eye, unit, cove, index / (float)segments, top );
var far = Turn( face, eye, unit, cove, (index + 1) / (float)segments, top );
Sweep( canvas, side, brush, corner, near, far );
}
}
// Round the far corner of the cove's own square: at nothing it stands on the pier face a rise down, at one it
// stands on the soffit a rise along.
static Vector3 Turn( Vector2 face, Vector2 eye, Vector2 unit, float cove, float fraction, float top )
{
var angle = fraction * MathF.PI * 0.5f;
var flat = eye - unit * cove * MathF.Cos( angle );
return new Vector3( flat.x, flat.y, top - cove + cove * MathF.Sin( angle ) );
}
// The ring is clamped - unclamped, the extrados flared sideways past its column.
static void Arch( ArchMesh canvas, Vector2 start, Vector2 end, Vector2 unit, Vector2 side, float chord, ArchSpanRun run, ArchBrush brush )
{
var centre = Vector2.Lerp( start, end, 0.5f );
var segments = Math.Max( 3, run.Segments );
var band = MathF.Max( 6f, run.Top - run.Springing );
var ring = Math.Clamp( MathF.Max( 2f, run.Ring ), 2f, band - 2f );
var rise = band - ring;
var radius = (chord * chord * 0.25f + rise * rise) / (2f * rise);
var eye = run.Springing + rise - radius;
var halfAngle = MathF.Asin( Math.Clamp( chord * 0.5f / radius, -1f, 1f ) );
var reach = chord * 0.5f;
Vector3 At( float along, float height ) => new( centre.x + unit.x * along, centre.y + unit.y * along, height );
Voussoir Cut( float t )
{
var angle = -halfAngle + t * halfAngle * 2f;
return new Voussoir
{
Along = radius * MathF.Sin( angle ),
Inner = eye + radius * MathF.Cos( angle ),
OuterAlong = Math.Clamp( radius * MathF.Sin( angle ) + MathF.Sin( angle ) * ring, -reach, reach ),
Outer = eye + radius * MathF.Cos( angle ) + MathF.Cos( angle ) * ring
};
}
for ( var index = 0; index < segments; index++ )
{
var near = Cut( index / (float)segments );
var far = Cut( (index + 1) / (float)segments );
Sweep( canvas, side, brush,
At( near.Along, near.Inner ),
At( far.Along, far.Inner ),
At( far.OuterAlong, far.Outer ),
At( near.OuterAlong, near.Outer ) );
if ( MathF.Min( near.Outer, far.Outer ) >= run.Top - 0.2f || MathF.Abs( far.OuterAlong - near.OuterAlong ) < 0.05f )
{
continue;
}
Sweep( canvas, side, brush,
At( near.OuterAlong, MathF.Min( near.Outer, run.Top ) ),
At( far.OuterAlong, MathF.Min( far.Outer, run.Top ) ),
At( far.OuterAlong, run.Top ),
At( near.OuterAlong, run.Top ) );
}
}
readonly struct Voussoir
{
public float Along { get; init; }
public float Inner { get; init; }
public float OuterAlong { get; init; }
public float Outer { get; init; }
}
static void Sweep( ArchMesh canvas, Vector2 side, ArchBrush brush, params Vector3[] section )
{
var near = new List<Vector3>();
var far = new List<Vector3>();
foreach ( var point in section )
{
near.Add( point + new Vector3( side.x, side.y, 0f ) );
far.Add( point - new Vector3( side.x, side.y, 0f ) );
}
canvas.Prism( near, far, brush );
}
}