Editor/Services/ArchContacts.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Scope of a contact query - outside its reach a part has nothing it may join to.
public readonly struct ArchReach {
public int? Storey { get; init; }
public IReadOnlySet<int> Buildings { get; init; }
public static ArchReach Anywhere => new();
public static ArchReach Grouped( ArchPlan plan, int itemId ) => new() { Buildings = ArchLayerGroups.Reach( plan, itemId ) };
public ArchReach OnStorey( int storey ) => new() { Storey = storey, Buildings = Buildings };
public bool Holds( ArchBuilding building ) => Buildings is null || Buildings.Contains( building.Id );
public bool Holds( ArchPlan plan, ArchRoom room ) {
if ( Storey is { } floor && room.Floor != floor ) {
return false;
}
if ( Buildings is null ) {
return true;
}
return plan.OwnerOf( room ) is { } owner && Buildings.Contains( owner.Id );
}
}
// One touched host, measured in that host's own frame.
public readonly record struct ArchTouch {
public int HostId { get; init; }
public ArchWall Wall { get; init; }
public ArchRoom Room { get; init; }
public float Along { get; init; }
public float Span { get; init; }
public float Height { get; init; }
public float Distance { get; init; }
}
// What a part is against: the surface under it, the face in front of it, the walls it lies along.
public static class ArchContacts {
// Bounded by a ceiling, not by a storey.
public static float? Under( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 at, float ceiling ) {
var seat = float.MinValue;
foreach ( var surface in Surfaces( plan, kit, reach, at ) ) {
if ( surface > seat && surface <= ceiling ) {
seat = surface;
}
}
return seat > float.MinValue ? seat : null;
}
// A negative Distance means the probe started inside the wall's thickness - flush, not nothing found.
public static ArchTouch? Facing( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 from, Vector2 direction, float limit ) {
ArchTouch? nearest = null;
foreach ( var (room, wall) in Walls( plan, reach ) ) {
if ( !Meets( wall, kit, from, direction, limit, out var face, out var along ) ) {
continue;
}
if ( nearest is { } standing && standing.Distance <= face ) {
continue;
}
nearest = new ArchTouch {
HostId = wall.Id,
Wall = wall,
Room = room,
Along = along,
Span = wall.Length,
Distance = face
};
}
return nearest;
}
// Overlap expressed in each wall's own axis.
public static IEnumerable<ArchTouch> Along( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 from, Vector2 to ) {
var span = to - from;
var length = span.Length;
if ( length < 1f ) {
yield break;
}
var direction = span / length;
foreach ( var (room, wall) in Walls( plan, reach ) ) {
if ( wall.Length < 1f || !ArchWallJoins.Aligned( direction, from, wall, kit.WallThickness ) ) {
continue;
}
var a = Vector2.Dot( from - wall.Start, wall.Direction );
var b = Vector2.Dot( to - wall.Start, wall.Direction );
var near = MathF.Max( MathF.Min( a, b ), 0f );
var far = MathF.Min( MathF.Max( a, b ), wall.Length );
if ( far - near < 4f ) {
continue;
}
yield return new ArchTouch {
HostId = wall.Id,
Wall = wall,
Room = room,
Along = (near + far) * 0.5f,
Span = far - near,
Height = ArchWallSection.Height( wall, room, kit )
};
}
}
public static IEnumerable<(ArchRoom Room, ArchWall Wall)> Walls( ArchPlan plan, ArchReach reach ) {
foreach ( var room in plan?.AllRooms() ?? Enumerable.Empty<ArchRoom>() ) {
if ( room.Spans || !ArchLayerGate.On( room ) || !reach.Holds( plan, room ) ) {
continue;
}
foreach ( var wall in room.Walls ) {
yield return (room, wall);
}
}
}
// Tests out to the outer wall face, not the centreline.
static IEnumerable<float> Surfaces( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 at ) {
var answers = ArchAnswers.Load();
foreach ( var building in plan?.Buildings ?? Enumerable.Empty<ArchBuilding>() ) {
if ( !reach.Holds( building ) ) {
continue;
}
var lift = answers.Lift( plan, building, kit );
foreach ( var room in building.Rooms ) {
if ( room.Spans || !ArchLayerGate.On( room ) || !reach.Holds( plan, room ) ) {
continue;
}
if ( ArchFootprint.Encloses( ArchRegion.Shell( ArchRegion.Footprints( new[] { room }, false ), kit.WallThickness ), at ) ) {
yield return room.BaseHeight;
}
}
// Offset by lift - platforms are poured under the building's grade.
foreach ( var platform in ArchLayerGate.Enabled( building.Platforms ) ) {
if ( ArchFootprint.Contains( platform.Outline(), at ) ) {
yield return ArchPlatformDeck.Seat( platform, at ) - lift;
}
}
foreach ( var beam in building.Rooms.SelectMany( standing => standing.Beams ).Where( ArchLayerGate.On ) ) {
if ( ArchFootprint.Contains( beam.Outline(), at ) ) {
yield return beam.TopHeight;
}
}
// Foundation plinth extends past the outer wall face.
var foundationLevel = ArchFloorGen.FoundationLevel( building );
if ( building.Rooms.FirstOrDefault( standing =>
standing.Floor == foundationLevel && standing.HasFloor && ArchLayerGate.On( standing ) ) is not { } foundation ) {
continue;
}
foreach ( var plinth in ArchFloorGen.FoundationOutline( plan, building, kit, foundationLevel ) ) {
if ( ArchFootprint.Contains( plinth, at ) ) {
yield return foundation.BaseHeight;
}
}
}
}
static bool Meets( ArchWall wall, ArchKit kit, Vector2 from, Vector2 direction, float limit, out float face, out float along ) {
face = 0f;
along = 0f;
var length = wall.Length;
if ( length < 1f ) {
return false;
}
// Only a wall square to the probe counts, or every side of everything reads walled.
var facing = Vector2.Dot( wall.Normal, direction );
if ( MathF.Abs( facing ) < 0.8f ) {
return false;
}
var half = (wall.Thickness > 0f ? wall.Thickness : kit.WallThickness) * 0.5f;
var travel = Vector2.Dot( wall.Normal, wall.Start - from ) / facing;
face = travel - half;
if ( face < -half - 1f || face > limit ) {
return false;
}
along = Vector2.Dot( from + direction * travel - wall.Start, wall.Direction );
return along >= -1f && along <= length + 1f;
}
}