Editor/Services/ArchPick.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One pick for every caller: flights by footprint, not by their far-away origin.
public static class ArchPick {
static IEnumerable<ArchRoom> Storey( ArchPlan plan, int level ) {
return plan?.Buildings.SelectMany( building => building.Rooms ).Where( room => room.Floor == level )
?? Enumerable.Empty<ArchRoom>();
}
// By its rectangle, not its origin: the origin sits at the wall end.
public static ArchApproachPart ApproachIn( ArchRoom room, Vector2 point ) {
return room?.Approaches.FirstOrDefault( approach => Covers( approach, point ) );
}
public static ArchApproachPart ApproachUnder( ArchPlan plan, int level, Vector2 point, out ArchRoom room ) {
room = null;
foreach ( var candidate in Storey( plan, level ) ) {
if ( ApproachIn( candidate, point ) is not { } approach ) {
continue;
}
room = candidate;
return approach;
}
return null;
}
static bool Covers( ArchApproachPart approach, Vector2 point ) {
if ( approach.IsSpline ) {
return approach.Curve().Nearest( point, out var frame, out var gap )
&& gap <= MathF.Max( 12f, approach.Width * frame.WidthScale ) * 0.5f + 4f;
}
var outward = ArchApproachAxes.Outward( approach );
var across = new Vector2( -outward.y, outward.x );
var local = point - approach.Origin;
var along = Vector2.Dot( local, outward );
var side = MathF.Abs( Vector2.Dot( local, across ) );
return along >= -4f && along <= MathF.Max( 12f, approach.Run ) + 4f && side <= approach.Width * 0.5f + 4f;
}
// Wound first: an authored outline can arrive either way round.
public static ArchRoofPart RoofUnder( ArchBuilding building, int level, Vector2 point ) {
return building?.Roofs.FirstOrDefault( roof =>
roof.Level == level && ArchFootprint.Contains( ArchFootprint.Wind( roof.Outline() ), point ) );
}
public static ArchRoofPart RoofUnder( ArchPlan plan, int level, Vector2 point, out ArchBuilding owner ) {
owner = null;
foreach ( var building in plan?.Buildings ?? new List<ArchBuilding>() ) {
if ( RoofUnder( building, level, point ) is not { } roof ) {
continue;
}
owner = building;
return roof;
}
return null;
}
public static IReadOnlyList<ArchHit> HitsAt( ArchLayerTree tree, ArchKit kit, int level, Vector2 point, int limit = 12 ) {
var hits = new List<ArchHit>();
var kinds = ArchKinds.Load();
if ( tree is not null ) {
foreach ( var group in tree.Domains ) {
foreach ( var root in group.Children ) {
Collect( root, kit, level, point, hits, kinds );
}
}
}
// Groups are pickable but rank below their members.
foreach ( var group in hits
.Select( hit => tree?.Find( hit.Layer.ItemId ) )
.Select( GroupAbove )
.Where( node => node is not null )
.Distinct()
.ToList() ) {
hits.Add( Hit( group, ArchHitChannel.Boundary, 0f, false ) );
}
hits.Sort( ( left, right ) => {
var rank = Rank( kinds, left ).CompareTo( Rank( kinds, right ) );
return rank != 0 ? rank : left.Distance.CompareTo( right.Distance );
} );
return hits.Take( Math.Max( 1, limit ) ).ToList();
}
static void Collect( ArchLayerNode node, ArchKit kit, int level, Vector2 point, List<ArchHit> hits, ArchKinds kinds ) {
if ( node is null ) {
return;
}
if ( node.Payload is { } payload ) {
if ( ArchPickers.Load().For( node.Kind ) is { } picker ) {
picker.Under( new ArchPicking( node, kit, level, point, hits ) );
} else
switch ( payload ) {
case ArchBuilding building:
if ( ArchHandles.Bounds( building, out var boundsMin, out var boundsMax )
&& point.x >= boundsMin.x && point.x <= boundsMax.x
&& point.y >= boundsMin.y && point.y <= boundsMax.y ) {
hits.Add( Hit( node, ArchHitChannel.Boundary, 0f, false ) );
}
break;
// A spanning room is a Walkway; the rank, not the case, decides which it outranks.
case ArchRoom room:
var loop = ArchFloorGen.Footprint( room );
if ( loop.Count >= 3 && ArchFootprint.Contains( loop, point ) ) {
hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
}
break;
case ArchWall wall:
WallHit( node, wall, point, hits );
break;
case ArchOpening opening:
if ( node.Parent?.Payload is ArchWall owner ) {
StationHit( node, owner, opening.Left, opening.Right, point, hits );
}
break;
case ArchWallModPart modifier:
if ( node.Parent?.Payload is ArchWall host ) {
StationHit( node, host, modifier.Left, modifier.Right, point, hits );
}
break;
case ArchApproachPart approach when node.Room is not null:
if ( ApproachIn( node.Room, point ) == approach ) {
hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
}
break;
case ArchFencePart fence:
if ( fence.Curve().Nearest( point, out _, out var fenceGap )
&& fenceGap <= MathF.Max( 36f, fence.Barrier.PostSize * 4f ) ) {
hits.Add( Hit( node, ArchHitChannel.Path, fenceGap, true ) );
}
break;
case ArchCutPart cut when cut.Level == level:
if ( cut.Outlines().Any( outline => ArchFootprint.Contains( outline, point ) ) ) {
hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
}
break;
case ArchPlatformPart platform when platform.Level == level:
if ( ArchFootprint.Contains( platform.Outline(), point ) ) {
hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
}
break;
case ArchBeamPart beam when beam.Level == level:
if ( ArchFootprint.Contains( beam.Outline(), point ) ) {
hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
}
break;
case ArchDownpipePart pipe:
var pipeGap = (point - pipe.Wall).Length;
if ( pipeGap < 96f ) {
hits.Add( Hit( node, ArchHitChannel.Anchor, pipeGap, false ) );
}
break;
case ArchRoofPart roof when roof.Level == level:
if ( node.Building is { } roofOwner && RoofUnder( roofOwner, level, point ) == roof ) {
hits.Add( Hit( node, ArchHitChannel.Footprint, 0f, true ) );
}
break;
}
}
foreach ( var child in node.Children ) {
Collect( child, kit, level, point, hits, kinds );
}
}
static void StationHit( ArchLayerNode node, ArchWall wall, float left, float right, Vector2 point, List<ArchHit> hits ) {
var length = wall.Length;
if ( length < 0.5f ) {
return;
}
var at = Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length );
var gap = (point - wall.PointAt( at )).Length;
if ( gap < 96f && at >= left && at <= right ) {
hits.Add( Hit( node, ArchHitChannel.Span, gap, false ) );
}
}
static void WallHit( ArchLayerNode node, ArchWall wall, Vector2 point, List<ArchHit> hits ) {
if ( wall.Length < 0.5f ) {
return;
}
var at = Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, wall.Length );
var gap = (point - wall.PointAt( at )).Length;
if ( gap < 96f ) {
hits.Add( Hit( node, ArchHitChannel.Boundary, gap, false ) );
}
}
static ArchLayerNode GroupAbove( ArchLayerNode node ) {
for ( var current = node?.Parent; current is not null; current = current.Parent ) {
if ( current.Kind == ArchKind.Assembly ) {
return current;
}
}
return null;
}
internal static ArchLayerNode Child( ArchLayerNode parent, object payload ) {
return parent?.Children.FirstOrDefault( child => ReferenceEquals( child.Payload, payload ) );
}
internal static ArchHit Hit( ArchLayerNode node, ArchHitChannel channel, float distance, bool direct ) {
return new ArchHit {
Layer = node.Ref ?? new ArchLayerRef { ItemId = -1, Kind = node.Kind },
Channel = channel,
Distance = distance,
Depth = Depth( node ),
Direct = direct,
};
}
static int Depth( ArchLayerNode node ) {
var depth = 0;
for ( var current = node.Parent; current is not null; current = current.Parent ) {
depth++;
}
return depth;
}
static int Rank( ArchKinds kinds, ArchHit hit ) {
var kind = hit.Layer.Kind;
if ( kind == ArchKind.Assembly ) {
return -1;
}
if ( hit.Direct ) {
return kinds.For( kind )?.DirectRank ?? 200;
}
return kinds.For( kind )?.IndirectRank ?? 999;
}
}