Editor/Output/ArchLocate.Resolve.cs

Editor helper for locating architectural targets in the scene for the ArchTool. It builds ArchTarget responses by collecting planned extents and scene objects for queries like point/around, whole plan, selected part, storey, kind, or specific part id, assembling geometry bounds and face counts.

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

namespace Sunless.Architecture;

public static partial class ArchLocate
{
	static ArchTarget Around( ArchTool tool, Vector3 point, float radius )
	{
		var planned = new ArchExtent();

		planned.Add( point );

		var target = Assemble( $"{radius:0.#}in around {point}", "point", null, null, null,
			ArchScene.FindRoots( tool.Scene ), planned.Grown( radius ), 0f );

		return target.Aimed();
	}

	static ArchTarget Whole( ArchTool tool )
	{
		var roots = ArchScene.FindRoots( tool.Scene );
		var planned = new ArchExtent();
		var extents = ArchExtents.Load();

		foreach ( var building in tool.Plan.Buildings )
		{
			planned.Add( BuildingExtent( building, tool.Kit ) );
		}

		foreach ( var road in tool.Plan.Roads() )
		{
			planned.Add( extents.Measure( ArchKind.Road, road,
				new ArchExtentContext( tool.Plan, tool.Kit, null, null, tool ) ) );
		}

		return Assemble( "the whole plan", "plan", null, null, null, roots, planned, 0f );
	}

	static ArchTarget Selected( ArchTool tool )
	{
		if ( tool.Picked?.Item is null )
		{
			return new ArchTarget { Describe = "nothing selected", Kind = "none", Note = "Nothing is picked in the viewport. Pass a part id, a kind keyword, or 'plan'." };
		}

		var id = ArchPlanStore.IdOf( tool.Picked.Item );

		return id > 0 ? Focus( tool, Part( tool, id ), null ) : Whole( tool );
	}

	static ArchTarget Storey( ArchTool tool, int level )
	{
		var planned = new ArchExtent();
		var objects = new List<GameObject>();

		foreach ( var building in tool.Plan.Buildings )
		{
			foreach ( var room in building.Rooms.Where( room => room.Floor == level ) )
			{
				planned.Add( RoomExtent( room, tool.Kit ) );
				Collect( tool, ArchNames.Room( room ), objects );
			}

			foreach ( var roof in building.Roofs.Where( roof => roof.Level == level ) )
			{
				planned.Add( RoofExtent( roof ) );
				Collect( tool, ArchNames.Roof( roof ), objects );
			}
		}

		foreach ( var root in ArchScene.FindRoots( tool.Scene ).SelectMany( ArchScene.Descendants ) )
		{
			if ( root.Name.StartsWith( $"Floor_{level}_", StringComparison.Ordinal ) && !objects.Contains( root ) )
			{
				objects.Add( root );
			}
		}

		return Assemble( $"storey {level}", "level", null, null, null, objects, planned, 0f, true, level );
	}

	// 'porches'/'approaches' lose their e too: trimming just 's' resolves to nothing.
	static string Singular( string query )
	{
		var kind = (query ?? string.Empty).Trim().ToLowerInvariant();

		if ( kind.Length > 3 && kind.EndsWith( "es", StringComparison.Ordinal ) && kind[..^2].EndsWith( "ch", StringComparison.Ordinal ) )
		{
			return kind[..^2];
		}

		return kind.TrimEnd( 's' );
	}

