Editor-side generator for a single architectural band (board) mesh. Defines ArchBandFrame for coordinate transforms and ArchBandGen with functions to compute folds, outward normals, and to emit mesh geometry for straight and raked bands into an ArchMesh using given ends, frame, dimensions and brush.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Where a band's along/across coordinates land. A wall canvas is already drawn in its own wall's
// frame, so it bands in Local; anything drawing a board in world space - a walkway's own passage -
// hands over the wall it runs on instead, and the two emit the same solid from the same ends.
public readonly struct ArchBandFrame
{
public Vector2 Origin { get; init; }
public Vector2 Along { get; init; }
public Vector2 Across { get; init; }
public static ArchBandFrame Local => new()
{
Origin = Vector2.Zero,
Along = new Vector2( 1f, 0f ),
Across = new Vector2( 0f, 1f )
};
// Across is the wall's normal, so the pair turns the same way Local's does and the windings hold.
public static ArchBandFrame On( ArchWall wall ) => new()
{
Origin = wall.Start,
Along = wall.Direction,
Across = wall.Normal
};
public Vector3 At( float along, float across, float height )
{
var point = Origin + Along * along + Across * across;
return new Vector3( point.x, point.y, height );
}
}
// The one board. Its back and underside are buried in what it trims, so neither is emitted; its ends
// come from ArchBandEnd, and a Mitred end grows no cap because the board round the corner meets it
// on the 45. A Free end caps like any other - the finding belongs to arch_audit, not to the mesh.
public static class ArchBandGen
{
// How far along the outgoing edge a corner folds the board's section, per inch of offset from the run
// line - so one number mitres the near face and the far face of the same board on one bisector plane.
// The sign is the corner's own: an inside corner shortens the face, an outside corner runs it past, and
// the board coming the other way is folded by the same amount the other way. Butting them instead left
// a wedge missing at every outside corner and a doubled board at every inside one.
public const float SharpestFold = 4f;
public static float Fold( Vector2 incoming, Vector2 outgoing, float side )
{
if ( incoming.Length < 0.001f || outgoing.Length < 0.001f )
{
return 0f;
}
var before = Outward( incoming, side );
var after = Outward( outgoing, side );
var opened = 1f + Vector2.Dot( before, after );
if ( opened < 0.05f )
{
return 0f;
}
var fold = Vector2.Dot( before, outgoing.Normal ) / opened;
// Past the clamp the two boards no longer meet on one plane, so there is no mitre to make: the end
// caps and butts instead. Clamping the number would have left a suppressed cap over a real hole.
return MathF.Abs( fold ) > SharpestFold ? 0f : fold;
}
public static Vector2 Outward( Vector2 along, float side )
{
var unit = along.Normal;
return new Vector2( -unit.y, unit.x ) * side;
}
public static void Run(
ArchMesh canvas,
ArchBandEnd from,
ArchBandEnd to,
ArchBandFrame frame,
float back,
float depth,
float bottom,
float top,
ArchBrush brush )
{
if ( to.Inner - from.Inner < 0.5f || to.Outer - from.Outer < 0.5f || top - bottom < 0.05f )
{
return;
}
var backFrom = frame.At( from.Inner, back, bottom );
var backTo = frame.At( to.Inner, back, bottom );
var faceTo = frame.At( to.Outer, back + depth, bottom );
var faceFrom = frame.At( from.Outer, back + depth, bottom );
// Wound counter-clockwise, the winding that points every side face outward.
void Side( Vector3 a, Vector3 b )
{
canvas.Quad( a, b, b.WithZ( top ), a.WithZ( top ), brush );
}
Side( faceTo, faceFrom );
if ( from.Capped )
{
Side( faceFrom, backFrom );
}
if ( to.Capped )
{
Side( backTo, faceTo );
}
canvas.Polygon( new List<Vector3> { backFrom, backTo, faceTo, faceFrom }.Select( point => point.WithZ( top ) ).ToList(), brush );
Return( canvas, from, frame, back, depth, bottom, top, brush, true );
Return( canvas, to, frame, back, depth, bottom, top, brush, false );
}
static void Return(
ArchMesh canvas,
ArchBandEnd end,
ArchBandFrame frame,
float back,
float depth,
float bottom,
float top,
ArchBrush brush,
bool starting )
{
if ( end.Stop != ArchBandStop.Returned || end.Return < 0.5f )
{
return;
}
var seat = frame.At( end.Inner, back, 0f );
var returned = new ArchBandFrame
{
Origin = new Vector2( seat.x, seat.y ),
Along = -frame.Across,
Across = frame.Along * (starting ? -1f : 1f)
};
Run(
canvas,
new ArchBandEnd( 0f, -depth, ArchBandStop.Mitred ),
new ArchBandEnd( end.Return, end.Return, ArchBandStop.Cut ),
returned,
0f,
depth,
bottom,
top,
brush );
}
// The same board following a climb: a flight's rake, a rising link's floor. Both edges are given
// their own pair of heights, so a run that starts level and ends raked needs no second call. Its
// ends carry the same splay a level board's do - a rake reaching a mouth mitres round the jamb
// like anything else, and a solid prism always caps, so nothing is left open where it does not.
public static void Raked(
ArchMesh canvas,
ArchBandEnd from,
ArchBandEnd to,
ArchBandFrame frame,
float back,
float depth,
float lowFrom,
float lowTo,
float highFrom,
float highTo,
ArchBrush brush )
{
if ( to.Inner - from.Inner < 0.5f || (highFrom - lowFrom < 0.1f && highTo - lowTo < 0.1f) )
{
return;
}
// An end's two corners share ITS heights, splay or no splay. Carrying the rake on past the
// mitre tilts the cut face by the slope over the board's depth, and the level board coming
// the other way round the jamb meets a plane half an inch off its own.
var lower = new List<Vector3>
{
frame.At( from.Inner, back, lowFrom ),
frame.At( to.Inner, back, lowTo ),
frame.At( to.Outer, back + depth, lowTo ),
frame.At( from.Outer, back + depth, lowFrom )
};
var upper = new List<Vector3>
{
frame.At( from.Inner, back, highFrom ),
frame.At( to.Inner, back, highTo ),
frame.At( to.Outer, back + depth, highTo ),
frame.At( from.Outer, back + depth, highFrom )
};
canvas.Prism( lower, upper, brush );
}
}