Editor/Stair/ArchStairRail.cs
using System;
using System.Linq;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

public readonly struct ArchStairGuardNode {
	public Vector2 At { get; init; }
	public float Foot { get; init; }
	public float Top { get; init; }
	public StairRailing Railing { get; init; }
}

public static partial class ArchStairGen {
	const float RailDepth = 3f;
	const float RailSection = 2.2f;
	const float BalusterSection = 1.3f;
	const float CapOversail = 0.9f;
	const float CapDepth = 1.8f;
	const float WellReturn = RailDepth * 1.5f;
	const float WallRailGap = 1.6f;
	const float WallBracketSpacing = 48f;
	const float PostAngle = 60f;

	static float Spacing( ArchStairPart stair, ArchKit kit ) {
		return MathF.Max( 3f, stair.BalusterSpacing > 0.01f ? stair.BalusterSpacing : kit.StairBalusterSpacing );
	}

	static void Balustrade( ArchMesh canvas, ArchStairPart stair, ArchStairShape shape, ArchKit kit, ArchStairSkin skin, ArchStairVoiding voiding, List<ArchStairGuardNode> posts, ArchPlan plan, ArchBuilding building ) {
		var height = MathF.Max( 12f, kit.HandrailHeight );
		var walks = new List<List<ArchStairGuardNode>>();

		WallRails( canvas, stair, shape, kit, skin, height );

		if ( (stair.Guard != StairGuard.None && stair.AutoRailings) || stair.Lanes.Any( lane => lane.Guards.Count > 0 ) ) {
			walks.AddRange( Walks( stair, shape, kit, height, right: true ) );
			walks.AddRange( Walks( stair, shape, kit, height, right: false ) );

			foreach ( var run in shape.Runs ) {
				foreach ( var border in ArchStairVoid.Borders( voiding, run ) ) {
					BorderWalk( walks, run, border, kit, height );
				}
			}
		}

		var chains = walks.Concat( WellRings( stair, shape, kit, height, walks, plan, building ) ).ToList();

		foreach ( var chain in chains ) {
			Handrail( canvas, chain, kit, skin );
			Posting( chain, kit, posts );
		}

		foreach ( var chain in chains ) {
			Infill( canvas, stair, kit, skin, chain, posts );
		}
	}

	static List<List<ArchStairGuardNode>> Walks( ArchStairPart stair, ArchStairShape shape, ArchKit kit, float height, bool right ) {
		var walks = new List<List<ArchStairGuardNode>>();
		var walk = new List<ArchStairGuardNode>();

		for ( var index = 0; index < shape.Runs.Count; index++ ) {
			var run = shape.Runs[index];

			if ( !Guarded( run, right ) ) {
				walk = Break( walks, walk );

				continue;
			}

			var lane = Lane( run, right );

			// A railing dragged to part of a flight comes out over that part. Nothing authored spans 0 to 1, so
			// this is a no-op for every stair that never touched one.
			var span = right ? run.SpanRight : run.SpanLeft;
			var from = MathF.Max( 0f, span.x * run.Length );
			var to = MathF.Min( Reaches( shape, stair, run, height ), span.y * run.Length );

			if ( to - from < 1f ) {
				walk = Break( walks, walk );

				continue;
			}

			if ( from > 0.5f ) {
				walk = Break( walks, walk );
			}

			Climbing( walk, run, lane, from, to, height, right ? run.RailingRight : run.RailingLeft );

			// A landing standing on a floor does NOT end the walk. The rail turns onto it and carries round its
			// railed edges, which is the quarter turn at the head of a flight - the rake arriving, the newel, and
			// the run along the storey above leaving it. Handing the landing to the well ring instead is what
			// stopped the rail dead at the top; the ring knows to leave those edges alone.
			var pad = to > run.Length - 0.5f ? PadBetween( shape, run ) : null;

			if ( pad is null ) {
				walk = Break( walks, walk );

				continue;
			}

			var above = index + 1 < shape.Runs.Count ? shape.Runs[index + 1] : null;
			var carries = above is not null && Guarded( above, right );

			if ( !Across( pad, walk[^1].At, carries ? above.Axes.Flat( 0f, Lane( above, right ) ) : null, walk, height ) ) {
				walk = Break( walks, walk );
			}
		}

		Break( walks, walk );

		return walks;
	}