	static ArchTarget Kind( ArchTool tool, string kind, int? level )
	{
		var planned = new ArchExtent();
		var objects = new List<GameObject>();
		var extents = kind is "stair" or "pillar" or "porch" or "pipes" or "brackets" or "road" or "pavement" or "kerb" or "marking"
			or "bridge" or "tunnel" or "lining" or "portal" or "walkway" or "connection" ? ArchExtents.Load() : null;

		if ( kind is "walkway" or "connection" )
		{
			foreach ( var building in tool.Plan.Buildings )
			{
				foreach ( var room in building.Rooms.Where( room => (!level.HasValue || room.Floor == level) && ArchAsks.IsWalkway( room ) ) )
				{
					planned.Add( extents.Measure( ArchKind.Walkway, room,
						new ArchExtentContext( tool.Plan, tool.Kit, room, building, tool ) ) );
					Collect( tool, ArchNames.Room( room ), objects );
				}
			}

			if ( objects.Count == 0 && !planned.Any )
			{
				return Missing( kind, $"Nothing in the plan matches '{kind}'." );
			}

			var target = Assemble( $"every {kind}", kind, null, null, null, objects, planned, 0f, true, level );
			return Focus( tool, target, level );
		}

		if ( kind is "tunnel" or "lining" or "portal" )
		{
			foreach ( var road in tool.Plan.Roads() )
			{
				foreach ( var tunnel in road.Tunnels )
				{
					planned.Add( extents.Measure( ArchKind.Tunnel, tunnel,
						new ArchExtentContext( tool.Plan, tool.Kit, null, null, tool ) ) );
					Collect( tool, ArchNames.Tunnel( tunnel ), objects );
				}
			}

			return Assemble( $"every {kind}", kind, null, null, null, objects, planned, 0f, true, level );
		}

		if ( kind == "bridge" )
		{
			foreach ( var road in tool.Plan.Roads() )
			{
				foreach ( var bridge in road.Bridges )
				{
					planned.Add( extents.Measure( ArchKind.Bridge, bridge,
						new ArchExtentContext( tool.Plan, tool.Kit, null, null, tool ) ) );
					Collect( tool, ArchNames.Bridge( bridge ), objects );
				}
			}

			return Assemble( "every bridge", kind, null, null, null, objects, planned, 0f, true, level );
		}

		if ( kind is "road" or "pavement" or "kerb" or "marking" )
		{
			foreach ( var road in tool.Plan.Roads() )
			{
				planned.Add( extents.Measure( ArchKind.Road, road,
					new ArchExtentContext( tool.Plan, tool.Kit, null, null, tool ) ) );
				Collect( tool, ArchNames.Road( road ), objects );
			}

			return Assemble( $"every {kind}", kind, null, null, null, objects, planned, 0f, true, level );
		}

		// A junction is derived: its extent is the reach of the legs that make it.
		if ( kind is "junction" )
		{
			foreach ( var junction in ArchAnswers.Load().RoadJunctions( tool.Plan, tool.Kit ) )
			{
				planned.Add( junction.Extent );
				Collect( tool, junction.Name, objects );
			}

			return Assemble( "every junction", kind, null, null, null, objects, planned, 0f );
		}

		foreach ( var building in tool.Plan.Buildings )
		{
			switch ( kind )
			{
				case "building":
					planned.Add( BuildingExtent( building, tool.Kit, level ) );
					Collect( tool, ArchNames.Building( building ), objects );
					continue;

				case "gutter":
					foreach ( var roof in building.Roofs.Where( roof => !level.HasValue || roof.Level == level ) )
					{
						planned.Add( RoofExtent( roof ) );
					}

					Collect( tool, "Gutters", objects );
					continue;

				case "roof":
					foreach ( var roof in building.Roofs.Where( roof => !level.HasValue || roof.Level == level ) )
					{
						planned.Add( RoofExtent( roof ) );
						Collect( tool, ArchNames.Roof( roof ), objects );
					}

					continue;

				case "downpipe":
					foreach ( var pipe in building.Downpipes )
					{
						planned.Add( DownpipeExtent( pipe ) );
						Collect( tool, ArchNames.Downpipe( pipe ), objects );
					}

					continue;

				case "pipes":
					foreach ( var run in building.Pipes )
					{
						planned.Add( extents.Measure( ArchKind.Pipe, run,
							new ArchExtentContext( tool.Plan, tool.Kit, null, building ) ) );
						Collect( tool, ArchNames.Pipe( run ), objects );
					}

					continue;

				case "brackets":
					foreach ( var bracket in building.Brackets )
					{
						planned.Add( extents.Measure( ArchKind.Bracket, bracket,
							new ArchExtentContext( tool.Plan, tool.Kit, null, building ) ) );
						Collect( tool, ArchNames.Bracket( bracket ), objects );
					}

					continue;

				case "fence":
					foreach ( var fence in building.Fences )
					{
						planned.Add( FenceExtent( fence ) );
						Collect( tool, ArchNames.Fence( fence ), objects );
					}

					continue;

				case "platform":
					foreach ( var platform in building.Platforms.Where( entry => !level.HasValue || entry.Level == level ) )
					{
						planned.Add( PlatformExtent( platform ) );
						Collect( tool, ArchNames.Platform( platform ), objects );
					}

					continue;

				// A cut generates nothing (an absence), so its extent is the only thing collected.
				case "cut":
					foreach ( var entry in building.Cuts.Where( entry => !level.HasValue || entry.Level == level ) )
					{
						planned.Add( CutExtent( entry ) );
					}

					continue;

				case "rooflight":
					foreach ( var section in building.Roofs.Where( roof => !level.HasValue || roof.Level == level ) )
					{
						foreach ( var light in section.Lights )
						{
							planned.Add( extents.Measure( ArchKind.RoofLight, light,
								new ArchExtentContext( tool.Plan, tool.Kit, null, building, Host: section ) ) );
						}

						if ( section.Lights.Count > 0 )
						{
							Collect( tool, ArchNames.Roof( section ), objects );
						}
					}

					continue;
			}

			foreach ( var room in building.Rooms.Where( room => !level.HasValue || room.Floor == level ) )
			{
				switch ( kind )
				{
					case "room":
						planned.Add( RoomExtent( room, tool.Kit ) );
						Collect( tool, ArchNames.Room( room ), objects );
						break;

					case "slab":
						planned.Add( RoomExtent( room, tool.Kit ) );
						Collect( tool, "Slab", objects );
						break;

					case "foundation":
						planned.Add( RoomExtent( room, tool.Kit ) );
						Collect( tool, "Foundation", objects );
						break;

					case "wall":
						foreach ( var wall in room.Walls )
						{
							planned.Add( WallExtent( wall, room, tool.Kit ) );
							Collect( tool, ArchNames.Wall( wall ), objects );
						}

						break;

					case "opening":
					case "door":
					case "window":
						foreach ( var wall in room.Walls )
						{
							foreach ( var opening in Openings( wall, kind ) )
							{
								planned.Add( OpeningExtent( opening, wall, room, tool.Kit ) );
							}

							if ( Openings( wall, kind ).Any() )
							{
								Collect( tool, ArchNames.Wall( wall ), objects );
							}
						}

						break;

					case "stair":
						foreach ( var stair in room.Stairs )
						{
							planned.Add( extents.Measure( ArchKind.Stair, stair,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ) );
							Collect( tool, ArchNames.Stair( stair ), objects );
						}

						break;

					case "pillar":
						foreach ( var pillar in room.Pillars )
						{
							planned.Add( extents.Measure( ArchKind.Pillar, pillar,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ) );
							Collect( tool, ArchNames.Pillar( pillar ), objects );
						}

						break;

					case "beam":
						foreach ( var beam in room.Beams )
						{
							planned.Add( BeamExtent( beam ) );
							Collect( tool, ArchNames.Beam( beam ), objects );
						}

						break;

					case "porch":
						foreach ( var porch in room.Porches )
						{
							planned.Add( extents.Measure( ArchKind.Porch, porch,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ) );
							Collect( tool, ArchNames.Porch( porch ), objects );
						}

						break;

					case "trim":
						foreach ( var trim in room.Trims )
						{
							planned.Add( TrimExtent( trim ) );
							Collect( tool, ArchNames.Trim( trim ), objects );
						}

						break;

					case "approach":
						foreach ( var approach in room.Approaches )
						{
							planned.Add( extents.Measure( ArchKind.Approach, approach,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ) );
							Collect( tool, ArchNames.Approach( approach ), objects );
						}

						break;
				}
			}
		}

		if ( objects.Count == 0 && !planned.Any )
		{
			return new ArchTarget
			{
				Describe = kind,
				Kind = "none",
				Note = $"Nothing in the plan matches '{kind}'. Use a part id, 'plan', 'selection', 'level:<n>', or one of: {string.Join( ", ", Kinds )}."
			};
		}

		var assembled = Assemble( $"every {kind}", kind, null, null, null, objects, planned, 0f, true, level );

		return kind is "road" or "junction" or "tunnel" ? assembled : Focus( tool, assembled, level );
	}

