Editor/InteriorLayoutBuilder/RoomLayoutTool.Drawing.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace ReusableRoomLayout;
public sealed partial class RoomLayoutTool
{
private const TextFlag LayoutLabelFlags = TextFlag.Center | TextFlag.DontClip;
private void DrawLayout()
{
using ( Gizmo.Scope( "Interior Layout Builder" ) )
{
Gizmo.Draw.IgnoreDepth = true;
foreach ( var corridor in document.Corridors.Where( IsActiveFloor ) )
{
DrawCorridor( corridor );
}
if ( drawingCorridor )
{
DrawCorridorDraft();
}
foreach ( var room in document.Rooms.Where( IsActiveFloor ) )
{
DrawRoom( room, room.Id == selectedRoomId );
}
foreach ( var cutout in document.FloorCutouts.Where( IsActiveFloor ) )
{
DrawFloorCutout( cutout, cutout.Id == selectedFloorCutoutId );
}
foreach ( var door in document.Doors.Where( IsActiveFloor ) )
{
DrawDoor( door, door.Id == selectedDoorId );
}
foreach ( var window in document.Windows.Where( IsActiveFloor ) )
{
DrawWindow( window, window.Id == selectedWindowId );
}
if ( drawingRoom )
{
DrawRect( SnapRect( RoomLayoutRect.FromPoints( dragStart, dragCurrent ) ), Color.Green );
}
if ( drawingFloorCutout )
{
DrawRect( SnapRect( RoomLayoutRect.FromPoints( dragStart, dragCurrent ) ), Color.Red, 3.0f );
}
}
}
private void DrawRoom( RoomLayoutRoom room, bool selected )
{
DrawRect( room.Bounds, selected ? Color.Cyan : Color.White );
DrawLayoutLabel( room.Name, room.Bounds.Center, selected ? Color.Cyan : Color.White );
}
private void DrawFloorCutout( RoomLayoutFloorCutout cutout, bool selected )
{
var color = selected ? Color.Orange : Color.Red;
DrawRect( cutout.Bounds, color, selected ? 4.0f : 2.5f );
DrawCutoutCross( cutout.Bounds, color );
DrawLayoutLabel(
string.IsNullOrWhiteSpace( cutout.Name ) ? $"Floor Cutout {cutout.Id}" : cutout.Name,
cutout.Bounds.Center,
color );
}
private void DrawCutoutCross( RoomLayoutRect rect, Color color )
{
var z = ActiveFloorZ() + 4.0f;
Gizmo.Draw.Color = color.WithAlpha( 0.8f );
Gizmo.Draw.LineThickness = 1.5f;
Gizmo.Draw.Line(
new Vector3( rect.X, rect.Y, z ),
new Vector3( rect.X + rect.Width, rect.Y + rect.Height, z ) );
Gizmo.Draw.Line(
new Vector3( rect.X + rect.Width, rect.Y, z ),
new Vector3( rect.X, rect.Y + rect.Height, z ) );
Gizmo.Draw.LineThickness = 1.0f;
}
private void DrawLayoutLabel( string text, Vector2 position, Color color )
{
var textScope = new TextRendering.Scope
{
Text = text,
TextColor = color,
FontName = "Roboto",
FontSize = 16.0f,
Outline = new TextRendering.Outline { Enabled = true, Color = Color.Black, Size = 2.0f }
};
Gizmo.Draw.ScreenText( textScope, new Vector3( position.x, position.y, ActiveFloorZ() + 8.0f ), Vector2.Zero, LayoutLabelFlags );
}
private void DrawDoor( RoomLayoutDoor door, bool selected )
{
if ( !TryGetDoorFootprint( door, out var footprint ) )
{
return;
}
DrawRect( footprint, selected ? Color.Orange : Color.Yellow, selected ? 4.0f : 2.5f );
}
private void DrawWindow( RoomLayoutWindow window, bool selected )
{
if ( !TryGetWindowFootprint( window, out var footprint ) )
{
return;
}
DrawRect( footprint, selected ? Color.Orange : Color.Magenta, selected ? 4.0f : 2.5f );
}
private void DrawCorridor( RoomLayoutCorridor corridor )
{
if ( !TryGetCorridorPath( corridor, out var points ) )
{
return;
}
var selected = corridor.Id == selectedCorridorId;
var width = CorridorClearWidth( corridor );
var boundsColor = selected ? Color.Orange : Color.Blue.WithAlpha( 0.75f );
Gizmo.Draw.LineThickness = selected ? 3.0f : 2.0f;
if ( document.Settings.ThresholdsEnabled )
{
DrawCorridorThresholds( corridor, width, boundsColor );
}
for ( var i = 0; i < points.Count - 1; i++ )
{
DrawCorridorSegmentBounds( points[i], points[i + 1], width, boundsColor );
}
DrawCorridorJoinBounds( points, width, boundsColor );
Gizmo.Draw.Color = selected ? Color.Orange.WithAlpha( 0.9f ) : Color.Blue.WithAlpha( 0.55f );
Gizmo.Draw.LineThickness = selected ? 4.0f : 2.0f;
for ( var i = 0; i < points.Count - 1; i++ )
{
Gizmo.Draw.Line(
new Vector3( points[i].x, points[i].y, ActiveFloorZ() + 5.0f ),
new Vector3( points[i + 1].x, points[i + 1].y, ActiveFloorZ() + 5.0f ) );
}
Gizmo.Draw.LineThickness = 1.0f;
DrawLayoutLabel( $"Corridor {corridor.Id}", CorridorLabelPosition( points ), selected ? Color.Orange : Color.White );
}
private static Vector2 CorridorLabelPosition( IReadOnlyList<Vector2> points )
{
var bestLength = 0.0f;
var bestPosition = points.Count > 0 ? points[0] : Vector2.Zero;
for ( var i = 0; i < points.Count - 1; i++ )
{
var a = points[i];
var b = points[i + 1];
var length = (b - a).Length;
if ( length <= bestLength )
{
continue;
}
bestLength = length;
bestPosition = (a + b) * 0.5f;
}
return bestPosition;
}
private void DrawCorridorDraft()
{
if ( !TryGetDoorAnchor( selectedDoorId, out var start ) )
{
return;
}
var width = CorridorDraftWidth();
var points = new List<Vector2>();
AddCorridorPoint( points, CorridorPortalPoint( start ) );
AddCorridorPoint( points, CorridorApproachPoint( start, width ) );
foreach ( var bend in corridorDraftBendPoints )
{
AddCorridorPoint( points, bend.Vector );
}
AddCorridorPoint( points, corridorDraftCurrent );
SimplifyCorridorPath( points );
var color = Color.Green.WithAlpha( 0.85f );
Gizmo.Draw.LineThickness = 2.0f;
if ( document.Settings.ThresholdsEnabled )
{
DrawCorridorThreshold( start, width, color );
}
for ( var i = 0; i < points.Count - 1; i++ )
{
DrawCorridorSegmentBounds( points[i], points[i + 1], width, color );
}
DrawCorridorJoinBounds( points, width, color );
Gizmo.Draw.Color = color;
Gizmo.Draw.LineThickness = 4.0f;
for ( var i = 0; i < points.Count - 1; i++ )
{
Gizmo.Draw.Line(
new Vector3( points[i].x, points[i].y, ActiveFloorZ() + 6.0f ),
new Vector3( points[i + 1].x, points[i + 1].y, ActiveFloorZ() + 6.0f ) );
}
Gizmo.Draw.LineThickness = 1.0f;
}
private void DrawCorridorThresholds( RoomLayoutCorridor corridor, float width, Color color )
{
if ( TryGetDoorAnchor( corridor.StartDoorId, out var start ) )
{
DrawCorridorThreshold( start, width, color );
}
if ( TryGetDoorAnchor( corridor.EndDoorId, out var end ) )
{
DrawCorridorThreshold( end, width, color );
}
}
private void DrawCorridorThreshold( RoomLayoutDoorAnchor anchor, float width, Color color )
{
var center = anchor.Point + anchor.Normal * (document.Settings.WallThickness * 0.5f);
var verticalWall = MathF.Abs( anchor.Normal.y ) > MathF.Abs( anchor.Normal.x );
var thresholdDepth = Math.Clamp( document.Settings.ThresholdDepth, 0.5f, 512.0f );
var rect = verticalWall
? new RoomLayoutRect( center.x - width * 0.5f, center.y - thresholdDepth * 0.5f, width, thresholdDepth )
: new RoomLayoutRect( center.x - thresholdDepth * 0.5f, center.y - width * 0.5f, thresholdDepth, width );
DrawRect( rect, color );
}
private void DrawCorridorJoinBounds( IReadOnlyList<Vector2> points, float width, Color color )
{
for ( var i = 1; i < points.Count - 1; i++ )
{
DrawRect( new RoomLayoutRect( points[i].x - width * 0.5f, points[i].y - width * 0.5f, width, width ), color );
}
}
private void DrawCorridorSegmentBounds( Vector2 a, Vector2 b, float width, Color color )
{
var delta = b - a;
if ( delta.Length < 1.0f )
{
return;
}
var horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
if ( horizontal )
{
var minX = MathF.Min( a.x, b.x );
DrawRect( new RoomLayoutRect( minX, a.y - width * 0.5f, MathF.Abs( delta.x ), width ), color );
return;
}
var minY = MathF.Min( a.y, b.y );
DrawRect( new RoomLayoutRect( a.x - width * 0.5f, minY, width, MathF.Abs( delta.y ) ), color );
}
private void DrawSelectedRoomBounds()
{
if ( mode is not (RoomLayoutToolMode.Rooms or RoomLayoutToolMode.Select) ||
drawingRoom ||
movingRoom ||
selectedRoomId == 0 ||
document.FindRoom( selectedRoomId ) is not { } room )
{
return;
}
var bounds = room.Bounds;
var box = new BBox(
new Vector3( bounds.X, bounds.Y, ActiveFloorZ() ),
new Vector3( bounds.X + bounds.Width, bounds.Y + bounds.Height, ActiveFloorZ() + document.Settings.WallHeight ) );
using ( Gizmo.Scope( $"Room {room.Id} Resize" ) )
{
Gizmo.Hitbox.DepthBias = 0.01f;
if ( !Gizmo.Pressed.Any )
{
roomResizeStartBox = box;
roomResizeDeltaBox = default;
}
if ( Gizmo.Control.BoundingBox( $"Room {room.Id} Bounds", box, out var resized ) )
{
BeginLayoutEdit();
resizingRoom = true;
roomResizeDeltaBox.Mins += resized.Mins - box.Mins;
roomResizeDeltaBox.Maxs += resized.Maxs - box.Maxs;
var snapped = SnapGizmoBox( roomResizeStartBox, roomResizeDeltaBox );
var next = SnapRect( new RoomLayoutRect( snapped.Mins.x, snapped.Mins.y, snapped.Size.x, snapped.Size.y ) );
if ( next.Width >= document.Settings.GridSize && next.Height >= document.Settings.GridSize )
{
room.Bounds = next;
}
}
else if ( resizingRoom && !Gizmo.IsLeftMouseDown )
{
resizingRoom = false;
CommitLayoutEdit();
}
}
}
private void DrawSelectedCorridorBounds()
{
if ( mode is not (RoomLayoutToolMode.Corridors or RoomLayoutToolMode.Select) ||
selectedCorridorId == 0 ||
SelectedCorridor() is not { } corridor )
{
return;
}
if ( !TryGetCorridorFootprint( corridor, out var bounds, out var horizontal ) )
{
return;
}
var box = new BBox(
new Vector3( bounds.X, bounds.Y, ActiveFloorZ() ),
new Vector3( bounds.X + bounds.Width, bounds.Y + bounds.Height, ActiveFloorZ() + document.Settings.WallHeight ) );
using ( Gizmo.Scope( $"Corridor {corridor.Id} Width" ) )
{
Gizmo.Hitbox.DepthBias = 0.01f;
if ( !Gizmo.Pressed.Any )
{
corridorResizeStartBox = box;
corridorResizeDeltaBox = default;
}
if ( Gizmo.Control.BoundingBox( $"Corridor {corridor.Id} Width Bounds", box, out var resized ) )
{
BeginLayoutEdit();
resizingCorridor = true;
corridorResizeDeltaBox.Mins += resized.Mins - box.Mins;
corridorResizeDeltaBox.Maxs += resized.Maxs - box.Maxs;
var snapped = SnapGizmoBox( corridorResizeStartBox, corridorResizeDeltaBox );
var resizedWidth = horizontal ? snapped.Size.y : snapped.Size.x;
SetCorridorWidth( corridor, resizedWidth );
}
else if ( resizingCorridor && !Gizmo.IsLeftMouseDown )
{
resizingCorridor = false;
CommitLayoutEdit();
}
}
}
private void DrawSelectedFloorCutoutBounds()
{
if ( mode is not (RoomLayoutToolMode.FloorCutouts or RoomLayoutToolMode.Select) ||
drawingFloorCutout ||
movingFloorCutout ||
selectedFloorCutoutId == 0 ||
SelectedFloorCutout() is not { } cutout )
{
return;
}
var bounds = cutout.Bounds;
var editHeight = MathF.Max( 16.0f, document.Settings.FloorThickness );
var box = new BBox(
new Vector3( bounds.X, bounds.Y, ActiveFloorZ() ),
new Vector3( bounds.X + bounds.Width, bounds.Y + bounds.Height, ActiveFloorZ() + editHeight ) );
using ( Gizmo.Scope( $"Floor Cutout {cutout.Id} Resize" ) )
{
Gizmo.Hitbox.DepthBias = 0.01f;
if ( !Gizmo.Pressed.Any )
{
floorCutoutResizeStartBox = box;
floorCutoutResizeDeltaBox = default;
}
if ( Gizmo.Control.BoundingBox( $"Floor Cutout {cutout.Id} Bounds", box, out var resized ) )
{
BeginLayoutEdit();
resizingFloorCutout = true;
floorCutoutResizeDeltaBox.Mins += resized.Mins - box.Mins;
floorCutoutResizeDeltaBox.Maxs += resized.Maxs - box.Maxs;
var snapped = SnapGizmoBox( floorCutoutResizeStartBox, floorCutoutResizeDeltaBox );
var next = SnapRect( new RoomLayoutRect( snapped.Mins.x, snapped.Mins.y, snapped.Size.x, snapped.Size.y ) );
if ( next.Width >= document.Settings.GridSize && next.Height >= document.Settings.GridSize )
{
cutout.Bounds = next;
}
}
else if ( resizingFloorCutout && !Gizmo.IsLeftMouseDown )
{
resizingFloorCutout = false;
CommitLayoutEdit();
}
}
}
private bool TryGetCorridorFootprint( RoomLayoutCorridor corridor, out RoomLayoutRect bounds, out bool horizontal )
{
bounds = default;
horizontal = true;
if ( !TryGetCorridorPath( corridor, out var points ) || points.Count < 2 )
{
return false;
}
var width = CorridorClearWidth( corridor );
var hasBounds = false;
var longestSegment = 0.0f;
for ( var i = 0; i < points.Count - 1; i++ )
{
var a = points[i];
var b = points[i + 1];
var delta = b - a;
if ( delta.Length < 1.0f )
{
continue;
}
var segmentHorizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
var segmentLength = segmentHorizontal ? MathF.Abs( delta.x ) : MathF.Abs( delta.y );
var rect = segmentHorizontal
? new RoomLayoutRect( MathF.Min( a.x, b.x ), a.y - width * 0.5f, segmentLength, width )
: new RoomLayoutRect( a.x - width * 0.5f, MathF.Min( a.y, b.y ), width, segmentLength );
bounds = hasBounds ? UnionRect( bounds, rect ) : rect;
hasBounds = true;
if ( segmentLength > longestSegment )
{
longestSegment = segmentLength;
horizontal = segmentHorizontal;
}
}
for ( var i = 1; i < points.Count - 1; i++ )
{
var join = new RoomLayoutRect( points[i].x - width * 0.5f, points[i].y - width * 0.5f, width, width );
bounds = hasBounds ? UnionRect( bounds, join ) : join;
hasBounds = true;
}
return hasBounds;
}
private static RoomLayoutRect UnionRect( RoomLayoutRect a, RoomLayoutRect b )
{
var minX = MathF.Min( a.X, b.X );
var minY = MathF.Min( a.Y, b.Y );
var maxX = MathF.Max( a.X + a.Width, b.X + b.Width );
var maxY = MathF.Max( a.Y + a.Height, b.Y + b.Height );
return new RoomLayoutRect( minX, minY, maxX - minX, maxY - minY );
}
private void DrawRect( RoomLayoutRect rect, Color color, float lineThickness = 2.0f )
{
var z = ActiveFloorZ() + 4.0f;
var a = new Vector3( rect.X, rect.Y, z );
var b = new Vector3( rect.X + rect.Width, rect.Y, z );
var c = new Vector3( rect.X + rect.Width, rect.Y + rect.Height, z );
var d = new Vector3( rect.X, rect.Y + rect.Height, z );
Gizmo.Draw.Color = color;
Gizmo.Draw.LineThickness = lineThickness;
Gizmo.Draw.Line( a, b );
Gizmo.Draw.Line( b, c );
Gizmo.Draw.Line( c, d );
Gizmo.Draw.Line( d, a );
Gizmo.Draw.LineThickness = 1.0f;
}
}