Editor-side shape resolver for wall modifiers. Defines ArchWallModBlock and ArchWallModShape, computes resolved modifier geometry (positions, extents, blocks) for wall joins and runs, and provides helpers to enumerate faces and stack blocks per modifier kind.
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);
public bool Spans( float bottom, float top ) => Stands && top > Bottom + 0.05f && bottom < Top - 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 = MathF.Max( 0f, 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.Depth );
var depth = part.Carves ? MathF.Min( reach, thickness - ArchContact.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.Kind, 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( ArchWallMod kind, float from, float to, float bottom, float top, float depth )
{
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;
}
if ( !kind.Coursed() )
{
yield return new ArchWallModBlock( from, to, bottom, top, depth, depth );
yield break;
}
var width = to - from;
var courses = ArchDivide.AtMost( top - bottom, MathF.Max( ArchGridService.FinestSize, width * 0.75f ) );
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.
var half = width * (index % 2 == 0 ? 0.5f : 0.28f);
var bay = courses.Bay( index );
yield return new ArchWallModBlock(
centre - half, centre + half,
bottom + bay.From, bottom + bay.To,
depth, depth );
}
}
}