Editor/InteriorLayoutBuilder/RoomLayoutTool.Selection.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace ReusableRoomLayout;
public sealed partial class RoomLayoutTool
{
private bool TryGetCursorPoint( out Vector2 point )
{
return TryGetCursorPoint( out point, out _ );
}
private bool TryGetCursorPoint( out Vector2 point, out bool hitGeneratedLayout )
{
return TryGetCursorPoint( out point, out hitGeneratedLayout, out _ );
}
private bool TryGetCursorPoint( out Vector2 point, out bool hitGeneratedLayout, out GameObject hitGameObject )
{
point = default;
hitGeneratedLayout = false;
hitGameObject = null;
var trace = Trace.UseRenderMeshes( true ).UsePhysicsWorld( false ).Run();
if ( trace.Hit && trace.GameObject.IsValid() && IsGeneratedLayoutObject( trace.GameObject ) )
{
hitGeneratedLayout = true;
hitGameObject = trace.GameObject;
point = SnapCursorPoint( trace.HitPosition );
return true;
}
var plane = new Plane( Vector3.Up, ActiveFloorZ() );
if ( !plane.TryTrace( Gizmo.CurrentRay, out var hit, true ) )
{
return false;
}
point = SnapCursorPoint( hit );
return true;
}
private bool TryFindRoom( Vector2 point, out RoomLayoutRoom room )
{
room = document.Rooms.LastOrDefault( candidate => IsActiveFloor( candidate ) && candidate.Bounds.Contains( point ) );
return room is not null;
}
private bool TryFindRoomWall( Vector2 point, out RoomLayoutRoom room )
{
return TryFindNearestWall( point, out room, out _, out _ );
}
private bool TryFindNearestWall( Vector2 point, out RoomLayoutRoom room, out RoomLayoutWallSide side, out float offset )
{
RoomLayoutRoom bestRoom = null;
var bestSide = default( RoomLayoutWallSide );
var bestOffset = 0.0f;
var bestDistance = MathF.Max( 16.0f, document.Settings.GridSize * 0.35f );
foreach ( var candidate in document.Rooms.Where( IsActiveFloor ) )
{
var bounds = candidate.Bounds;
CheckWall( candidate, RoomLayoutWallSide.North, MathF.Abs( point.y - (bounds.Y + bounds.Height) ), point.x - bounds.X, bounds.Width );
CheckWall( candidate, RoomLayoutWallSide.South, MathF.Abs( point.y - bounds.Y ), point.x - bounds.X, bounds.Width );
CheckWall( candidate, RoomLayoutWallSide.East, MathF.Abs( point.x - (bounds.X + bounds.Width) ), point.y - bounds.Y, bounds.Height );
CheckWall( candidate, RoomLayoutWallSide.West, MathF.Abs( point.x - bounds.X ), point.y - bounds.Y, bounds.Height );
}
room = bestRoom;
side = bestSide;
offset = bestOffset;
return room is not null;
void CheckWall( RoomLayoutRoom candidate, RoomLayoutWallSide candidateSide, float distance, float candidateOffset, float length )
{
if ( distance > bestDistance || candidateOffset < 0.0f || candidateOffset > length )
{
return;
}
bestDistance = distance;
bestRoom = candidate;
bestSide = candidateSide;
bestOffset = candidateOffset;
}
}
private bool TryFindNearestCorridorWall(
Vector2 point,
out RoomLayoutCorridor corridor,
out int segmentIndex,
out int side,
out float offset,
out float length )
{
corridor = null;
segmentIndex = 0;
side = 0;
offset = 0.0f;
length = 0.0f;
var bestDistance = MathF.Max( 16.0f, document.Settings.GridSize * 0.35f );
foreach ( var candidate in document.Corridors.Where( IsActiveFloor ) )
{
if ( !TryGetCorridorPath( candidate, out var points ) )
{
continue;
}
var width = CorridorClearWidth( candidate );
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 horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
var minAlong = horizontal ? MathF.Min( a.x, b.x ) : MathF.Min( a.y, b.y );
var maxAlong = horizontal ? MathF.Max( a.x, b.x ) : MathF.Max( a.y, b.y );
var candidateLength = maxAlong - minAlong;
var along = horizontal ? point.x : point.y;
for ( var candidateSide = -1; candidateSide <= 1; candidateSide += 2 )
{
var fixedCoordinate = (horizontal ? a.y : a.x) + candidateSide * width * 0.5f;
var distance = MathF.Abs( (horizontal ? point.y : point.x) - fixedCoordinate );
if ( distance > bestDistance || along < minAlong || along > maxAlong )
{
continue;
}
bestDistance = distance;
corridor = candidate;
segmentIndex = i;
side = candidateSide;
offset = along - minAlong;
length = candidateLength;
}
}
}
return corridor is not null;
}
private bool TryFindNearestDoor( Vector2 point, out RoomLayoutDoor door, float maxDistance, bool roomOnly = false )
{
door = null;
var bestDistance = maxDistance;
foreach ( var candidate in document.Doors.Where( IsActiveFloor ) )
{
if ( roomOnly && candidate.RoomId == 0 )
{
continue;
}
if ( !TryGetDoorFootprint( candidate, out var footprint ) )
{
continue;
}
var distance = DistanceToRect( point, footprint );
if ( distance > bestDistance )
{
continue;
}
bestDistance = distance;
door = candidate;
}
return door is not null;
}
private bool TryFindNearestWindow( Vector2 point, out RoomLayoutWindow window, float maxDistance )
{
window = null;
var bestDistance = maxDistance;
foreach ( var candidate in document.Windows.Where( IsActiveFloor ) )
{
if ( !TryGetWindowFootprint( candidate, out var footprint ) )
{
continue;
}
var distance = DistanceToRect( point, footprint );
if ( distance > bestDistance )
{
continue;
}
bestDistance = distance;
window = candidate;
}
return window is not null;
}
private bool TryFindWindowUnderCursor( out RoomLayoutWindow window )
{
window = null;
var padding = MathF.Max( 4.0f, document.Settings.GridSize * 0.05f );
for ( var i = document.Windows.Count - 1; i >= 0; i-- )
{
var candidate = document.Windows[i];
if ( !IsActiveFloor( candidate ) )
{
continue;
}
if ( !TryGetWindowSegment( candidate, out var a, out var b ) )
{
continue;
}
var sillHeight = EffectiveWindowSillHeight( candidate );
var openingHeight = EffectiveWindowHeight( candidate, sillHeight );
var windowTop = MathF.Min( document.Settings.WallHeight, sillHeight + openingHeight );
var horizontal = MathF.Abs( b.x - a.x ) >= MathF.Abs( b.y - a.y );
var planeNormal = horizontal
? new Vector3( 0.0f, 1.0f, 0.0f )
: new Vector3( 1.0f, 0.0f, 0.0f );
var planeDistance = horizontal ? a.y : a.x;
var plane = new Plane( planeNormal, planeDistance );
if ( !plane.TryTrace( Gizmo.CurrentRay, out var hit, true ) )
{
continue;
}
var along = horizontal ? hit.x : hit.y;
var minAlong = horizontal ? MathF.Min( a.x, b.x ) : MathF.Min( a.y, b.y );
var maxAlong = horizontal ? MathF.Max( a.x, b.x ) : MathF.Max( a.y, b.y );
var floorZ = FloorWorldZ( document.FloorFor( candidate ) );
if ( along < minAlong - padding ||
along > maxAlong + padding ||
hit.z < floorZ + sillHeight - padding ||
hit.z > floorZ + windowTop + padding )
{
continue;
}
window = candidate;
return true;
}
return false;
}
private bool TrySelectGeneratedLayoutObject( GameObject gameObject )
{
for ( var current = gameObject; current.IsValid(); current = current.Parent )
{
var name = current.Name ?? "";
if ( TryReadGeneratedLayoutId( name, "Window", out var windowId ) &&
document.FindWindow( windowId ) is { } window )
{
SelectWindow( window );
SetActiveFloorFromSelection();
return true;
}
if ( TryReadGeneratedLayoutId( name, "Door", out var doorId ) &&
document.FindDoor( doorId ) is { } door )
{
SelectDoor( door );
SetActiveFloorFromSelection();
return true;
}
if ( TryReadGeneratedLayoutId( name, "Corridor", out var corridorId ) &&
document.Corridors.FirstOrDefault( corridor => corridor.Id == corridorId ) is { } corridor )
{
SelectCorridor( corridor );
SetActiveFloorFromSelection();
return true;
}
}
return false;
}
private static bool TryReadGeneratedLayoutId( string name, string marker, out int id )
{
id = 0;
var index = name.IndexOf( marker, StringComparison.OrdinalIgnoreCase );
if ( index < 0 )
{
return false;
}
index += marker.Length;
while ( index < name.Length && char.IsWhiteSpace( name[index] ) )
{
index++;
}
var start = index;
while ( index < name.Length && char.IsDigit( name[index] ) )
{
index++;
}
return index > start && int.TryParse( name[start..index], out id );
}
private bool TryFindNearestCorridor( Vector2 point, out RoomLayoutCorridor corridor )
{
corridor = null;
var bestDistance = MathF.Max( 12.0f, document.Settings.GridSize * 0.25f );
foreach ( var candidate in document.Corridors.Where( IsActiveFloor ) )
{
if ( !TryGetCorridorPath( candidate, out var points ) )
{
continue;
}
var width = CorridorClearWidth( candidate );
var hitDistance = width * 0.5f + MathF.Max( 8.0f, document.Settings.GridSize * 0.15f );
for ( var i = 0; i < points.Count - 1; i++ )
{
var distance = DistanceToSegment( point, points[i], points[i + 1] );
if ( distance > hitDistance || distance > bestDistance + width * 0.5f )
{
continue;
}
bestDistance = distance;
corridor = candidate;
}
}
return corridor is not null;
}
private bool TryGetDoorSegment( RoomLayoutDoor door, out Vector3 a, out Vector3 b )
{
a = default;
b = default;
if ( door.CorridorId != 0 )
{
return TryGetCorridorOpeningSegment( door.CorridorId, door.CorridorSegmentIndex, door.CorridorSide, door.Offset, door.Width, 7.0f, out a, out b );
}
var room = document.FindRoom( door.RoomId );
if ( room is null )
{
return false;
}
var half = DoorOpeningWidth( door ) * 0.5f;
var bounds = room.Bounds;
switch ( door.Side )
{
case RoomLayoutWallSide.North:
a = new Vector3( bounds.X + door.Offset - half, bounds.Y + bounds.Height, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
b = new Vector3( bounds.X + door.Offset + half, bounds.Y + bounds.Height, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
return true;
case RoomLayoutWallSide.South:
a = new Vector3( bounds.X + door.Offset - half, bounds.Y, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
b = new Vector3( bounds.X + door.Offset + half, bounds.Y, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
return true;
case RoomLayoutWallSide.East:
a = new Vector3( bounds.X + bounds.Width, bounds.Y + door.Offset - half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
b = new Vector3( bounds.X + bounds.Width, bounds.Y + door.Offset + half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
return true;
default:
a = new Vector3( bounds.X, bounds.Y + door.Offset - half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
b = new Vector3( bounds.X, bounds.Y + door.Offset + half, FloorWorldZ( document.FloorFor( door ) ) + 7.0f );
return true;
}
}
private bool TryGetDoorFootprint( RoomLayoutDoor door, out RoomLayoutRect footprint )
{
footprint = default;
return TryGetDoorSegment( door, out var a, out var b ) &&
TryGetOpeningFootprint( a, b, out footprint );
}
private bool TryGetWindowSegment( RoomLayoutWindow window, out Vector3 a, out Vector3 b )
{
a = default;
b = default;
var sillHeight = EffectiveWindowSillHeight( window );
var openingHeight = EffectiveWindowHeight( window, sillHeight );
var localZ = Math.Clamp(
sillHeight + openingHeight * 0.5f,
8.0f,
MathF.Max( 8.0f, document.Settings.WallHeight ) );
if ( window.CorridorId != 0 )
{
return TryGetCorridorOpeningSegment( window.CorridorId, window.CorridorSegmentIndex, window.CorridorSide, window.Offset, window.Width, localZ, out a, out b );
}
var room = document.FindRoom( window.RoomId );
if ( room is null )
{
return false;
}
var half = window.Width * 0.5f;
var bounds = room.Bounds;
var z = FloorWorldZ( document.FloorFor( window ) ) + localZ;
switch ( window.Side )
{
case RoomLayoutWallSide.North:
a = new Vector3( bounds.X + window.Offset - half, bounds.Y + bounds.Height, z );
b = new Vector3( bounds.X + window.Offset + half, bounds.Y + bounds.Height, z );
return true;
case RoomLayoutWallSide.South:
a = new Vector3( bounds.X + window.Offset - half, bounds.Y, z );
b = new Vector3( bounds.X + window.Offset + half, bounds.Y, z );
return true;
case RoomLayoutWallSide.East:
a = new Vector3( bounds.X + bounds.Width, bounds.Y + window.Offset - half, z );
b = new Vector3( bounds.X + bounds.Width, bounds.Y + window.Offset + half, z );
return true;
default:
a = new Vector3( bounds.X, bounds.Y + window.Offset - half, z );
b = new Vector3( bounds.X, bounds.Y + window.Offset + half, z );
return true;
}
}
private bool TryGetWindowFootprint( RoomLayoutWindow window, out RoomLayoutRect footprint )
{
footprint = default;
return TryGetWindowSegment( window, out var a, out var b ) &&
TryGetOpeningFootprint( a, b, out footprint );
}
private bool TryGetOpeningFootprint( Vector3 a, Vector3 b, out RoomLayoutRect footprint )
{
footprint = default;
var deltaX = MathF.Abs( b.x - a.x );
var deltaY = MathF.Abs( b.y - a.y );
var depth = MathF.Max( 4.0f, document.Settings.WallThickness );
if ( MathF.Max( deltaX, deltaY ) < 1.0f )
{
return false;
}
if ( deltaX >= deltaY )
{
footprint = new RoomLayoutRect( MathF.Min( a.x, b.x ), a.y - depth * 0.5f, deltaX, depth );
return true;
}
footprint = new RoomLayoutRect( a.x - depth * 0.5f, MathF.Min( a.y, b.y ), depth, deltaY );
return true;
}
private bool TryGetCorridorOpeningSegment(
int corridorId,
int segmentIndex,
int side,
float offset,
float width,
float z,
out Vector3 a,
out Vector3 b )
{
a = default;
b = default;
var corridor = document.Corridors.FirstOrDefault( candidate => candidate.Id == corridorId );
if ( corridor is null || !TryGetCorridorPath( corridor, out var points ) || segmentIndex < 0 || segmentIndex >= points.Count - 1 )
{
return false;
}
var start = points[segmentIndex];
var end = points[segmentIndex + 1];
var delta = end - start;
if ( delta.Length < 1.0f )
{
return false;
}
var horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
var clearWidth = CorridorClearWidth( corridor );
var minAlong = horizontal ? MathF.Min( start.x, end.x ) : MathF.Min( start.y, end.y );
var maxAlong = horizontal ? MathF.Max( start.x, end.x ) : MathF.Max( start.y, end.y );
var halfOpening = width * 0.5f;
var centerAlong = Math.Clamp( minAlong + offset, minAlong + halfOpening, MathF.Max( minAlong + halfOpening, maxAlong - halfOpening ) );
var fixedCoordinate = (horizontal ? start.y : start.x) + Math.Sign( side == 0 ? 1 : side ) * clearWidth * 0.5f;
z += FloorWorldZ( document.FloorFor( corridor ) );
if ( horizontal )
{
a = new Vector3( centerAlong - halfOpening, fixedCoordinate, z );
b = new Vector3( centerAlong + halfOpening, fixedCoordinate, z );
return true;
}
a = new Vector3( fixedCoordinate, centerAlong - halfOpening, z );
b = new Vector3( fixedCoordinate, centerAlong + halfOpening, z );
return true;
}
private bool TryGetDoorCenter( RoomLayoutDoor door, out Vector2 center )
{
center = default;
if ( !TryGetDoorSegment( door, out var a, out var b ) )
{
return false;
}
center = new Vector2( (a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f );
return true;
}
private bool TryGetWindowCenter( RoomLayoutWindow window, out Vector2 center )
{
center = default;
if ( !TryGetWindowSegment( window, out var a, out var b ) )
{
return false;
}
center = new Vector2( (a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f );
return true;
}
private float EffectiveDoorHeight( RoomLayoutDoor door )
{
var height = door.Height > 0.0f ? door.Height : document.Settings.DoorHeight;
return Math.Clamp( height, 0.0f, document.Settings.WallHeight );
}
private float EffectiveWindowSillHeight( RoomLayoutWindow window )
{
var sillHeight = window.SillHeight >= 0.0f ? window.SillHeight : document.Settings.WindowSillHeight;
return Math.Clamp( sillHeight, 0.0f, MathF.Max( 0.0f, document.Settings.WallHeight - 1.0f ) );
}
private float EffectiveWindowHeight( RoomLayoutWindow window, float sillHeight )
{
var height = window.Height > 0.0f ? window.Height : document.Settings.WindowHeight;
return Math.Clamp( height, 1.0f, MathF.Max( 1.0f, document.Settings.WallHeight - sillHeight ) );
}
private float DoorOpeningWidth( RoomLayoutDoor door )
{
var connectedWidth = 0.0f;
foreach ( var corridor in document.Corridors )
{
if ( corridor.StartDoorId != door.Id && corridor.EndDoorId != door.Id )
{
continue;
}
connectedWidth = MathF.Max( connectedWidth, CorridorClearWidth( corridor ) );
}
return connectedWidth > 0.0f
? MathF.Min( door.Width, connectedWidth )
: door.Width;
}
private bool TryGetDoorPoint( int doorId, out Vector2 point )
{
point = default;
if ( !TryGetDoorAnchor( doorId, out var anchor ) )
{
return false;
}
point = anchor.Point;
return true;
}
private bool TryGetCorridorPath( RoomLayoutCorridor corridor, out List<Vector2> points )
{
points = null;
if ( !document.CorridorDoorsAreOnFloor( corridor ) )
{
return false;
}
if ( !TryGetDoorAnchor( corridor.StartDoorId, out var start ) ||
!TryGetDoorAnchor( corridor.EndDoorId, out var end ) )
{
return false;
}
var width = CorridorClearWidth( corridor );
var startPortal = CorridorPortalPoint( start );
var endPortal = CorridorPortalPoint( end );
var startApproach = CorridorApproachPoint( start, width );
var endApproach = CorridorApproachPoint( end, width );
points = new List<Vector2>();
if ( corridor.ManualPath )
{
AddCorridorPoint( points, startPortal );
AddCorridorPoint( points, startApproach );
foreach ( var bend in corridor.BendPoints )
{
AddCorridorPoint( points, bend.Vector );
}
AddCorridorPoint( points, endApproach );
AddCorridorPoint( points, endPortal );
SimplifyCorridorPath( points );
return true;
}
AddCorridorPoint( points, startPortal );
AddCorridorPoint( points, startApproach );
foreach ( var bend in corridor.BendPoints )
{
AddCorridorPoint( points, bend.Vector );
}
if ( corridor.BendPoints.Count == 0 && !startApproach.x.AlmostEqual( endApproach.x ) && !startApproach.y.AlmostEqual( endApproach.y ) )
{
AddCorridorPoint( points, new Vector2( endApproach.x, startApproach.y ) );
}
AddCorridorPoint( points, endApproach );
AddCorridorPoint( points, endPortal );
SimplifyCorridorPath( points );
return true;
}
private Vector2 CorridorPortalPoint( RoomLayoutDoorAnchor door )
{
return door.Point;
}
private Vector2 CorridorApproachPoint( RoomLayoutDoorAnchor door, float width )
{
return door.Point + door.Normal * (width * 0.5f + document.Settings.WallThickness);
}
private static void AddCorridorPoint( List<Vector2> points, Vector2 point )
{
if ( points.Count == 0 || Vector2.DistanceBetween( points[^1], point ) > 0.5f )
{
points.Add( point );
}
}
private static void SimplifyCorridorPath( List<Vector2> points )
{
for ( var i = 1; i < points.Count - 1; )
{
if ( IsRedundantCorridorPoint( points[i - 1], points[i], points[i + 1] ) )
{
points.RemoveAt( i );
continue;
}
i++;
}
}
private static bool IsRedundantCorridorPoint( Vector2 a, Vector2 b, Vector2 c )
{
if ( a.x.AlmostEqual( b.x ) && b.x.AlmostEqual( c.x ) )
{
return IsBetween( b.y, a.y, c.y );
}
return a.y.AlmostEqual( b.y ) && b.y.AlmostEqual( c.y ) && IsBetween( b.x, a.x, c.x );
}
private static bool IsBetween( float value, float a, float b )
{
return value >= MathF.Min( a, b ) - 0.5f && value <= MathF.Max( a, b ) + 0.5f;
}
private bool TryGetDoorAnchor( int doorId, out RoomLayoutDoorAnchor anchor )
{
anchor = default;
var door = document.FindDoor( doorId );
if ( door is null || !TryGetDoorCenter( door, out var point ) )
{
return false;
}
anchor = new RoomLayoutDoorAnchor( point, DoorNormal( door.Side ) );
return true;
}
private static Vector2 DoorNormal( RoomLayoutWallSide side )
{
return side switch
{
RoomLayoutWallSide.North => new Vector2( 0.0f, 1.0f ),
RoomLayoutWallSide.South => new Vector2( 0.0f, -1.0f ),
RoomLayoutWallSide.East => new Vector2( 1.0f, 0.0f ),
_ => new Vector2( -1.0f, 0.0f )
};
}
private static float DistanceToSegment( Vector2 point, Vector2 a, Vector2 b )
{
var segment = b - a;
var lengthSquared = segment.LengthSquared;
if ( lengthSquared.AlmostEqual( 0.0f ) )
{
return Vector2.DistanceBetween( point, a );
}
var t = (((point.x - a.x) * segment.x) + ((point.y - a.y) * segment.y)) / lengthSquared;
t = Math.Clamp( t, 0.0f, 1.0f );
var closest = a + segment * t;
return Vector2.DistanceBetween( point, closest );
}
private static float DistanceToRect( Vector2 point, RoomLayoutRect rect )
{
var dx = MathF.Max( MathF.Max( rect.X - point.x, 0.0f ), point.x - (rect.X + rect.Width) );
var dy = MathF.Max( MathF.Max( rect.Y - point.y, 0.0f ), point.y - (rect.Y + rect.Height) );
return MathF.Sqrt( dx * dx + dy * dy );
}
private RoomLayoutRect SnapRect( RoomLayoutRect rect )
{
var min = new Vector2( Snap( rect.X ), Snap( rect.Y ) );
var max = new Vector2( Snap( rect.X + rect.Width ), Snap( rect.Y + rect.Height ) );
return RoomLayoutRect.FromPoints( min, max );
}
private float Snap( float value )
{
if ( BypassGridSnap )
{
return value;
}
var grid = MathF.Max( 1.0f, document.Settings.GridSize );
return MathF.Round( value / grid ) * grid;
}
private Vector2 SnapCursorPoint( Vector3 point )
{
if ( mode == RoomLayoutToolMode.Select || BypassGridSnap )
{
return new Vector2( point.x, point.y );
}
return mode is RoomLayoutToolMode.Corridors or RoomLayoutToolMode.Doors or RoomLayoutToolMode.Windows
? new Vector2( SnapHalfGrid( point.x ), SnapHalfGrid( point.y ) )
: new Vector2( Snap( point.x ), Snap( point.y ) );
}
private float SnapHalfGrid( float value )
{
if ( BypassGridSnap )
{
return value;
}
var grid = MathF.Max( 1.0f, document.Settings.GridSize * 0.5f );
return MathF.Round( value / grid ) * grid;
}
private BBox SnapGizmoBox( BBox startBox, BBox deltaBox )
{
if ( !BypassGridSnap )
{
return Gizmo.Snap( startBox, deltaBox );
}
return new BBox(
startBox.Mins + deltaBox.Mins,
startBox.Maxs + deltaBox.Maxs );
}
private static bool BypassGridSnap => global::Editor.Application.IsKeyDown( KeyCode.Shift );
private readonly record struct RoomLayoutDoorAnchor( Vector2 Point, Vector2 Normal );
}