	// Last flight stops short of the slab so the ring can turn onto it.
	static float Reaches( ArchStairShape shape, ArchStairPart stair, ArchStairRun run, float height ) {
		if ( !run.Last ) {
			return run.Length;
		}

		foreach ( var well in shape.Levels ) {
			if ( well.Height > run.TopHeight + 1f ) {
				return run.Under( height ) - MathF.Max( 0f, stair.WellGap );
			}
		}

		return run.Length;
	}

	// Intermediate station at the last nosing prevents the rail sagging off the treads mid-flight.
	static void Climbing( List<ArchStairGuardNode> walk, ArchStairRun run, float lane, float from, float to, float height, StairRailing railing ) {
		var levels = run.Length - run.Going;

		walk.Add( Station( run, lane, from, height, railing ) );

		if ( levels > from + 0.5f && levels < to - 0.5f ) {
			walk.Add( Station( run, lane, levels, height, railing ) );
		}

		walk.Add( Station( run, lane, to, height, railing ) );
	}

	static ArchStairGuardNode Station( ArchStairRun run, float lane, float along, float height, StairRailing railing ) {
		return new ArchStairGuardNode {
			At = run.Axes.Flat( along, lane ),
			Foot = run.Rake( along ),
			Top = run.Nosing( along ) + height,
			Railing = railing
		};
	}

	static bool Guarded( ArchStairRun run, bool right ) {
		return right ? run.GuardRight && !run.WalledRight : run.GuardLeft && !run.WalledLeft;
	}

	static float Lane( ArchStairRun run, bool right ) {
		return right ? Centre( 0f, run.Width ) : Centre( run.Width, 0f );
	}

	static List<ArchStairGuardNode> Break( List<List<ArchStairGuardNode>> walks, List<ArchStairGuardNode> walk ) {
		if ( walk.Count > 1 ) {
			walks.Add( walk );
		}

		return new List<ArchStairGuardNode>();
	}

	static bool Across( ArchStairPad pad, Vector2 entry, Vector2? exit, List<ArchStairGuardNode> walk, float height ) {
		var inset = RailDepth * 0.5f;
		var corners = new List<Vector2>
		{
			pad.Axes.Flat( pad.AlongFrom, pad.AcrossFrom + inset ),
			pad.Axes.Flat( pad.AlongTo - inset, pad.AcrossFrom + inset ),
			pad.Axes.Flat( pad.AlongTo - inset, pad.AcrossTo - inset ),
			pad.Axes.Flat( pad.AlongFrom, pad.AcrossTo - inset )
		};

		var rails = new[] { pad.RailsFrom, pad.RailsHead, pad.RailsTo };
		var start = Nearest( corners, entry );
		var carried = walk.Count > 0 ? walk[^1].Railing : StairRailing.Balustrade;

		ArchStairGuardNode Corner( Vector2 at ) => new() {
			At = at,
			Foot = pad.Height,
			Top = pad.Height + height,
			Railing = carried
		};

		if ( start < 0 ) {
			return exit is not null;
		}

		if ( exit is not null ) {
			var end = Nearest( corners, exit.Value );

			// The walk picks up somewhere the landing's boundary does not reach: it crosses to it directly,
			// which is the inside of a switchback and nothing else.
			if ( end < 0 || end == start ) {
				return true;
			}

			var toward = end > start ? 1 : -1;

			for ( var index = start; index != end; index += toward ) {
				if ( !rails[toward > 0 ? index : index - 1] ) {
					return false;
				}
			}

			for ( var index = start + toward; index != end; index += toward ) {
				walk.Add( Corner( corners[index] ) );
			}

			return true;
		}

		// Nothing picks the walk up above: it runs on over every edge the landing owns and stops at the last.
		var away = start < 2 ? 1 : -1;

		for ( var index = start; index + away >= 0 && index + away < corners.Count; index += away ) {
			if ( !rails[away > 0 ? index : index - 1] ) {
				break;
			}

			walk.Add( Corner( corners[index + away] ) );
		}

		return false;
	}

	static int Nearest( IReadOnlyList<Vector2> corners, Vector2 point ) {
		var nearest = -1;
		var closest = RailDepth * 1.5f;

		for ( var index = 0; index < corners.Count; index++ ) {
			var reach = (corners[index] - point).Length;

			if ( reach < closest ) {
				closest = reach;
				nearest = index;
			}
		}

		return nearest;
	}