	static IEnumerable<ArchOpening> Openings( ArchWall wall, string kind )
	{
		return wall.Openings.Where( opening => kind switch
		{
			"window" => opening.Kind.Glazes(),
			"door" => opening.Kind.Hangs(),
			_ => true
		} );
	}

	static ArchTarget Part( ArchTool tool, int id )
	{
		// No own object: empty geometry aims the ortho at the hole, not the host. Asked plan-wide because a cut
		// is filed on whatever holds it - a building or a road - and it carves in world space either way.
		if ( tool.Plan.FindCut( id ) is { } cut )
		{
			return Assemble( $"cut {cut.Name}", "cut", cut, null, tool.Plan.OwnerOf( cut ), new List<GameObject>(),
				CutExtent( cut ), cut.Segments.FirstOrDefault()?.Yaw ?? 0f );
		}

		foreach ( var road in tool.Plan.Roads() )
		{
			if ( road.Id == id )
			{
				return Single( tool, $"road {road.Name}", "road", road, null, null,
					ArchNames.Road( road ), ArchExtents.Load().Measure( ArchKind.Road, road,
						new ArchExtentContext( tool.Plan, tool.Kit, null, null, tool ) ), RoadYaw( road ) );
			}

			foreach ( var bridge in road.Bridges )
			{
				if ( bridge.Id != id )
				{
					continue;
				}

				var objects = new List<GameObject>();

				Collect( tool, ArchNames.Bridge( bridge ), objects );

				return Assemble( $"bridge {bridge.Name}", "bridge", bridge, null, null, objects,
					ArchExtents.Load().Measure( ArchKind.Bridge, bridge,
						new ArchExtentContext( tool.Plan, tool.Kit, null, null, tool ) ), RoadYaw( road ) );
			}

			foreach ( var tunnel in road.Tunnels )
			{
				if ( tunnel.Id != id )
				{
					continue;
				}

				var objects = new List<GameObject>();

				Collect( tool, ArchNames.Tunnel( tunnel ), objects );

				return Assemble( $"tunnel {tunnel.Name}", "tunnel", tunnel, null, null, objects,
					ArchExtents.Load().Measure( ArchKind.Tunnel, tunnel,
						new ArchExtentContext( tool.Plan, tool.Kit, null, null, tool ) ), RoadYaw( road ) );
			}
		}

		foreach ( var building in tool.Plan.Buildings )
		{
			if ( building.Id == id )
			{
				return Single( tool, $"building {building.Name}", "building", building, null, building,
					ArchNames.Building( building ), BuildingExtent( building, tool.Kit ), 0f );
			}

			foreach ( var roof in building.Roofs )
			{
				if ( roof.Id == id )
				{
					return Single( tool, $"roof {roof.Name}", "roof", roof, null, building,
						ArchNames.Roof( roof ), RoofExtent( roof ), roof.RidgeAlongX ? 0f : 90f );
				}
			}

			foreach ( var pipe in building.Downpipes )
			{
				if ( pipe.Id == id )
				{
					return Single( tool, $"downpipe {pipe.Name}", "downpipe", pipe, null, building,
						ArchNames.Downpipe( pipe ), DownpipeExtent( pipe ), 0f );
				}
			}

			foreach ( var run in building.Pipes )
			{
				if ( run.Id == id )
				{
					return Single( tool, $"pipe run {run.Name}", "pipes", run, null, building,
						ArchNames.Pipe( run ), ArchExtents.Load().Measure( ArchKind.Pipe, run,
							new ArchExtentContext( tool.Plan, tool.Kit, null, building ) ), 0f );
				}
			}

			foreach ( var bracket in building.Brackets )
			{
				if ( bracket.Id == id )
				{
					return Single( tool, $"brackets {bracket.Name}", "brackets", bracket, null, building,
						ArchNames.Bracket( bracket ), ArchExtents.Load().Measure( ArchKind.Bracket, bracket,
							new ArchExtentContext( tool.Plan, tool.Kit, null, building ) ), bracket.AlongX ? 0f : 90f );
				}
			}

			foreach ( var fence in building.Fences )
			{
				if ( fence.Id == id )
				{
					return Single( tool, $"fence {fence.Name}", "fence", fence, null, building,
						ArchNames.Fence( fence ), FenceExtent( fence ), FenceYaw( fence ) );
				}
			}

			foreach ( var platform in building.Platforms )
			{
				if ( platform.Id == id )
				{
					return Single( tool, $"platform {platform.Name}", "platform", platform, null, building,
						ArchNames.Platform( platform ), PlatformExtent( platform ), 0f );
				}
			}


			// A light lives in its roof section's mesh, so it answers with that section's name.
			foreach ( var light in building.Roofs.SelectMany( section => section.Lights ) )
			{
				if ( light.Id == id )
				{
					var section = building.Roofs.First( roof => roof.Lights.Contains( light ) );

					return Single( tool, $"roof light {light.Name}", "rooflight", light, null, building,
						ArchNames.Roof( section ), ArchExtents.Load().Measure( ArchKind.RoofLight, light,
							new ArchExtentContext( tool.Plan, tool.Kit, null, building, Host: section ) ), 0f );
				}
			}

			foreach ( var room in building.Rooms )
			{
				if ( room.Id == id )
				{
					return Single( tool, $"room {room.Name}", "room", room, room, building,
						ArchNames.Room( room ), RoomExtent( room, tool.Kit ), 0f );
				}

				foreach ( var wall in room.Walls )
				{
					if ( wall.Id == id )
					{
						return Single( tool, $"wall {wall.Id}", "wall", wall, room, building,
							ArchNames.Wall( wall ), WallExtent( wall, room, tool.Kit ), WallYaw( wall ) );
					}

					foreach ( var opening in wall.Openings )
					{
						if ( opening.Id != id )
						{
							continue;
						}

						// Frames the opening's own box; its geometry is still the wall's mesh.
						return Single( tool, $"{opening.Preset} {opening.Id} in wall {wall.Id}", "opening", opening, room, building,
							ArchNames.Wall( wall ), OpeningExtent( opening, wall, room, tool.Kit ), WallYaw( wall ) );
					}
				}

				foreach ( var stair in room.Stairs )
				{
					if ( stair.Id == id )
					{
						return Single( tool, $"stair {stair.Name}", "stair", stair, room, building,
							ArchNames.Stair( stair ), ArchExtents.Load().Measure( ArchKind.Stair, stair,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ), stair.Yaw );
					}
				}

				foreach ( var pillar in room.Pillars )
				{
					if ( pillar.Id == id )
					{
						return Single( tool, $"pillars {pillar.Name}", "pillar", pillar, room, building,
							ArchNames.Pillar( pillar ), ArchExtents.Load().Measure( ArchKind.Pillar, pillar,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ), pillar.Yaw );
					}
				}

				foreach ( var beam in room.Beams )
				{
					if ( beam.Id == id )
					{
						return Single( tool, $"beam {beam.Name}", "beam", beam, room, building,
							ArchNames.Beam( beam ), BeamExtent( beam ), beam.Yaw );
					}
				}

				foreach ( var porch in room.Porches )
				{
					if ( porch.Id == id )
					{
						return Single( tool, $"porch {porch.Name}", "porch", porch, room, building,
							ArchNames.Porch( porch ), ArchExtents.Load().Measure( ArchKind.Porch, porch,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ), 0f );
					}
				}

				foreach ( var approach in room.Approaches )
				{
					if ( approach.Id == id )
					{
						return Single( tool, $"{approach.Kind} {approach.Name}".ToLowerInvariant(), "approach", approach, room, building,
							ArchNames.Approach( approach ), ArchExtents.Load().Measure( ArchKind.Approach, approach,
								new ArchExtentContext( tool.Plan, tool.Kit, room, building ) ), approach.Yaw );
					}
				}

				foreach ( var trim in room.Trims )
				{
					if ( trim.Id == id )
					{
						return Single( tool, $"trim {trim.Name}", "trim", trim, room, building,
							ArchNames.Trim( trim ), TrimExtent( trim ), 0f );
					}
				}
			}
		}

		return new ArchTarget
		{
			Describe = $"id {id}",
			Kind = "none",
			Note = $"No part in the plan has id {id}. arch_status lists building, room and roof ids; arch_walls lists wall and opening ids. A platform or a roof light answers to its own id, or to the 'platforms' and 'rooflights' keywords."
		};
	}

