Editor/Output/ArchCull.Interiors.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// One shallow room behind one opening on a facade nobody can walk into. DERIVED, so it carries no id and no
// layer record - exactly as slabs, ceilings, roof decks and platforms carry none.
public sealed class ArchFalseInterior {
public ArchBuilding Building { get; init; }
public ArchRoom Room { get; init; }
public ArchWall Wall { get; init; }
public ArchOpening Opening { get; init; }
// The room face of the wall, in the wall's own frame - the plane the box is measured off.
public float Face { get; init; }
public float Depth { get; init; }
public float Back => Face + Depth;
}
public static partial class ArchCull {
// No way in and nothing above, asked of a whole unit rather than one of its roofs: a shell with no door
// and no archway, capped everywhere, is a facade. Anything else is a room, or becoming one.
public static bool Unenterable( ArchBuilding building, ArchPlan plan, ArchKit kit ) {
if ( building is null ) {
return false;
}
var walls = building.Rooms.SelectMany( room => room.Walls.Select( wall => (Room: room, Wall: wall) ) ).ToList();
if ( walls.Count == 0 ) {
return false;
}
// A single-sided shell emits no room face at all, so there is nothing in there to be in.
if ( walls.All( standing => ArchWallSection.SingleSided( standing.Wall, standing.Room ) ) ) {
return true;
}
if ( walls.SelectMany( standing => standing.Wall.Openings ).Any( Entered ) ) {
return false;
}
return building.Roofs.Count > 0 && building.Roofs.All( roof => Sealed( roof, building, plan, kit ) );
}
// A leaf hangs in a way in and an archway is a hole walked straight through. A window is neither.
static bool Entered( ArchOpening opening ) {
return opening.Kind.Hangs() || opening.Kind == OpeningKind.Archway;
}
// Every box the plan asks for, resolved without a scene, so the pass, the report and the test all read one
// answer. A unit ArchCull calls enterable contributes none: behind a real room the box would z-fight the
// room it is standing inside.
public static List<ArchFalseInterior> Behind( ArchPlan plan, ArchKit kit ) {
var found = new List<ArchFalseInterior>();
if ( plan is null ) {
return found;
}
var depth = MathF.Max( 1f, kit.FalseInteriorDepth );
foreach ( var building in plan.Buildings.Where( unit => Unenterable( unit, plan, kit ) ) ) {
foreach ( var room in building.Rooms ) {
foreach ( var wall in room.Walls.Where( wall => !ArchWallJoins.CoveredBy( building, room, wall ) ) ) {
var face = ArchWallSection.Thickness( wall, kit ) * 0.5f;
found.AddRange( Showing( wall ).Select( opening => new ArchFalseInterior {
Building = building,
Room = room,
Wall = wall,
Opening = opening,
Face = face,
Depth = depth
} ) );
}
}
}
return found;
}
// The units the wall generator actually cut a hole for - the same filter it runs, because a box behind a
// hole nothing opened is a box hanging in a solid wall.
static IEnumerable<ArchOpening> Showing( ArchWall wall ) {
return wall.Openings
.Where( opening => opening.Width > 0.5f && opening.Height > 0.5f )
.Where( opening => ArchLayerGate.On( opening ) && ArchLayerGate.Owned( opening.OwnerId ) )
.Where( opening => opening.SwallowedBy == 0 || !ArchLayerGate.Owned( opening.SwallowedBy ) );
}
// The box, in the wall's own frame, wound to face the hole. Its front IS the hole: a face on the wall's
// room plane would be the sticker this exists to remove.
public static void Line( ArchMesh canvas, ArchFalseInterior interior, ArchBrush brush ) {
var opening = interior.Opening;
var left = opening.Left;
var right = opening.Right;
var bottom = MathF.Max( 0f, opening.SillHeight );
var top = opening.Top;
var face = interior.Face;
var back = interior.Back;
if ( right - left < 0.5f || top - bottom < 0.5f || interior.Depth < 0.5f ) {
return;
}
canvas.Quad(
new Vector3( left, back, bottom ),
new Vector3( right, back, bottom ),
new Vector3( right, back, top ),
new Vector3( left, back, top ),
brush );
canvas.Quad(
new Vector3( left, face, bottom ),
new Vector3( left, back, bottom ),
new Vector3( left, back, top ),
new Vector3( left, face, top ),
brush );
canvas.Quad(
new Vector3( right, back, bottom ),
new Vector3( right, face, bottom ),
new Vector3( right, face, top ),
new Vector3( right, back, top ),
brush );
canvas.Quad(
new Vector3( left, face, top ),
new Vector3( left, back, top ),
new Vector3( right, back, top ),
new Vector3( right, face, top ),
brush );
canvas.Quad(
new Vector3( left, back, bottom ),
new Vector3( left, face, bottom ),
new Vector3( right, face, bottom ),
new Vector3( right, back, bottom ),
brush );
}
// The pass on its own, for when the faces have already been cleaned. A rebuild prunes what it emits, the
// same way it takes back the faces Clean removed.
public static int Interiors( Scene scene, ArchPlan plan, ArchKit kit ) {
var root = ArchScene.FindRoot( scene );
if ( !root.IsValid() ) {
Log.Warning( "Architecture: nothing to line - no generated root in this scene." );
return 0;
}
using ( SceneEditorSession.Active.UndoScope( "Line False Interiors" ).WithGameObjectChanges( root, GameObjectUndoFlags.All ).Push() ) {
return Lined( root, plan, kit );
}
}
// Re-derived every run - a door added to a facade makes it a room, and the box behind its window has to GO rather
// than be left standing inside it - but only HANDED OVER where the box actually changed. Drawing one is five
// quads; giving it to the engine cooks a collision hull, a physics mesh and a trace mesh, and this pass reaches
// every window on every facade in the plan.
//
// A null cache lines them all, which is what the menu action and a scene nobody has built through mean.
public static int Lined( GameObject root, ArchPlan plan, ArchKit kit, ArchBuildCache cache = null ) {
var nodes = Walls( root );
var style = new ArchStyle( kit );
var wanted = Behind( plan, kit ).GroupBy( interior => interior.Wall.Id ).ToDictionary( group => group.Key, group => group.ToList() );
var lined = 0;
foreach ( var bare in nodes.Where( entry => !wanted.ContainsKey( entry.Key ) ) ) {
Strip( bare.Value );
cache?.Unlined( bare.Key );
}
foreach ( var group in wanted ) {
if ( !nodes.TryGetValue( group.Key, out var node ) ) {
continue;
}
// The wall's own frame, so the boxes are drawn in the coordinates the wall generator uses and the
// projection puts them on the same grain as the room face they hang behind.
var canvas = new ArchMesh( node.WorldTransform );
foreach ( var interior in group.Value ) {
Line( canvas, interior, style.Brush(
ArchSurface.WallInterior, interior.Wall.Palette, interior.Room.Palette, interior.Building.Palette ) );
lined++;
}
// Asked of the box's own key, the way a part is - and of the scene as well, because a hand-delete leaves
// the cache saying something is standing that is not.
if ( cache?.Lined( group.Key, canvas.Content ) == true && Standing( node ) ) {
continue;
}
Strip( node );
Fit( node, canvas );
}
return lined;
}
static void Strip( GameObject wall ) {
if ( wall.Children.FirstOrDefault( child => child.Name == ArchPieces.Interior ) is { } standing ) {
standing.DestroyImmediate();
}
}
static bool Standing( GameObject wall ) {
return wall.Children.Any( child => child.Name == ArchPieces.Interior );
}
static Dictionary<int, GameObject> Walls( GameObject root ) {
var nodes = new Dictionary<int, GameObject>();
foreach ( var node in ArchScene.Descendants( root ) ) {
if ( ArchNames.TryParseId( node.Name, "Wall", out var id ) ) {
nodes[id] = node;
}
}
return nodes;
}
static void Fit( GameObject wall, ArchMesh canvas ) {
if ( canvas.IsEmpty ) {
return;
}
var node = wall.Scene.CreateObject();
node.Name = ArchPieces.Interior;
node.SetParent( wall, false );
node.Tags.Add( ArchScene.GeneratedTag );
var renderer = node.Components.GetOrCreate<MeshComponent>();
renderer.Color = Color.White;
renderer.SmoothingAngle = 0f;
// It must not be traceable: the cleaning pass reads cover by ray, and a box behind a window it could
// hit would have the elevation in front of it stripped as buried.
ArchCollision.Write( node, renderer, canvas, ArchCollisionMode.None );
renderer.Mesh = canvas.Finish();
}
}