Editor UI tool code that updates visibility of generated architecture GameObjects in the scene. It walks the scene hierarchy under a root, toggles Enabled on buildings, layers, roofs, floors and pieces according to various hide flags, hidden layer/piece lists, and the current level, and caches roof lookup for efficiency.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// Name-matched on generated roots - the plan never changes, so unhiding costs nothing and a rebuild reapplies the toggles.
public partial class ArchTool
{
public void ApplyVisibility()
{
var root = ArchScene.FindRoot( Scene );
if ( root.IsValid() )
{
ApplyVisibility( root );
}
}
// Nothing hidden means nothing to hide: a build comes out of the generator entirely enabled, so the walk only
// has to happen when a toggle or an eye is actually holding something back.
bool Hiding => HideRoofs || HideWalls || HideFloors || HideFoundation || HideTrim || OnlyCurrentLevel
|| hiddenLayers.Count > 0 || hiddenPieces.Count > 0;
// Remembered, because turning the LAST toggle off still needs one walk to put everything back - skipping that
// one would leave the scene holding the state the toggle was just released from.
bool hid;
void ApplyVisibility( GameObject root )
{
if ( !Hiding && !hid )
{
return;
}
hid = Hiding;
roofsById = null;
foreach ( var building in root.Children )
{
if ( MutedLayer( building ) )
{
building.Enabled = false;
continue;
}
building.Enabled = true;
foreach ( var node in building.Children )
{
if ( MutedLayer( node ) )
{
node.Enabled = false;
continue;
}
var isRoof = node.Name.StartsWith( "Roof", StringComparison.Ordinal )
|| node.Name.StartsWith( "Downpipe", StringComparison.Ordinal );
if ( isRoof )
{
// Gutters and downpipes hang off a roof, so they follow the section's storey too.
var section = RoofOf( node );
node.Enabled = !HideRoofs && !(OnlyCurrentLevel && section is not null && section.Level != Level);
if ( node.Enabled )
{
Reveal( node, ArchNames.TrySourceId( node.Name, out var roofed ) ? roofed : 0, "" );
}
continue;
}
// A floor is filed under its level, not a room, so it answers the level hide itself.
var storey = ArchNames.TryParseId( node.Name, "Floor", out var floored ) ? floored : (int?)null;
var room = Plan.FindRoom( RoomIdOf( node ) );
var levelHidden = OnlyCurrentLevel && (storey ?? room?.Floor ?? Level) != Level;
node.Enabled = !levelHidden;
if ( levelHidden )
{
continue;
}
Reveal( node, ArchNames.TrySourceId( node.Name, out var owned ) ? owned : 0, "" );
}
}
}
// EVERY generated object below a building, not the two ranks the walk used to reach: a fascia is three deep
// and the eye beside its row has to reach it. A name carrying a plan id starts a new layer, so what an eye
// holds below that point is a PIECE of it - and hiding an object takes everything under it with it, which is
// exactly what hiding a group of pieces means.
void Reveal( GameObject node, int owner, string piece )
{
foreach ( var child in node.Children )
{
var layer = owner;
var named = piece;
if ( ArchNames.TrySourceId( child.Name, out var id ) )
{
layer = id;
named = "";
}
else
{
named = ArchParts.Joined( named, child.Name );
}
child.Enabled = !Hidden( child.Name ) && !LayerHidden( layer )
&& (named.Length == 0 || !PieceHidden( layer, named ));
if ( child.Enabled )
{
Reveal( child, layer, named );
}
}
}
// The stack's eye, resolved through the id every generated name carries. Presentation only - the
// layer still generates, so unhiding it costs nothing and a rebuild reapplies the toggle.
bool MutedLayer( GameObject node )
{
return ArchNames.TrySourceId( node.Name, out var id ) && LayerHidden( id );
}
bool Hidden( string name )
{
if ( name.StartsWith( "Wall", StringComparison.Ordinal ) )
{
return HideWalls;
}
if ( name is "Slab" or "Overhang" )
{
return HideFloors;
}
if ( name == "Foundation" )
{
return HideFoundation;
}
if ( name.StartsWith( "Trim", StringComparison.Ordinal ) )
{
return HideTrim;
}
return false;
}
static int RoomIdOf( GameObject node )
{
return ArchNames.TryParseId( node.Name, "Room", out var id ) ? id : 0;
}
Dictionary<int, ArchRoofPart> roofsById;
// Indexed once per walk: this was a SelectMany over every roof in the plan, per roof node in the scene.
ArchRoofPart RoofOf( GameObject node )
{
if ( !ArchNames.TryParseId( node.Name, "Roof", out var id ) )
{
return null;
}
if ( roofsById is null )
{
roofsById = new Dictionary<int, ArchRoofPart>();
foreach ( var roof in Plan.Buildings.SelectMany( building => building.Roofs ) )
{
roofsById[roof.Id] = roof;
}
}
return roofsById.TryGetValue( id, out var found ) ? found : null;
}
}