Editor/Bands/ArchCorniceGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly record struct ArchCorniceLayer( ArchCorniceCourse Course, float Bottom, float Top, float Projection, int Sinks, bool Crowns ) {
public float Rise => Top - Bottom;
// A course stood on the one below lands its underside exactly on that top face, which is a coplanar pair per
// block, so it seats a BITE into it. A course over a MOULDING sinks one bite further: a swept section cannot be
// stretched downward, so it sank whole and its own top came down with it.
public float Seat( ArchKit kit ) => Bottom - Sinks * ArchLap.Bite( kit );
// The top course LAPS into whatever it hangs from. Finished level with the head it lands its lid on the deck's
// own underside, which is a coplanar pair the whole way round - and the face one of them loses to the contact
// pass is the ring reading as an open box from below. TWO bites, because the slab covering that head has
// already lapped one of its own down into it and a single bite lands exactly on that slab's top.
public float Lid( ArchKit kit ) => Crowns ? Top + ArchLap.Bite( kit ) * 2f : Top;
}
// The one resolution of where a cornice's courses stand, and the one emitter for the block rows in them. A wall
// crowns its own head and a deck's parapet ring crowns itself, so both ask here rather than each stacking courses
// its own way and disagreeing by a course.
public static class ArchCorniceGen {
// A cornice HANGS from the head it crowns: the top course finishes level with the wall, so dressing an
// elevation never makes the building taller and a parapet standing over it keeps the height it was drawn at.
// Only a pier steps above, which is the one course whose whole point is breaking the line upward.
public static List<ArchCorniceLayer> Layers( ArchCorniceStyle style, float head ) {
var layers = new List<ArchCorniceLayer>();
if ( style is null || !style.Stands ) {
return layers;
}
var standing = style.Courses.Where( course => course.Stands ).ToList();
var cursor = head - style.Rise;
var sinks = 0;
for ( var index = 0; index < standing.Count; index++ ) {
var course = standing[index];
layers.Add( new ArchCorniceLayer( course, cursor, cursor + course.Rise, MathF.Max( 0f, course.Projection ),
sinks, index == standing.Count - 1 ) );
sinks = course.Kind == CorniceCourse.Moulding ? sinks + 1 : 1;
cursor += course.Rise;
}
return layers;
}
// How far the widest course stands out of the face it hangs on. Read by the generator AND by anything
// measuring the elevation, or the crown is one number in the mesh and another on the sheet.
public static float Reach( ArchCorniceStyle style ) => style is null ? 0f : style.Projection;
// The blocks a dentil or modillion course stands on, counted off the run by ArchDivide rather than typed
// against the style. They do not mitre and they do not return: a block that turned a corner would be a
// moulding pretending to be one.
public static void Blocks(
ArchMesh canvas,
ArchCorniceLayer layer,
ArchKit kit,
ArchBandFrame frame,
float from,
float to,
float back,
float outward,
ArchProfile section,
ArchBrush brush ) {
var course = layer.Course;
var width = MathF.Max( 0.4f, course.Block > 0.05f ? course.Block : kit.DentilWidth );
var gap = MathF.Max( 0.2f, course.Gap > 0.05f ? course.Gap : kit.DentilGap );
var span = to - from;
if ( span < width || MathF.Abs( outward ) < 0.05f ) {
return;
}
var division = ArchDivide.AtMost( span, width + gap );
var bottom = layer.Seat( kit );
for ( var block = 0; block < division.Count; block++ ) {
// Centred in its own step, so a run that does not divide evenly loses the slack at both ends rather
// than standing a half block against the corner.
var at = from + division.Step * (block + 0.5f) - width * 0.5f;
if ( section is not null ) {
Bracket( canvas, layer, frame, at + width * 0.5f, back, outward, bottom, section, brush );
continue;
}
var seat = new List<Vector3>
{
frame.At( at, back, bottom ),
frame.At( at + width, back, bottom ),
frame.At( at + width, back + outward, bottom ),
frame.At( at, back + outward, bottom )
};
canvas.Prism( seat, seat.Select( point => point.WithZ( layer.Lid( kit ) ) ).ToList(), brush );
}
}
// Swept ACROSS the projection rather than along the run, so the section is the bracket's own side view - the
// scroll a modillion is read by. Its length is the projection, so one section serves every course depth.
static void Bracket( ArchMesh canvas, ArchCorniceLayer layer, ArchBandFrame frame, float centre, float back, float outward, float bottom, ArchProfile section, ArchBrush brush ) {
canvas.Extrude(
new List<Vector3>
{
frame.At( centre, back + outward, bottom ),
frame.At( centre, back, bottom )
},
section,
1f,
Rotation.Identity,
brush );
}
}