Editor/Services/ArchFixtureAnchor.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Exterior wall wins over a nearer partition; free-standing walls use click side.
public readonly record struct ArchFixtureStation(
ArchWall Wall,
ArchBuilding Building,
ArchRoom Room,
Vector2 Outward,
float Along,
float Width,
bool Dragged ) {
public Vector2 Centre => Wall.PointAt( Along );
}
public static class ArchFixtureAnchor {
public const float Reach = 240f;
public const float Least = 12f;
public static ArchWall Nearest(
ArchPlan plan,
IEnumerable<ArchBuilding> buildings,
int? level,
Vector2 point,
out ArchBuilding host,
out ArchRoom room,
out float along,
out Vector2 outward,
bool exteriorFirst = true,
float reach = Reach ) {
host = null;
room = null;
along = 0f;
outward = default;
if ( buildings is null ) {
return null;
}
var kinds = ArchKinds.Load();
var found = new Candidate();
var loose = new Candidate();
foreach ( var building in buildings ) {
foreach ( var candidateRoom in building.Rooms ) {
if ( level is { } storey && candidateRoom.Floor != storey ) {
continue;
}
foreach ( var wall in plan.Filed<ArchWall>( candidateRoom, kinds ) ) {
Consider( building, candidateRoom, wall, point, ref found, ref loose );
}
}
}
var best = exteriorFirst
? found.Wall is not null ? found : loose
: Nearer( found, loose );
if ( best.Wall is null || best.Gap > reach ) {
return null;
}
host = best.Building;
room = best.Room;
along = best.Along;
outward = best.Outward;
return best.Wall;
}
// Clamp both ends into the run BEFORE measuring width; a short drag reads as a click.
public static ArchFixtureStation? Station(
IEnumerable<ArchBuilding> buildings,
int? level,
ArchGridService grid,
Vector2 from,
Vector2 to,
float typedWidth,
ArchPlan plan = null,
bool exteriorFirst = true,
float reach = Reach ) {
var wall = Nearest( plan, buildings, level, (from + to) * 0.5f, out var host, out var room, out _, out var outward,
exteriorFirst, reach );
if ( wall is null || room is null ) {
return null;
}
var run = wall.Length;
var usable = MathF.Max( Least, run );
var start = Math.Clamp( Vector2.Dot( from - wall.Start, wall.Direction ), 0f, run );
var finish = Math.Clamp( Vector2.Dot( to - wall.Start, wall.Direction ), 0f, run );
if ( finish < start ) {
(start, finish) = (finish, start);
}
var dragged = finish - start >= grid.SubgridSize();
var width = dragged
? finish - start
: Math.Clamp( typedWidth > Least ? typedWidth : Least, Least, usable );
if ( dragged ) {
start = Snapped( wall, grid, start );
finish = Snapped( wall, grid, finish );
width = MathF.Max( Least, finish - start );
} else {
width = MathF.Max( Least, grid.Subgrid( width, 4 ) );
start = Math.Clamp( Vector2.Dot( from - wall.Start, wall.Direction ) - width * 0.5f, 0f, MathF.Max( 0f, run - width ) );
}
width = MathF.Min( width, usable );
var along = Math.Clamp( start + width * 0.5f, width * 0.5f, MathF.Max( width * 0.5f, run - width * 0.5f ) );
return new ArchFixtureStation( wall, host, room, outward, along, width, dragged );
}
// The click form: one point, and the width comes from what was typed.
public static ArchFixtureStation? At( IEnumerable<ArchBuilding> buildings, int? level, ArchGridService grid, Vector2 point, float typedWidth, ArchPlan plan = null, bool exteriorFirst = true ) {
return Station( buildings, level, grid, point, point, typedWidth, plan, exteriorFirst );
}
// Snap to neighbouring opening/fixture edges first, grid second.
static float Snapped( ArchWall wall, ArchGridService grid, float along ) {
var reach = grid.SubgridSize();
var best = float.MaxValue;
var landed = along;
foreach ( var station in Neighbours( wall ) ) {
var gap = MathF.Abs( station - along );
if ( gap < reach && gap < best ) {
best = gap;
landed = station;
}
}
return best < float.MaxValue ? landed : grid.Subgrid( along, 4 );
}
static IEnumerable<float> Neighbours( ArchWall wall ) {
foreach ( var opening in wall.Openings ) {
var half = opening.Width * 0.5f;
yield return opening.Offset - half;
yield return opening.Offset + half;
}
}
static void Consider( ArchBuilding building, ArchRoom room, ArchWall wall, Vector2 point, ref Candidate found, ref Candidate loose ) {
var length = wall.Length;
if ( length < 0.5f ) {
return;
}
var along = Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length );
var gap = (point - wall.PointAt( along )).Length;
if ( ArchWallFaces.TryOutward( wall, room, building, out var outward ) ) {
found.Take( building, room, wall, along, gap, outward );
return;
}
loose.Take( building, room, wall, along, gap, Clicked( wall, along, point ) );
}
static Candidate Nearer( Candidate one, Candidate other ) {
if ( one.Wall is null ) {
return other;
}
return other.Wall is not null && other.Gap < one.Gap ? other : one;
}
static Vector2 Clicked( ArchWall wall, float along, Vector2 point ) {
var normal = wall.Normal;
return Vector2.Dot( normal, point - wall.PointAt( along ) ) < 0f ? -normal : normal;
}
struct Candidate {
public ArchBuilding Building;
public ArchRoom Room;
public ArchWall Wall;
public float Along;
public float Gap;
public Vector2 Outward;
public void Take( ArchBuilding building, ArchRoom room, ArchWall wall, float along, float gap, Vector2 outward ) {
if ( Wall is not null && gap >= Gap ) {
return;
}
Building = building;
Room = room;
Wall = wall;
Along = along;
Gap = gap;
Outward = outward;
}
}
}