	static void Handrail( ArchMesh canvas, IReadOnlyList<ArchStairGuardNode> walk, ArchKit kit, ArchStairSkin skin ) {
		for ( var index = 0; index + 1 < walk.Count; index++ ) {
			Stretch( canvas, walk[index], walk[index + 1], kit, skin );
		}
	}

	static void Posting( IReadOnlyList<ArchStairGuardNode> walk, ArchKit kit, List<ArchStairGuardNode> posts ) {
		if ( walk.Count < 2 ) {
			return;
		}

		Post( walk[0], kit, posts );
		Post( walk[^1], kit, posts );

		for ( var index = 1; index + 1 < walk.Count; index++ ) {
			if ( Turns( walk, index ) ) {
				Post( walk[index], kit, posts );
			}
		}
	}

	static void Stretch( ArchMesh canvas, ArchStairGuardNode a, ArchStairGuardNode b, ArchKit kit, ArchStairSkin skin ) {
		var reach = (b.At - a.At).Length;

		if ( reach < 1f ) {
			return;
		}

		var bite = ArchLap.Bite( kit );
		var unit = (b.At - a.At) / reach;
		var axes = new ArchStairAxes { Origin = a.At, Yaw = MathF.Atan2( unit.y, unit.x ).RadianToDegree() };
		var capping = (b.Top - a.Top) / reach;

		Bar( canvas, axes, -bite, reach + bite, 0f, a.Top - capping * bite, b.Top + capping * bite, skin.Rail );
	}

	// Spacing is divided along the whole walk so balusters carry across stations.
	static void Infill( ArchMesh canvas, ArchStairPart stair, ArchKit kit, ArchStairSkin skin, IReadOnlyList<ArchStairGuardNode> walk, IReadOnlyList<ArchStairGuardNode> posts ) {
		var spacing = Spacing( stair, kit );
		var clear = MathF.Max( 1f, kit.NewelSize ) * 0.5f + BalusterSection;
		var bite = ArchLap.Bite( kit );
		var total = 0f;

		for ( var index = 0; index + 1 < walk.Count; index++ ) {
			total += (walk[index + 1].At - walk[index].At).Length;
		}

		if ( total < 1f ) {
			return;
		}

		foreach ( var offset in ArchDivide.AtLeast( total, spacing ).Inner ) {
			Stands( canvas, walk, offset, clear, bite, posts, skin );
		}
	}

	static void Stands( ArchMesh canvas, IReadOnlyList<ArchStairGuardNode> walk, float offset, float clear, float bite, IReadOnlyList<ArchStairGuardNode> posts, ArchStairSkin skin ) {
		var (index, along) = Seat( walk, offset );

		if ( index < 0 ) {
			return;
		}

		var a = walk[index];
		var b = walk[index + 1];

		if ( a.Railing != StairRailing.Balustrade ) {
			return;
		}

		var reach = (b.At - a.At).Length;
		var unit = (b.At - a.At) / reach;
		var at = a.At + unit * along;

		// A newel already fills that space, and a baluster inside one is a face nobody can see.
		if ( posts.Any( post => (post.At - at).Length < clear ) ) {
			return;
		}

		var seating = (b.Foot - a.Foot) / reach;
		var capping = (b.Top - a.Top) / reach;
		var axes = new ArchStairAxes { Origin = at, Yaw = MathF.Atan2( unit.y, unit.x ).RadianToDegree() };

		Baluster( canvas, axes, 0f, a.Foot + seating * along - bite, a.Top + capping * along + bite, skin, seating, capping );
	}

	internal static (int Index, float Along) Seat( IReadOnlyList<ArchStairGuardNode> walk, float offset ) {
		var along = offset;

		for ( var index = 0; index + 1 < walk.Count; index++ ) {
			var reach = (walk[index + 1].At - walk[index].At).Length;

			if ( reach < 0.01f ) {
				continue;
			}

			if ( along > reach ) {
				along -= reach;

				continue;
			}

			return (index, along);
		}

		return (-1, 0f);
	}

