Editor-side generator for walkway (arch) geometry. It produces mesh geometry for walkway boards, rising walkway walls, jambs, ramps, floors, kerbs and raked trims using provided kit, style and connection helpers.
using System;
using Sandbox;
namespace Sunless.Architecture;
// The walkway's own mesh: only what a normal wall generator cannot say. A flat link's boards die
// square at the mouth corners (the wall joins would mitre them into the host's boards, leaving
// a board tapering to nothing) - its walls are ordinary walls. A rising link's sides rake between
// the two mouths, and the climb stands on a slope the link draws itself - the flight comes from
// the stair generator.
public static class ArchWalkwayGen
{
public static bool IsWalkway( ArchRoom room )
{
return room?.Name?.StartsWith( "Walkway", StringComparison.OrdinalIgnoreCase ) == true;
}
public static bool Rising( ArchRoom room )
{
return IsWalkway( room ) && room.WalkwayTop > room.BaseHeight + 1f;
}
// Boards on the passage faces, drawn on the link's own canvas rather than each wall's, so they
// die square at the mouth corners instead of being mitred into the host's boards. Each end still
// goes through the wall's own join, and an end meeting nothing comes out Free for arch_audit to find.
public static void Boards( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style, ArchConnectionService connections, ArchPlan plan = null )
{
if ( !room.WalkwaySkirting )
{
return;
}
var height = kit.BaseboardHeight;
var depth = kit.BaseboardDepth;
if ( height <= 0.1f || depth <= 0.05f || room.Walls.Count < 2 )
{
return;
}
var half = kit.WallThickness * 0.5f;
var finish = room.FloorBoards ? MathF.Max( 0f, kit.FloorBoardThickness ) : 0f;
var bottom = ArchContact.Bury( kit, room.BaseHeight + finish, 1f );
var top = room.BaseHeight + finish + height;
var brush = style.Brush( ArchSurface.Baseboard, new[] { room.Palette, building.Palette } );
foreach ( var wall in room.Walls )
{
var join = connections is null
? ArchWallJoins.For( wall, room, building, kit.WallThickness )
: connections.JoinWall( wall, room, kit.WallThickness );
// Broken over the shafts crossing the link, the same way a room's own fittings are: a board
// running on through the hole its wall just lost is the trim standing in mid-air.
foreach ( var span in ArchCut.Outside( plan, kit, room.Floor, building?.Id ?? 0, wall.Start, wall.End, bottom, top ) )
{
ArchBandGen.Run( canvas,
Stopped( join.Foot( depth ), span.From, wall.Length, span.From > 0.001f ),
Stopped( join.Head( depth ), span.To, wall.Length, span.To < 0.999f ),
ArchBandFrame.On( wall ), half, depth, bottom, top, brush );
}
}
}
// A board stopped part way along a wall caps square on the jamb it now dies into; only an end that
// still reaches the wall's own end keeps the mitre the join resolved for it.
static ArchBandEnd Stopped( ArchBandEnd end, float along, float length, bool broken )
{
return broken ? new ArchBandEnd( along * length, along * length, ArchBandStop.Free ) : end;
}
// The rising link's shell: two raked walls between the mouths, plus the surface the climb
// stands on - a plain slope, a kerbed ramp, or nothing where the stair generator fills it.
public static void BuildRising( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style, ArchConnectionService connections )
{
ArchFootprint.Bounds( room.Footprint, out var min, out var max );
var alongX = (max - min).x >= (max - min).y;
var low = room.BaseHeight;
var high = room.WalkwayTop;
var clear = room.WallHeight;
var half = kit.WallThickness * 0.5f;
var chain = new[] { room.Palette, building.Palette };
var interior = style.Brush( ArchSurface.WallInterior, chain );
var exterior = style.Brush( ArchSurface.WallExterior, chain );
var reveal = style.Brush( ArchSurface.Reveal, chain );
var from = alongX ? min.x : min.y;
var to = alongX ? max.x : max.y;
// Edge A hugs min across the run, edge B hugs max; inner faces the passage.
var innerA = alongX ? min.y + half : min.x + half;
var innerB = alongX ? max.y - half : max.x - half;
var outerA = alongX ? min.y - half : min.x - half;
var outerB = alongX ? max.y + half : max.x + half;
var cap = style.Brush( ArchSurface.WallCap, chain );
Wall( canvas, alongX, from, to, innerA, outerA, low, high, clear, interior, exterior, cap );
Wall( canvas, alongX, from, to, innerB, outerB, low, high, clear, interior, exterior, cap, flip: true );
// A jamb is the END OF A WALL, so there are two per mouth. One quad across outerA to outerB
// walled the passage off at both ends and read as a link you could not walk into.
Jamb( canvas, alongX, from, outerA, innerA, low, clear, reveal, upRun: false );
Jamb( canvas, alongX, from, innerB, outerB, low, clear, reveal, upRun: false );
Jamb( canvas, alongX, to, outerA, innerA, high, clear, reveal, upRun: true );
Jamb( canvas, alongX, to, innerB, outerB, high, clear, reveal, upRun: true );
if ( room.Interior == WalkwayInterior.Ramp )
{
Ramp( canvas, alongX, from, to, innerA, innerB, low, high, kit, style, room, building );
}
else if ( room.Interior != WalkwayInterior.Stairs )
{
Floor( canvas, alongX, from, to, innerA, innerB, low, high, kit, style, room, building );
}
Raking( canvas, room, building, kit, style, connections, alongX, from, to, low, high );
}
// The thickness face at one wall's end. The near mouth's faces look back along the run, the far
// mouth's look on up it, and each spans only the wall it closes.
static void Jamb( ArchMesh canvas, bool alongX, float along, float near, float far, float seat, float clear, ArchBrush brush, bool upRun )
{
var face = upRun
? new[]
{
P( alongX, along, near, seat ), P( alongX, along, far, seat ),
P( alongX, along, far, seat + clear ), P( alongX, along, near, seat + clear )
}
: new[]
{
P( alongX, along, near, seat ), P( alongX, along, near, seat + clear ),
P( alongX, along, far, seat + clear ), P( alongX, along, far, seat )
};
canvas.Polygon( face, brush, Mirrored( alongX ) );
}
// P swaps the two plan axes for a north-south link, and a swap is a reflection - every hand-wound
// face it places comes out inside out. Prisms normalise themselves; a raw quad has to be told.
static bool Mirrored( bool alongX ) => !alongX;
// The trim where a raked wall meets what is underfoot - the slope, the ramp, or the flight's own
// soffit. A rising link carries no level board, so without this its walls die into the climb bare,
// and the host room's board opens a mitre at the mouth with nothing coming the other way to meet it.
static void Raking( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style, ArchConnectionService connections, bool alongX, float from, float to, float low, float high )
{
var height = kit.BaseboardHeight;
var depth = kit.BaseboardDepth;
if ( !room.WalkwaySkirting || height <= 0.1f || depth <= 0.05f )
{
return;
}
var half = kit.WallThickness * 0.5f;
var brush = style.Brush( ArchSurface.Baseboard, new[] { room.Palette, building.Palette } );
foreach ( var wall in room.Walls )
{
var join = connections is null
? ArchWallJoins.For( wall, room, building, kit.WallThickness )
: connections.JoinWall( wall, room, kit.WallThickness );
var start = Climbed( alongX ? wall.Start.x : wall.Start.y, from, to, low, high );
var finish = Climbed( alongX ? wall.End.x : wall.End.y, from, to, low, high );
ArchBandGen.Raked( canvas, join.Foot( depth ), join.Head( depth ), ArchBandFrame.On( wall ), half, depth,
ArchContact.Bury( kit, start, 1f ), ArchContact.Bury( kit, finish, 1f ),
start + height, finish + height, brush );
}
}
static float Climbed( float at, float from, float to, float low, float high )
{
return MathX.Lerp( low, high, MathX.Clamp( (at - from) / MathF.Max( 1f, to - from ), 0f, 1f ) );
}
// One raked wall: interior face toward the passage, exterior away, both ends open at the mouths,
// and a head and foot of its own - nothing else covers a raked wall's thickness.
static void Wall( ArchMesh canvas, bool alongX, float from, float to, float inner, float outer, float low, float high, float clear, ArchBrush interior, ArchBrush exterior, ArchBrush cap, bool flip = false )
{
var inside = new[]
{
P( alongX, from, inner, low ), P( alongX, from, inner, low + clear ), P( alongX, to, inner, high + clear ), P( alongX, to, inner, high )
};
var outside = new[]
{
P( alongX, from, outer, low ), P( alongX, to, outer, high ), P( alongX, to, outer, high + clear ), P( alongX, from, outer, low + clear )
};
var head = new[]
{
P( alongX, from, inner, low + clear ), P( alongX, from, outer, low + clear ), P( alongX, to, outer, high + clear ), P( alongX, to, inner, high + clear )
};
var foot = new[]
{
P( alongX, from, outer, low ), P( alongX, from, inner, low ), P( alongX, to, inner, high ), P( alongX, to, outer, high )
};
canvas.Polygon( inside, interior, flip ^ Mirrored( alongX ) );
canvas.Polygon( outside, exterior, flip ^ Mirrored( alongX ) );
canvas.Polygon( head, cap, flip ^ Mirrored( alongX ) );
canvas.Polygon( foot, cap, flip ^ Mirrored( alongX ) );
}
// The sloped walking surface: a level-bottomed slab, so the underside reads as one plane.
static void Floor( ArchMesh canvas, bool alongX, float from, float to, float innerA, float innerB, float low, float high, ArchKit kit, ArchStyle style, ArchRoom room, ArchBuilding building )
{
var thickness = MathF.Max( 1f, kit.FloorThickness );
var brush = style.Brush( ArchSurface.Floor, new[] { room.Palette, building.Palette } );
var bottom = new[]
{
P( alongX, from, innerA, low - thickness ), P( alongX, from, innerB, low - thickness ),
P( alongX, to, innerB, high - thickness ), P( alongX, to, innerA, high - thickness )
};
var top = new[]
{
P( alongX, from, innerA, low ), P( alongX, from, innerB, low ),
P( alongX, to, innerB, high ), P( alongX, to, innerA, high )
};
canvas.Prism( bottom, top, brush );
}
// The slope plus kerbed edges, like a driveway ramp but between the link's own walls.
static void Ramp( ArchMesh canvas, bool alongX, float from, float to, float innerA, float innerB, float low, float high, ArchKit kit, ArchStyle style, ArchRoom room, ArchBuilding building )
{
Floor( canvas, alongX, from, to, innerA, innerB, low, high, kit, style, room, building );
var kerbHeight = MathF.Max( 1f, kit.RampKerbHeight );
var kerbWidth = MathF.Max( 1f, kit.RampKerbWidth );
var brush = style.Brush( ArchSurface.Baseboard, new[] { room.Palette, building.Palette } );
Kerb( canvas, alongX, from, to, innerA, true, low, high, kerbHeight, kerbWidth, brush );
Kerb( canvas, alongX, from, to, innerB, false, low, high, kerbHeight, kerbWidth, brush );
}
// A kerbed edge along the ramp: a lofted board from the wall face into the passage.
static void Kerb( ArchMesh canvas, bool alongX, float from, float to, float inner, bool intoPassage, float low, float high, float height, float width, ArchBrush brush )
{
var proud = inner + (intoPassage ? width : -width);
var bottom = new[]
{
P( alongX, from, inner, low ), P( alongX, from, proud, low ),
P( alongX, to, proud, high ), P( alongX, to, inner, high )
};
var top = new[]
{
P( alongX, from, inner, low + height ), P( alongX, from, proud, low + height ),
P( alongX, to, proud, high + height ), P( alongX, to, inner, high + height )
};
canvas.Prism( bottom, top, brush );
}
static Vector3 P( bool alongX, float along, float across, float z )
{
return alongX ? new Vector3( along, across, z ) : new Vector3( across, along, z );
}
}