Editor/Services/ArchSoffit.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Real undersides with real footprints, not the infinite ceiling plane.
public static class ArchSoffit {
// Nearest underside the ray crosses inside its own footprint, looking up only.
public static bool Struck( ArchPlan plan, ArchKit kit, int level, Ray ray, out Vector3 hit, out ArchRoom on, out List<Vector2> surface ) {
hit = default;
on = null;
surface = null;
if ( plan is null || kit is null || ray.Forward.z <= 0.001f ) {
return false;
}
var nearest = float.MaxValue;
foreach ( var (height, lift, outline, room) in Undersides( plan, kit, level ) ) {
// Cross at lifted height (where it stands), report at authored height (where the plan reads).
if ( !Crosses( ray, height + lift, out var at ) ) {
continue;
}
var reach = Vector3.Dot( at - ray.Position, ray.Forward );
if ( reach <= 0f || reach >= nearest || !ArchFootprint.Contains( outline, new Vector2( at.x, at.y ) ) ) {
continue;
}
nearest = reach;
hit = new Vector3( at.x, at.y, height );
on = room;
surface = outline;
}
return nearest < float.MaxValue;
}
// True if any room on this storey covers the point, or if no rooms exist yet.
public static bool Ceilinged( ArchPlan plan, int level, Vector2 at ) {
var standing = false;
foreach ( var room in plan?.AllRooms() ?? Enumerable.Empty<ArchRoom>() ) {
if ( room.Floor != level ) {
continue;
}
standing = true;
if ( ArchFootprint.Contains( ArchFloorGen.Footprint( room ), at ) ) {
return true;
}
}
return !standing;
}
// Grown to the outer wall face, not the centreline.
static IEnumerable<(float Height, float Lift, List<Vector2> Outline, ArchRoom On)> Undersides( ArchPlan plan, ArchKit kit, int level ) {
foreach ( var building in plan.Buildings ) {
var lift = ArchAsks.Lift( plan, building, kit );
foreach ( var storey in Storeys( building, level ) ) {
var shells = ArchRegion.Shell( ArchRegion.Footprints( storey, false ), kit.WallThickness );
foreach ( var outline in ArchFootprint.Union( shells ) ) {
yield return (storey[0].BaseHeight - kit.FloorThickness, lift, outline, storey[0]);
}
foreach ( var room in storey.Where( standing => standing.Floor > 0 ) ) {
foreach ( var outline in ArchOverhangGen.Unsupported( room, building, kit ) ) {
yield return (ArchOverhangGen.Soffit( room, kit ), lift, outline, room);
}
}
}
foreach ( var platform in building.Platforms ) {
if ( !platform.Hung || platform.Level <= level || !ArchLayerGate.On( platform ) ) {
continue;
}
// Platform is poured under the lift, so its authored height is already where it stands.
yield return (platform.GradeHeight - lift, lift, platform.Outline(), null);
}
}
}
// Unioned per storey so a party wall between rooms does not read as an overhead edge.
static IEnumerable<List<ArchRoom>> Storeys( ArchBuilding building, int level ) {
return building.Rooms
.Where( room => room.Floor > level && room.HasFloor && !room.Spans && ArchLayerGate.On( room ) )
.GroupBy( room => (room.Floor, Height: MathF.Round( room.BaseHeight )) )
.Select( storey => storey.ToList() );
}
static bool Crosses( Ray ray, float height, out Vector3 hit ) {
hit = default;
if ( MathF.Abs( ray.Forward.z ) < 0.0001f ) {
return false;
}
var distance = (height - ray.Position.z) / ray.Forward.z;
hit = ray.Position + ray.Forward * distance;
return true;
}
}