	// Posts only at bends sharper than PostAngle.
	static bool Turns( IReadOnlyList<ArchStairGuardNode> walk, int index ) {
		var before = walk[index].At - walk[index - 1].At;
		var after = walk[index + 1].At - walk[index].At;

		if ( before.Length < 0.5f || after.Length < 0.5f ) {
			return true;
		}

		if ( Vector2.Dot( before.Normal, after.Normal ) < MathF.Cos( PostAngle.DegreeToRadian() ) ) {
			return true;
		}

		var rising = (walk[index].Top - walk[index - 1].Top) / before.Length;
		var leaving = (walk[index + 1].Top - walk[index].Top) / after.Length;

		return MathF.Abs( leaving - rising ) > 0.05f;
	}

	// Deduplicates posts at shared stations across chains.
	static void Post( ArchStairGuardNode node, ArchKit kit, List<ArchStairGuardNode> posts ) {
		if ( posts.Any( post => (post.At - node.At).Length < MathF.Max( 1f, kit.NewelSize ) ) ) {
			return;
		}

		posts.Add( node );
	}

	static void Newels( ArchMesh canvas, ArchKit kit, ArchStairSkin skin, IReadOnlyList<ArchStairGuardNode> posts ) {
		foreach ( var post in posts ) {
			Newel( canvas, new ArchStairAxes { Origin = post.At }, 0f, 0f, post.Foot, post.Top - post.Foot, kit, skin );
		}
	}


	static void BorderWalk( List<List<ArchStairGuardNode>> walks, ArchStairRun run, ArchStairBorder border, ArchKit kit, float height ) {
		var post = MathF.Max( 1f, kit.NewelSize ) * 0.5f;
		var lane = border.Lane + border.Inward * RailDepth * 0.5f;
		var foot = MathF.Max( 0f, border.From ) + post;
		var head = MathF.Min( run.Length, border.To ) - post;

		if ( head - foot < 1f ) {
			return;
		}

		walks.Add( new List<ArchStairGuardNode>
		{
			Station( run, lane, foot, height, StairRailing.Balustrade ),
			Station( run, lane, head, height, StairRailing.Balustrade )
		} );
	}

	static void WallRails( ArchMesh canvas, ArchStairPart stair, ArchStairShape shape, ArchKit kit, ArchStairSkin skin, float height ) {
		foreach ( var run in shape.Runs ) {
			if ( run.HandrailRight ) {
				WallRail( canvas, run, WallRailGap + RailDepth * 0.5f, kit, skin, height );
			}

			if ( run.HandrailLeft ) {
				WallRail( canvas, run, run.Width - WallRailGap - RailDepth * 0.5f, kit, skin, height );
			}
		}
	}

	static void WallRail( ArchMesh canvas, ArchStairRun run, float lane, ArchKit kit, ArchStairSkin skin, float height ) {
		var from = 0f;
		var to = run.Last ? run.Under( height ) : run.Length;

		if ( to - from < 12f ) {
			return;
		}

		Bar( canvas, run.Axes, from, to, lane, run.Nosing( from ) + height, run.Nosing( to ) + height, skin.Rail );

		var bite = ArchLap.Bite( kit );
		var wall = lane < run.Width * 0.5f ? -bite : run.Width + bite;

		foreach ( var offset in ArchDivide.AtLeast( to - from, WallBracketSpacing ).Inner.DefaultIfEmpty( (to - from) * 0.5f ) ) {
			var along = from + offset;
			var seat = run.Nosing( along ) + height;

			Block( canvas, run.Axes, along - 0.8f, along + 0.8f,
				MathF.Min( wall, lane ), MathF.Max( wall, lane ), seat - 1.6f, seat + bite, skin.Rail );
		}
	}

	static List<List<ArchStairGuardNode>> WellRings( ArchStairPart stair, ArchStairShape shape, ArchKit kit, float height, IReadOnlyList<List<ArchStairGuardNode>> walks, ArchPlan plan, ArchBuilding building ) {
		var rings = new List<List<ArchStairGuardNode>>();

		if ( !stair.WellGuard ) {
			return rings;
		}

		foreach ( var well in shape.Levels ) {
			foreach ( var merged in ArchFootprint.Union( well.Loops.Concat( Opened( plan, building, kit, shape, well ) ) ) ) {
				// Seam at rake-crossing stations so each segment is judged independently.
				var loop = shape.Seamed( merged );

				// CCW loop: void left, floor right — ring stands on the floor side, inset from the edge.
				var standing = ArchFootprint.FloorSide( loop );
				var guarded = shape.GuardedEdges( loop, well.Probe, kit, well.Height );

				foreach ( var ring in Rings( loop, guarded, standing, well.Height, height ) ) {
					Turning( ring, walks );

					rings.Add( ring );
				}
			}
		}

		return rings;
	}

