Editor tool code for placing openings (windows/doors) in architecture. Tracks drag start/end, computes a placement shape (width, height, sill) constrained to wall runs, grid units, and presets, and finds nearest wall points from cursor or a fixed wall during dragging.
using System;
using Sandbox;
namespace Sunless.Architecture;
public readonly struct ArchOpeningPoint
{
public ArchWall Wall { get; init; }
public ArchRoom Room { get; init; }
public ArchBuilding Building { get; init; }
public float Along { get; init; }
public float Height { get; init; }
public bool WallPlane { get; init; }
}
// The four answers a drag needs beyond the cursor, held here because both opening tools drag the
// same way and a toggle that lived on one of them would be a second set of rules.
public sealed class ArchOpeningDrag
{
public bool RestrictStartHeight { get; set; }
public bool RestrictEndHeight { get; set; }
public bool PadSides { get; set; } = true;
public bool ClearCornice { get; set; }
}
public sealed class ArchOpeningPlacement
{
public ArchOpeningDrag Drag { get; } = new();
ArchOpeningPoint anchor;
bool dragging;
public void Update(
ArchTool tool,
bool focused,
Action<ArchOpeningPoint, ArchOpeningPoint, bool> preview,
Action<ArchOpeningPoint, ArchOpeningPoint, bool> place )
{
if ( !focused || !Point( tool, out var current ) )
{
return;
}
if ( Gizmo.WasLeftMousePressed )
{
anchor = current;
dragging = true;
return;
}
using ( ArchGhost.Begin() )
{
preview( dragging ? anchor : current, current, dragging );
}
if ( !dragging || !Gizmo.WasLeftMouseReleased )
{
return;
}
dragging = false;
tool.EnsureTarget();
var unit = tool.Grid.SubgridSize( 4 );
var resized = MathF.Abs( current.Along - anchor.Along ) >= unit
|| MathF.Abs( current.Height - anchor.Height ) >= unit;
place( anchor, current, resized );
}
public static ArchOpeningShape Shape(
ArchOpeningPoint from,
ArchOpeningPoint to,
ArchOpeningPreset preset,
ArchGridService grid,
ArchKit kit,
ArchOpeningDrag drag )
{
var unit = grid.SubgridSize( 4 );
var wallHeight = ArchWallSection.Height( from.Wall, from.Room, kit );
var along = MathF.Abs( to.Along - from.Along );
var vertical = MathF.Abs( to.Height - from.Height );
var resizedWidth = along >= unit;
var resizedHeight = vertical >= unit;
var resizedVerticalSpan = resizedHeight && (!drag.RestrictStartHeight || !drag.RestrictEndHeight);
var minimumWidth = resizedWidth ? unit : ArchGridService.FinestSize;
var minimumHeight = resizedVerticalSpan ? unit : ArchGridService.FinestSize;
var headroom = drag.ClearCornice
? ArchOpeningClearance.Head( from.Room, from.Building, from.Wall, kit, wallHeight )
: wallHeight;
// Both ends are clamped into the run BEFORE the width is taken from them, so a drag that
// overshoots a corner lands exactly on the margin instead of leaving whatever the cursor
// happened to be short by. Measuring the width first and centring it afterwards is what
// left an uneven sliver at each end of a full-length window.
ArchOpeningSeats.Usable( from.Wall, preset, kit, drag.PadSides, minimumWidth, out var runFrom, out var runTo );
float left;
float right;
if ( resizedWidth )
{
left = Math.Clamp( MathF.Min( from.Along, to.Along ), runFrom, runTo );
right = Math.Clamp( MathF.Max( from.Along, to.Along ), runFrom, runTo );
}
else
{
ArchOpeningSeats.Centre( from.Along, ArchGridService.Fine( preset.Width ), runFrom, runTo, out left, out right );
}
var width = MathF.Max( minimumWidth, right - left );
var offset = (left + right) * 0.5f;
var wallPlaneDrag = resizedHeight && from.WallPlane && to.WallPlane;
var presetSill = ArchGridService.Fine( preset.SillHeight );
var presetTop = ArchGridService.Fine( preset.SillHeight + preset.Height );
var sill = wallPlaneDrag && !drag.RestrictStartHeight
? grid.Subgrid( MathF.Min( from.Height, to.Height ), 4 )
: presetSill;
var top = wallPlaneDrag && !drag.RestrictEndHeight
? grid.Subgrid( MathF.Max( from.Height, to.Height ), 4 )
: presetTop;
sill = Math.Clamp( sill, 0f, MathF.Max( 0f, headroom - minimumHeight ) );
top = Math.Clamp( top, minimumHeight, headroom );
if ( top - sill < minimumHeight )
{
if ( drag.RestrictEndHeight && !drag.RestrictStartHeight )
{
sill = MathF.Max( 0f, top - minimumHeight );
}
else
{
top = MathF.Min( headroom, sill + minimumHeight );
}
}
return new ArchOpeningShape
{
Offset = offset,
Width = width,
Height = top - sill,
SillHeight = sill
};
}
bool Point( ArchTool tool, out ArchOpeningPoint point )
{
if ( dragging )
{
return FixedWallPoint( tool, anchor, out point );
}
return NearestWallPoint( tool, out point );
}
static bool NearestWallPoint( ArchTool tool, out ArchOpeningPoint point )
{
point = default;
if ( WallPlanePoint( tool, Gizmo.CurrentRay, null, out point ) )
{
return true;
}
if ( !tool.GroundPoint( out var ground ) )
{
return false;
}
var wall = tool.NearestWall( ground, out var room, out var along, out var distance );
if ( wall is null || room is null || distance > 64f )
{
return false;
}
point = new ArchOpeningPoint
{
Wall = wall,
Room = room,
Building = tool.Plan.OwnerOf( room ),
Along = tool.Grid.Subgrid( along, 4 ),
Height = 0f,
WallPlane = false
};
return true;
}
static bool FixedWallPoint( ArchTool tool, ArchOpeningPoint anchor, out ArchOpeningPoint point )
{
if ( WallPlanePoint( tool, Gizmo.CurrentRay, anchor.Wall, out point ) )
{
return true;
}
if ( !tool.GroundPoint( out var ground ) )
{
point = anchor;
return true;
}
var along = Math.Clamp(
Vector2.Dot( ground - anchor.Wall.Start, anchor.Wall.Direction ),
0f,
anchor.Wall.Length );
var wallPoint = anchor.Wall.PointAt( along );
point = new ArchOpeningPoint
{
Wall = anchor.Wall,
Room = anchor.Room,
Building = anchor.Building,
Along = tool.Grid.Subgrid( along, 4 ),
Height = tool.Grid.Subgrid( MathF.Abs( Vector2.Dot( ground - wallPoint, anchor.Wall.Normal ) ), 4 ),
WallPlane = false
};
return true;
}
static bool WallPlanePoint( ArchTool tool, Ray ray, ArchWall fixedWall, out ArchOpeningPoint point )
{
point = default;
var nearest = float.MaxValue;
foreach ( var building in tool.Plan.Buildings )
{
var lift = ArchAsks.Lift( tool.Plan, building, tool.Kit );
foreach ( var room in building.Rooms )
{
if ( room.Floor != tool.Level )
{
continue;
}
var baseHeight = room.BaseHeight + lift;
var wallHeight = room.WallHeight > 0f ? room.WallHeight : tool.Kit.WallHeight;
foreach ( var wall in room.Walls )
{
if ( fixedWall is not null && wall != fixedWall )
{
continue;
}
var denominator = ray.Forward.x * wall.Normal.x + ray.Forward.y * wall.Normal.y;
if ( MathF.Abs( denominator ) < 0.0001f )
{
continue;
}
var offset = new Vector2( ray.Position.x, ray.Position.y ) - wall.Start;
var distance = -Vector2.Dot( offset, wall.Normal ) / denominator;
if ( distance < 0f || distance >= nearest )
{
continue;
}
var hit = ray.Position + ray.Forward * distance;
var flat = new Vector2( hit.x, hit.y );
var along = Vector2.Dot( flat - wall.Start, wall.Direction );
var height = hit.z - baseHeight;
if ( fixedWall is null
&& (along < -8f || along > wall.Length + 8f || height < -8f || height > wallHeight + 8f) )
{
continue;
}
nearest = distance;
point = new ArchOpeningPoint
{
Wall = wall,
Room = room,
Building = building,
Along = tool.Grid.Subgrid( Math.Clamp( along, 0f, wall.Length ), 4 ),
Height = tool.Grid.Subgrid( Math.Clamp( height, 0f, wallHeight ), 4 ),
WallPlane = true
};
}
}
}
return nearest < float.MaxValue;
}
}