Editor/Designers/ArchOpeningPlacement.cs
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;
}
// The AIM, not the settled point: which wall is being pointed at is a measurement, and the station along it
// is put on the ladder below anyway. Asked of a snapped point, the search reads whichever wall the snap had
// already chosen rather than the one under the cursor.
if ( !tool.AimPoint( 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.AimPoint( 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;
}
}