	// Union boolean cut holes that overlap the stairwell before walking the ring.
	static IEnumerable<List<Vector2>> Opened( ArchPlan plan, ArchBuilding building, ArchKit kit, ArchStairShape shape, ArchStairWell well ) {
		if ( plan is null || building is null ) {
			yield break;
		}

		// The landings standing ON this floor FILL what a cut took out under them, and a floored square is not a
		// drop. Without this the ring rails the cut's raw edge straight across the platform you walk off onto.
		var floored = shape.Pads
			.Where( pad => MathF.Abs( pad.Height - well.Height ) < 1f )
			.Select( pad => (IReadOnlyList<Vector2>)pad.Loop() )
			.ToList();

		foreach ( var loop in ArchCut.Guarded( plan, well.Level, kit, well.Height - kit.FloorThickness, well.Height, building.Id ) ) {
			if ( !well.Loops.Any( own => ArchFootprint.Overlaps( own, loop ) ) ) {
				continue;
			}

			foreach ( var open in ArchFootprint.Subtract( new[] { loop }, floored ) ) {
				yield return open;
			}
		}
	}

	static List<List<ArchStairGuardNode>> Rings( IReadOnlyList<Vector2> loop, IReadOnlyList<bool> guarded, float standing, float floor, float height ) {
		var rings = new List<List<ArchStairGuardNode>>();

		if ( !guarded.Any( edge => edge ) ) {
			return rings;
		}

		// Nothing open anywhere round it: the ring closes on itself, so it is walked from any vertex back to it.
		if ( guarded.All( edge => edge ) ) {
			var closed = new List<ArchStairGuardNode>();

			for ( var index = 0; index <= loop.Count; index++ ) {
				closed.Add( Standing( loop, index % loop.Count, true, true, standing, floor, height ) );
			}

			rings.Add( closed );

			return rings;
		}

		for ( var index = 0; index < loop.Count; index++ ) {
			if ( !guarded[index] || guarded[(index + loop.Count - 1) % loop.Count] ) {
				continue;
			}

			var ring = new List<ArchStairGuardNode> { Standing( loop, index, false, true, standing, floor, height ) };
			var edge = index;

			while ( guarded[edge] ) {
				var next = (edge + 1) % loop.Count;

				ring.Add( Standing( loop, next, true, guarded[next], standing, floor, height ) );
				edge = next;
			}

			rings.Add( ring );
		}

		return rings;
	}

	// Single-edge ends use their own normal, not the bisector.
	static ArchStairGuardNode Standing( IReadOnlyList<Vector2> loop, int index, bool arriving, bool leaving, float standing, float floor, float height ) {
		var before = Inward( loop[(index + loop.Count - 1) % loop.Count], loop[index], standing );
		var after = Inward( loop[index], loop[(index + 1) % loop.Count], standing );

		return new ArchStairGuardNode {
			At = loop[index] + (arriving && leaving ? Mitre( before, after ) : arriving ? before : after) * WellReturn,
			Foot = floor,
			Top = floor + height,
			Railing = StairRailing.Balustrade
		};
	}

	static Vector2 Mitre( Vector2 before, Vector2 after ) {
		var closing = 1f + Vector2.Dot( before, after );

		return closing < 0.01f ? before : (before + after) / closing;
	}

	static void Turning( List<ArchStairGuardNode> ring, IReadOnlyList<List<ArchStairGuardNode>> walks ) {
		if ( ring.Count > 1 && Ending( walks, ring[0], ring[1] ) is { } foot ) {
			ring.Insert( 0, Square( ring[0], ring[1], foot ) );
			ring.Insert( 0, foot );
		}

		if ( ring.Count > 1 && Ending( walks, ring[^1], ring[^2] ) is { } head ) {
			ring.Add( Square( ring[^1], ring[^2], head ) );
			ring.Add( head );
		}
	}

	internal static ArchStairGuardNode Square( ArchStairGuardNode end, ArchStairGuardNode inner, ArchStairGuardNode target ) {
		var span = end.At - inner.At;

		if ( span.Length < 0.5f ) {
			return end;
		}

		var unit = span.Normal;

		return end with { At = inner.At + unit * Vector2.Dot( target.At - inner.At, unit ) };
	}