	static ArchTarget Single(
		ArchTool tool, string describe, string kind, object item, ArchRoom room, ArchBuilding building,
		string objectName, ArchExtent planned, float yaw )
	{
		var objects = new List<GameObject>();
		Collect( tool, objectName, objects );

		return Assemble( describe, kind, item, room, building, objects, planned, yaw );
	}

	static ArchTarget Assemble(
		string describe, string kind, object item, ArchRoom room, ArchBuilding building,
		List<GameObject> objects, ArchExtent planned, float yaw, bool focused = false, int? level = null )
	{
		var built = new ArchExtent();
		var faces = 0;
		var seen = new HashSet<MeshComponent>();

		foreach ( var node in objects.Where( node => node.IsValid() ) )
		{
			foreach ( var mesh in node.Components.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants ) )
			{
				if ( mesh.Mesh is null || !seen.Add( mesh ) )
				{
					continue;
				}

				var transform = mesh.WorldTransform;

				foreach ( var face in mesh.Mesh.FaceHandles )
				{
					faces++;

					foreach ( var corner in mesh.Mesh.GetFaceVertexPositions( face, transform ) )
					{
						built.Add( corner );
					}
				}
			}
		}

		return new ArchTarget
		{
			Describe = describe,
			Kind = kind,
			Item = item,
			Room = room,
			Building = building,
			Objects = objects,
			Planned = planned.Box,
			Built = built.Box,
			Faces = faces,
			Yaw = yaw,
			Focused = focused,
			Level = level ?? room?.Floor
		};
	}
}