Editor/Tool/ArchToolVisibility.cs

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.

File Access
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;
	}
}