	// Nearest walk end beside the ring's last edge — takes the closest to avoid crossing the mouth.
	static ArchStairGuardNode? Ending( IReadOnlyList<List<ArchStairGuardNode>> walks, ArchStairGuardNode end, ArchStairGuardNode inner ) {
		var span = end.At - inner.At;

		if ( span.Length < 0.5f ) {
			return null;
		}

		var unit = span.Normal;
		var across = new Vector2( -unit.y, unit.x );
		var closest = float.MaxValue;
		var nearest = (ArchStairGuardNode?)null;

		foreach ( var walk in walks ) {
			foreach ( var node in new[] { walk[0], walk[^1] } ) {
				var offset = node.At - inner.At;

				// Ahead of the ring, never behind it: a walk end back down the chain is one the ring already
				// stands beside, not one it turns onto.
				if ( MathF.Abs( node.Top - end.Top ) > 1f || Vector2.Dot( offset, unit ) < 0f ) {
					continue;
				}

				if ( MathF.Abs( Vector2.Dot( offset, across ) ) >= WellReturn * 3f ) {
					continue;
				}

				var reach = (node.At - end.At).Length;

				if ( reach >= closest ) {
					continue;
				}

				closest = reach;
				nearest = node;
			}
		}

		return nearest;
	}

	static Vector2 Inward( Vector2 a, Vector2 b, float sign ) {
		var unit = (b - a).Normal;

		return new Vector2( -unit.y, unit.x ) * sign;
	}

	static ArchStairPad PadBetween( ArchStairShape shape, ArchStairRun run ) {
		foreach ( var pad in shape.Pads ) {
			if ( MathF.Abs( pad.AlongFrom - run.Length ) < 2f && MathF.Abs( pad.Height - run.TopHeight ) < 2f ) {
				return pad;
			}
		}

		return null;
	}

	static float Centre( float lane, float opposite ) => lane + MathF.Sign( opposite - lane ) * RailDepth * 0.5f;

	static void Bar( ArchMesh canvas, ArchStairAxes axes, float from, float to, float lane, float low, float high, ArchBrush brush ) {
		if ( to - from < 1f ) {
			return;
		}

		var half = RailDepth * 0.5f;

		var lower = new List<Vector3>
		{
			axes.Point( from, lane - half, low ),
			axes.Point( to, lane - half, high ),
			axes.Point( to, lane + half, high ),
			axes.Point( from, lane + half, low )
		};

		var upper = new List<Vector3>();

		foreach ( var point in lower ) {
			upper.Add( point.WithZ( point.z + RailSection ) );
		}

		canvas.Prism( lower, upper, brush );
	}

	static void Newel( ArchMesh canvas, ArchStairAxes axes, float along, float lane, float floor, float height, ArchKit kit, ArchStairSkin skin ) {
		var half = MathF.Max( 1f, kit.NewelSize ) * 0.5f;
		var top = floor + height + MathF.Max( 0f, kit.NewelRise );

		Block( canvas, axes, along - half, along + half, lane - half, lane + half, floor - RailSection, top, skin.Rail );

		Block( canvas, axes, along - half - CapOversail, along + half + CapOversail,
			lane - half - CapOversail, lane + half + CapOversail, top, top + CapDepth, skin.Rail );
	}

	static void Baluster( ArchMesh canvas, ArchStairAxes axes, float along, float bottom, float top, ArchStairSkin skin, float seatSlope, float headSlope ) {
		if ( top - bottom < 4f ) {
			return;
		}

		var half = BalusterSection * 0.5f;

		var lower = new List<Vector3>
		{
			axes.Point( along - half, -half, bottom - seatSlope * half ),
			axes.Point( along + half, -half, bottom + seatSlope * half ),
			axes.Point( along + half, half, bottom + seatSlope * half ),
			axes.Point( along - half, half, bottom - seatSlope * half )
		};

		var upper = new List<Vector3>
		{
			axes.Point( along - half, -half, top - headSlope * half ),
			axes.Point( along + half, -half, top + headSlope * half ),
			axes.Point( along + half, half, top + headSlope * half ),
			axes.Point( along - half, half, top - headSlope * half )
		};

		canvas.Prism( lower, upper, skin.Rail );
	}
}