Editor/Output/ArchLocate.Context.cs

Editor-side utility for locating architectural scene objects. It computes a focused context of rooms, walls, roofs and collects matching GameObject nodes from the scene based on an ArchTarget, level and toolkit, then assembles a resulting ArchTarget view.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static partial class ArchLocate
{
	static ArchTarget Focus( ArchTool tool, ArchTarget target, int? requestedLevel )
	{
		if ( target.Kind == "none" )
		{
			return target;
		}

		var level = requestedLevel ?? target.Level ?? tool.Level;
		var rooms = tool.Plan.Buildings
			.SelectMany( building => building.Rooms )
			.Where( room => room.Floor == level )
			.ToList();
		var context = ContextRooms( target, rooms, tool.Kit );
		var objects = target.Objects.ToList();
		var planned = new ArchExtent();

		if ( target.Planned.Size.Length > 0.01f )
		{
			planned.Add( target.Planned.Mins );
			planned.Add( target.Planned.Maxs );
		}

		var focusedRooms = target.Room is null
			? context.Where( ArchAsks.IsWalkway ).ToList()
			: new List<ArchRoom> { target.Room };

		foreach ( var room in focusedRooms )
		{
			Collect( tool, ArchNames.Room( room ), objects );
			planned.Add( RoomExtent( room, tool.Kit ) );
		}

		foreach ( var room in context.Where( room => !focusedRooms.Contains( room ) ) )
		{
			foreach ( var wall in room.Walls.Where( wall => focusedRooms.Any( focused => Near( WallExtent( wall, room, tool.Kit ).Box, RoomExtent( focused, tool.Kit ).Box, ContextMargin( tool.Kit ) ) ) ) )
			{
				Collect( tool, ArchNames.Wall( wall ), objects );
				planned.Add( WallExtent( wall, room, tool.Kit ) );
			}
		}

		var style = new ArchStyle( tool.Kit );

		foreach ( var building in tool.Plan.Buildings )
		{
			foreach ( var storey in ArchAsks.Storeys( building, tool.Kit, style )
				.Where( storey => storey.Level == level && storey.Rooms.Any( context.Contains ) ) )
			{
				Collect( tool, building, storey.Name, objects );
			}
		}

		foreach ( var building in tool.Plan.Buildings )
		{
			foreach ( var roof in building.Roofs.Where( roof => roof.Level == level ) )
			{
				if ( (target.Kind is "walkway" or "connection") && !roof.Name.StartsWith( "Walkway", StringComparison.OrdinalIgnoreCase ) )
				{
					continue;
				}

				if ( focusedRooms.Any( room => Near( RoomExtent( room, tool.Kit ).Box, RoofExtent( roof ).Box, ContextMargin( tool.Kit ) ) ) )
				{
					Collect( tool, ArchNames.Roof( roof ), objects );
					planned.Add( RoofExtent( roof ) );
				}
			}
		}

		return Assemble( target.Describe, target.Kind, target.Item, target.Room, target.Building,
			objects, planned, target.Yaw, true, level );
	}

	static List<ArchRoom> ContextRooms( ArchTarget target, List<ArchRoom> rooms, ArchKit kit )
	{
		if ( target.Room is null )
		{
			if ( target.Kind is "walkway" or "connection" )
			{
				var walkways = rooms.Where( ArchAsks.IsWalkway ).ToList();

				return rooms
					.Where( room => ArchAsks.IsWalkway( room ) || walkways.Any( walkway => Near( RoomExtent( walkway, kit ).Box, RoomExtent( room, kit ).Box, ContextMargin( kit ) ) ) )
					.ToList();
			}

			return rooms;
		}

		var anchor = RoomExtent( target.Room, kit ).Box;
		var margin = ContextMargin( kit );

		return rooms
			.Where( room => ReferenceEquals( room, target.Room ) || Near( anchor, RoomExtent( room, kit ).Box, margin ) )
			.ToList();
	}

	static float ContextMargin( ArchKit kit ) => MathF.Max( 24f, kit.WallThickness * 2f );

	static bool Near( BBox first, BBox second, float margin )
	{
		return first.Mins.x <= second.Maxs.x + margin
			&& first.Maxs.x + margin >= second.Mins.x
			&& first.Mins.y <= second.Maxs.y + margin
			&& first.Maxs.y + margin >= second.Mins.y;
	}

	// By name each time: the scene is rebuilt wholesale, so any cache is stale.
	static void Collect( ArchTool tool, string name, List<GameObject> into )
	{
		foreach ( var root in ArchScene.FindRoots( tool.Scene ) )
		{
			foreach ( var node in ArchScene.Descendants( root ) )
			{
				if ( node.Name == name && !into.Contains( node ) )
				{
					into.Add( node );
				}
			}
		}
	}

	static void Collect( ArchTool tool, ArchBuilding building, string name, List<GameObject> into )
	{
		foreach ( var root in ArchScene.FindRoots( tool.Scene ) )
		{
			var owner = ArchScene.Descendants( root ).FirstOrDefault( node => node.Name == ArchNames.Building( building ) );

			if ( owner is null )
			{
				continue;
			}

			foreach ( var node in ArchScene.Descendants( owner ) )
			{
				if ( node.Name == name && !into.Contains( node ) )
				{
					into.Add( node );
				}
			}
		}
	}
}