Editor/Wall/ArchWallModShape.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One solid of a modifier. A quoin is a stack of them; everything else is a single block. Foot and
// head depth differ only where the kind tapers, and that is what turns a beam into a prism.
public readonly record struct ArchWallModBlock( float From, float To, float Bottom, float Top, float Foot, float Head );
// The ONE resolution behind a wall modifier: where it sits on the run, how far it reaches, and the
// blocks it is made of. The generator emits these, the ghost draws these, the handles pull these.
public sealed class ArchWallModShape {
public ArchWallModPart Part { get; init; }
public bool Carves { get; init; }
public bool Sealed { get; init; }
// Which elevations carry it. Both is a pier reading on either face; the two are not exclusive.
public bool Outside { get; init; }
public bool Inside { get; init; }
public float From { get; init; }
public float To { get; init; }
public float Bottom { get; init; }
public float Top { get; init; }
public float Depth { get; init; }
public IReadOnlyList<ArchWallModBlock> Blocks { get; init; } = Array.Empty<ArchWallModBlock>();
public bool Stands => To - From > 0.5f && Top - Bottom > 0.5f && Depth > 0.05f && (Outside || Inside);
// A shape standing to the storey head carries a course straddling it: the clamp is the STOREY's, not the pier's.
public bool Spans( float bottom, float top, float wallHeight ) {
if ( !Stands || top <= Bottom + 0.05f ) {
return false;
}
var reach = wallHeight > 0.05f && Top >= wallHeight - 0.05f ? MathF.Max( Top, top ) : Top;
return bottom < reach - 0.05f;
}
// The faces this block is emitted on, as the sign that turns the wall's half-thickness into the
// plane it stands on: -1 is the exterior skin's side, +1 the room's.
public IEnumerable<float> Faces() {
if ( Outside ) {
yield return -1f;
}
if ( Inside ) {
yield return 1f;
}
}
// Every modifier on one wall, resolved against the run its join actually emits. The generator,
// the ghost and the handles all come through here, so a drag can never show a block that the
// rebuild will not build.
public static List<ArchWallModShape> For( ArchWall wall, ArchWallJoin join, float wallHeight, float thickness, ArchKit kit ) {
return ArchLayerGate.Enabled( wall.Modifiers )
.Select( part => Resolve( part, join.StartExtend, join.EndExtend, wallHeight, thickness, kit ) )
.Where( shape => shape.Stands )
.ToList();
}
public static ArchWallModShape Resolve( ArchWallModPart part, ArchWall wall, ArchKit kit, float wallHeight ) {
return Resolve( part, 0f, wall.Length, wallHeight, ArchWallSection.Thickness( wall, kit ), kit );
}
public static ArchWallModShape Resolve( ArchWallModPart part, float from, float to, float wallHeight, float thickness, ArchKit kit ) {
// The STATION is clamped to the run, not the block: one standing on the end straddles the
// corner, so the piers off two elevations meet and read as the single pier they are.
var runFrom = MathF.Min( from, to );
var runTo = MathF.Max( from, to );
var centre = Math.Clamp( part.Along, runFrom, runTo );
var reachAlong = MathF.Max( ArchGridService.FinestSize, part.Width ) * 0.5f;
var left = centre - reachAlong;
var right = centre + reachAlong;
var bottom = part.Carves ? MathF.Max( 0f, part.Base ) : part.Base;
// An authored height is honoured past the wall head, so one pier can climb several storeys as a
// single layer rather than a stack of them with a slab-thick gap at every floor line. A carved
// recess is the exception: past the head it would be cutting a field that is not there.
var climbed = bottom + part.Height;
var top = part.Height > 0.5f
? part.Carves ? MathF.Min( climbed, wallHeight ) : climbed
: wallHeight;
// Retract, never overextend: a recess landing on the far face is a coplanar pair, and one
// past it is a hole through a wall nobody asked to open.
var reach = MathF.Max( 0.05f, part.Kind.Reach( part.Depth ) );
var depth = part.Carves ? MathF.Min( reach, thickness - ArchLap.Bite( kit ) ) : reach;
depth = MathF.Max( 0f, depth );
// OnSurface is answered by the drag that authors the part; nothing downstream can guess a face,
// so it reads as the street elevation until placement has replaced it.
var both = part.Side == ArchWallSide.Both;
return new ArchWallModShape {
Part = part,
Carves = part.Carves,
Outside = both || part.Side != ArchWallSide.Inside,
Inside = both || part.Side == ArchWallSide.Inside,
From = left,
To = right,
Bottom = bottom,
Top = top,
Depth = depth,
Blocks = Stack( part, left, right, bottom, top, depth ).ToList()
};
}
// Every modifier resolves to the same vocabulary of blocks, so one emitter serves all of them.
static IEnumerable<ArchWallModBlock> Stack( ArchWallModPart part, float from, float to, float bottom, float top, float depth ) {
var kind = part.Kind;
if ( to - from < 0.5f || top - bottom < 0.5f || depth < 0.05f ) {
yield break;
}
if ( kind.Tapers() ) {
yield return new ArchWallModBlock( from, to, bottom, top, depth, depth * 0.4f );
yield break;
}
// Deep at the foot, dying into the elevation at the head: the same prism a buttress stands, fallen
// the other way, which is a canopy sloping down over the pavement.
if ( kind.Falls() ) {
yield return new ArchWallModBlock( from, to, bottom, top, depth, depth * 0.12f );
yield break;
}
if ( !kind.Coursed() && !kind.Banded() ) {
yield return new ArchWallModBlock( from, to, bottom, top, depth, depth );
yield break;
}
var width = to - from;
var pitch = part.Course > 0.5f
? part.Course
: kind.Banded() ? ArchWallModKinds.CourseHeight : MathF.Max( ArchGridService.FinestSize, width * 0.75f );
var courses = ArchDivide.AtMost( top - bottom, pitch );
// Half a joint off each end of every course, so the recess between two of them is the whole joint and the
// stack still finishes exactly on the band it was given.
var joint = (part.Joint > 0.01f ? part.Joint : MathF.Min( 1.5f, pitch * 0.12f )) * 0.5f;
var centre = (from + to) * 0.5f;
for ( var index = 0; index < courses.Count; index++ ) {
// Alternately long and short about the same centre - that is what reads as rustication. A band does
// not alternate: every course takes the whole width and the joint is the only thing that reads.
var half = kind.Banded() ? width * 0.5f : width * (index % 2 == 0 ? 0.5f : 0.28f);
var bay = courses.Bay( index );
var low = bottom + bay.From + joint;
var high = bottom + bay.To - joint;
if ( high - low < 0.05f ) {
continue;
}
yield return new ArchWallModBlock( centre - half, centre + half, low, high, depth, depth );
}
}
}