Editor utility for drawing architectural blueprint gizmos in the editor. It provides colours and many helper methods to draw footprints, volumes, walls, openings, posts, paths, roofs, tags and other visual aids using the Gizmo and TextRendering APIs so the editor previews the generator output without modifying the plan.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// The blueprint layer, drawn from the same numbers the generator is about to be handed - nothing here touches the plan.
public static class ArchGhost
{
public static readonly Color Line = new( 0.38f, 0.72f, 1f );
public static readonly Color Accent = new( 0.84f, 0.95f, 1f );
public static readonly Color Fill = new( 0.24f, 0.56f, 1f, 0.09f );
public static readonly Color Ground = new( 0.38f, 0.72f, 1f, 0.35f );
// One hue per box of a blueprint, cool to warm in drawing order, so overlapping boxes can be told
// apart and the square two of them share - the landing - reads as the seam between two colours.
static readonly Color[] Order =
{
new( 0.36f, 0.72f, 1f ),
new( 0.36f, 0.90f, 0.50f ),
new( 1f, 0.86f, 0.30f ),
new( 1f, 0.58f, 0.20f ),
new( 1f, 0.38f, 0.38f )
};
public static Color Nth( int index ) => Order[((index % Order.Length) + Order.Length) % Order.Length];
// A drawn box, flat on the floor: filled so it reads as an area, ringed bold so its edges are
// exact. Deliberately NOT a prism up the whole climb - a box the height of a storey hides the
// floor you are aiming the next one at, which is the one thing the blueprint is for.
public static void Footprint( IReadOnlyList<Vector2> loop, float height, Color color )
{
if ( loop is not { Count: >= 3 } )
{
return;
}
ArchFootprint.Bounds( loop, out var min, out var max );
Gizmo.Draw.Color = color.WithAlpha( 0.16f );
Gizmo.Draw.SolidBox( new BBox( new Vector3( min.x, min.y, height ), new Vector3( max.x, max.y, height + 0.25f ) ) );
Gizmo.Draw.LineThickness = 3f;
Gizmo.Draw.Color = color;
Ring( loop, height );
Gizmo.Draw.LineThickness = 2f;
Gizmo.Draw.Color = Line;
}
public static IDisposable Begin( string name = "arch-ghost" )
{
var scope = Gizmo.Scope( name );
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.LineThickness = 2f;
Gizmo.Draw.Color = Line;
return scope;
}
// The solid box reads as volume where lines read as a flat diagram; the corner ticks say which way is up.
public static void Volume( Vector2 min, Vector2 max, float bottom, float top )
{
if ( max.x - min.x < 0.5f || max.y - min.y < 0.5f )
{
return;
}
var box = new BBox( new Vector3( min.x, min.y, bottom ), new Vector3( max.x, max.y, MathF.Max( bottom + 0.5f, top ) ) );
Gizmo.Draw.Color = Fill;
Gizmo.Draw.SolidBox( box );
Gizmo.Draw.Color = Line;
Gizmo.Draw.LineBBox( box );
}
public static void Plate( Vector2 min, Vector2 max, float height, int hatch = 4 )
{
if ( max.x - min.x < 0.5f || max.y - min.y < 0.5f )
{
return;
}
Ring( ArchFootprint.Rect( min, max ), height );
Gizmo.Draw.Color = Ground;
for ( var index = 1; index < hatch; index++ )
{
var t = index / (float)hatch;
Gizmo.Draw.Line(
new Vector3( MathX.Lerp( min.x, max.x, t ), min.y, height ),
new Vector3( MathX.Lerp( min.x, max.x, t ), max.y, height ) );
}
Gizmo.Draw.Color = Line;
}
public static void Ring( IReadOnlyList<Vector2> loop, float height )
{
for ( var index = 0; index < loop.Count; index++ )
{
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
Gizmo.Draw.Line( new Vector3( from.x, from.y, height ), new Vector3( to.x, to.y, height ) );
}
}
public static void Prism( IReadOnlyList<Vector2> loop, float bottom, float top )
{
Solid( loop, bottom, ArchCarvePlane.Level( top ) );
}
// The top is a PLANE, so a wedge previews as the wedge it builds - drawn off the same answer the generator
// carves from rather than a second reading of the slope.
public static void Solid( IReadOnlyList<Vector2> loop, float bottom, ArchCarvePlane top )
{
Solid( loop, ArchCarvePlane.Level( bottom ), top );
}
// BOTH ends as planes, because a subtracted ramp rakes the floor it leaves rather than the top it stands.
public static void Solid( IReadOnlyList<Vector2> loop, ArchCarvePlane bottom, ArchCarvePlane top )
{
Raked( loop, bottom );
Raked( loop, top );
foreach ( var corner in loop )
{
Gizmo.Draw.Line(
new Vector3( corner.x, corner.y, bottom.At( corner ) ),
new Vector3( corner.x, corner.y, top.At( corner ) ) );
}
}
static void Raked( IReadOnlyList<Vector2> loop, ArchCarvePlane plane )
{
for ( var index = 0; index < loop.Count; index++ )
{
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
Gizmo.Draw.Line(
new Vector3( from.x, from.y, plane.At( from ) ),
new Vector3( to.x, to.y, plane.At( to ) ) );
}
}
public static void Wall( Vector2 start, Vector2 end, float bottom, float height, float thickness )
{
var reach = end - start;
if ( reach.Length < 0.5f )
{
return;
}
var side = new Vector2( -reach.y, reach.x ).Normal * thickness * 0.5f;
Prism( new List<Vector2> { start + side, end + side, end - side, start - side }, bottom, bottom + height );
}
// On the wall's own plane so sill and head read, with the cross that says hole, not block.
public static void Opening( Vector2 start, Vector2 end, float bottom, float sill, float height, float thickness )
{
var reach = end - start;
if ( reach.Length < 0.5f )
{
return;
}
var side = new Vector2( -reach.y, reach.x ).Normal * thickness * 0.5f;
foreach ( var face in new[] { side, -side } )
{
var a = new Vector3( start.x + face.x, start.y + face.y, bottom + sill );
var b = new Vector3( end.x + face.x, end.y + face.y, bottom + sill );
var c = new Vector3( end.x + face.x, end.y + face.y, bottom + sill + height );
var d = new Vector3( start.x + face.x, start.y + face.y, bottom + sill + height );
Gizmo.Draw.Line( a, b );
Gizmo.Draw.Line( b, c );
Gizmo.Draw.Line( c, d );
Gizmo.Draw.Line( d, a );
Gizmo.Draw.Color = Ground;
Gizmo.Draw.Line( a, c );
Gizmo.Draw.Line( b, d );
Gizmo.Draw.Color = Line;
}
}
public static void Post( Vector2 at, float size, float bottom, float top )
{
Volume( at - new Vector2( size * 0.5f, size * 0.5f ), at + new Vector2( size * 0.5f, size * 0.5f ), bottom, top );
}
public static void Path( IReadOnlyList<Vector3> path, bool closed )
{
var legs = closed ? path.Count : path.Count - 1;
for ( var index = 0; index < legs; index++ )
{
Gizmo.Draw.Line( path[index], path[(index + 1) % path.Count] );
}
}
public static void Curve( ArchCurve curve, float precision = 24f )
{
var frames = curve.Walk( precision );
for ( var index = 0; index < frames.Count - 1; index++ )
{
Gizmo.Draw.Line( frames[index].Position, frames[index + 1].Position );
}
if ( curve.Closed && frames.Count > 2 )
{
Gizmo.Draw.Line( frames[^1].Position, frames[0].Position );
}
}
// Both edges, so a drag shows the width, not only where the centre goes.
public static void Verges( ArchCurve curve, IReadOnlyList<ArchFrame> frames, float half )
{
for ( var index = 0; index < frames.Count - 1; index++ )
{
foreach ( var side in new[] { -half, half } )
{
Gizmo.Draw.Line( frames[index].Side( side ), frames[index + 1].Side( side ) );
}
}
}
// Drawn off the generator's own resolve, so the posts shown are the posts built.
public static void Barrier( ArchBarrierShape shape, ArchBarrierSpec spec )
{
foreach ( var post in shape.Posts )
{
Post( post.Flat, MathF.Max( 2f, spec.PostSize ), post.Foot, post.Seat + spec.Height + spec.PostOverhang );
}
foreach ( var bay in shape.Bays )
{
if ( bay.Gate )
{
continue;
}
// The bay's own resolved sag, so a slung line previews as the curve the generator is about to build.
Path( bay.Strand( bay.SeatFrom + spec.Height, bay.SeatTo + spec.Height ), false );
}
}
// Head line and open foot, off the same resolve the wall is built from: a bay with no fill to hold draws nothing.
public static void Retaining( ArchRetainingShape shape )
{
if ( shape is null || !shape.IsUsable )
{
return;
}
foreach ( var bay in shape.Bays )
{
Gizmo.Draw.Line( Corner( bay.From, bay.From.Head ), Corner( bay.To, bay.To.Head ) );
Gizmo.Draw.Color = Ground;
Gizmo.Draw.Line( Corner( bay.From, bay.From.Grade ), Corner( bay.To, bay.To.Grade ) );
Gizmo.Draw.Color = Line;
Gizmo.Draw.Line( Corner( bay.From, bay.From.Grade ), Corner( bay.From, bay.From.Head ) );
}
Gizmo.Draw.Color = Ground;
foreach ( var wing in shape.Returns )
{
Gizmo.Draw.Line( new Vector3( wing.From.x, wing.From.y, wing.Head ), new Vector3( wing.To.x, wing.To.y, wing.Head ) );
}
Gizmo.Draw.Color = Line;
}
static Vector3 Corner( ArchRetainingStation station, float height ) => new( station.At.x, station.At.y, height );
// The ridge or the fall line, so a roof says which way the water runs before it exists.
public static void Pitch( Vector2 min, Vector2 max, float eave, float pitch, RoofStyle style, bool alongX )
{
Ring( ArchFootprint.Rect( min, max ), eave );
var size = max - min;
var centre = (min + max) * 0.5f;
var span = (alongX ? size.y : size.x) * 0.5f;
var apex = eave + ArchPitch.Slope( pitch ) * span;
switch ( style )
{
case RoofStyle.Flat:
return;
case RoofStyle.Shed:
Gizmo.Draw.Color = Accent;
Gizmo.Draw.Line( new Vector3( min.x, min.y, eave ), new Vector3( min.x, max.y, eave + ArchPitch.Slope( pitch ) * size.y ) );
Gizmo.Draw.Line( new Vector3( max.x, min.y, eave ), new Vector3( max.x, max.y, eave + ArchPitch.Slope( pitch ) * size.y ) );
break;
default:
Gizmo.Draw.Color = Accent;
var ridgeFrom = alongX ? new Vector2( min.x, centre.y ) : new Vector2( centre.x, min.y );
var ridgeTo = alongX ? new Vector2( max.x, centre.y ) : new Vector2( centre.x, max.y );
if ( style == RoofStyle.Hip )
{
var inset = MathF.Min( span, (alongX ? size.x : size.y) * 0.5f );
var direction = (ridgeTo - ridgeFrom).Normal;
ridgeFrom += direction * inset;
ridgeTo -= direction * inset;
}
Gizmo.Draw.Line( new Vector3( ridgeFrom.x, ridgeFrom.y, apex ), new Vector3( ridgeTo.x, ridgeTo.y, apex ) );
foreach ( var corner in ArchFootprint.Rect( min, max ) )
{
var near = (corner - ridgeFrom).Length < (corner - ridgeTo).Length ? ridgeFrom : ridgeTo;
Gizmo.Draw.Line( new Vector3( corner.x, corner.y, eave ), new Vector3( near.x, near.y, apex ) );
}
break;
}
Gizmo.Draw.Color = Line;
}
public static void Note( Vector3 at, string text )
{
Gizmo.Draw.Color = Accent;
Gizmo.Draw.WorldText( text, new Transform( at, Rotation.Identity ), "Roboto", 11f );
Gizmo.Draw.Color = Line;
}
public static void Brackets( Vector2 min, Vector2 max, float height )
{
var centre = (min + max) * 0.5f;
var arm = MathF.Max( 8f, MathF.Min( max.x - min.x, max.y - min.y ) * 0.18f );
foreach ( var corner in ArchFootprint.Rect( min, max ) )
{
var inward = new Vector2( corner.x < centre.x ? arm : -arm, corner.y < centre.y ? arm : -arm );
Gizmo.Draw.Line( new Vector3( corner.x, corner.y, height ), new Vector3( corner.x + inward.x, corner.y, height ) );
Gizmo.Draw.Line( new Vector3( corner.x, corner.y, height ), new Vector3( corner.x, corner.y + inward.y, height ) );
}
}
// Screen space - world text lies in a plane that is edge-on in plan and both elevations.
public static void Tag( Vector3 at, string text, Color color )
{
var scope = new TextRendering.Scope( text, color, 13f, "Roboto", 600 )
{
Outline = new TextRendering.Outline
{
Enabled = true,
Size = 3,
Color = Color.Black.WithAlpha( 0.85f )
}
};
Gizmo.Draw.ScreenText( scope, at, new Vector2( 10f, -6f ) );
}
// A post where the next gesture begins, at the section and the rise of what it is about to stand. A flat
// cross is edge-on from every angle a building is actually drawn from, so it says where on the grid the
// cursor is and nothing about which surface it came to rest on.
// The ring is drawn at the REACH, not at the post: the one thing an author cannot see about a snap is how far
// it grabs, and a halo the size of the post says only "something happened".
public static void Marker( Vector2 at, float size, float bottom, float top, ArchSnapKind snap = ArchSnapKind.None, float reach = 0f )
{
var section = MathF.Max( 2f, size );
Cursor( at, bottom, section * 4f );
if ( snap != ArchSnapKind.None && reach > 0.01f )
{
var bite = new Vector2( reach, reach );
Gizmo.Draw.Color = Accent;
Ring( ArchFootprint.Rect( at - bite, at + bite ), bottom );
}
Post( at, section, bottom, MathF.Max( bottom + section, top ) );
Gizmo.Draw.Color = Line;
}
// Without it a click tool gives no feedback until something has already been placed.
public static void Cursor( Vector2 point, float height, float grid )
{
var cell = MathF.Max( 4f, grid ) * 0.5f;
Gizmo.Draw.Color = Ground;
Gizmo.Draw.Line( new Vector3( point.x - cell, point.y, height ), new Vector3( point.x + cell, point.y, height ) );
Gizmo.Draw.Line( new Vector3( point.x, point.y - cell, height ), new Vector3( point.x, point.y + cell, height ) );
Gizmo.Draw.Color = Line;
}
}