Editor/Services/ArchCarry.cs

Editor utility for moving architectural elements when a building, room, roof, porch or corner is translated. It applies an ArchRemap to many contained parts and provides logic to carry items that are coincident with or along a dragged point/edge, updating coordinates, boxes, loops and related derived geometry.

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

namespace Sunless.Architecture;

// One answer to "the shape moved - carry everything standing on it". ArchRemap is the arithmetic; this is the
// WALK, and a list forgotten here is a part left standing where its host used to be. Two questions, one set of
// lists: a whole host remapped, and a single authored CORNER carried to where it was dragged.
public static class ArchCarry
{
	// A roof outline is authored on the wall it caps and a parapet stands on the deck above that, so points
	// sharing a corner arrive near each other rather than exactly on each other.
	public const float CornerReach = 1f;

	public static void Shift( ArchBuilding building, Vector2 shift ) => Unit( building, ArchRemap.Translation( shift ) );

	public static void Shift( ArchRoom room, Vector2 shift ) => Room( room, ArchRemap.Translation( shift ) );

	// Everything on the shell rides with it: rooms, roofs, the walls standing on those roofs, yard parts,
	// service runs, fixtures and shafts. Pulling a corner of the blueprint is the same walk as sliding it -
	// only the map differs.
	public static void Unit( ArchBuilding building, ArchRemap map )
	{
		if ( building is null )
		{
			return;
		}

		foreach ( var room in building.Rooms )
		{
			Room( room, map );
		}

		foreach ( var roof in building.Roofs )
		{
			Roof( roof, map );
		}

		foreach ( var fence in building.Fences )
		{
			map.Nodes( fence.Nodes );
		}

		foreach ( var platform in building.Platforms )
		{
			Platform( platform, map );
		}

		foreach ( var pipe in building.Downpipes )
		{
			pipe.Outlet = map.Of( pipe.Outlet );
			pipe.Wall = map.Of( pipe.Wall );
		}

		foreach ( var run in building.Pipes )
		{
			map.Nodes( run.Nodes );
		}

		foreach ( var bracket in building.Brackets )
		{
			var min = bracket.Min;
			var max = bracket.Max;

			map.Box( ref min, ref max );

			bracket.Min = min;
			bracket.Max = max;
		}

		foreach ( var ladder in building.Ladders )
		{
			ladder.Anchor = map.Of( ladder.Anchor );
		}

		foreach ( var balcony in building.Balconies )
		{
			balcony.Anchor = map.Of( balcony.Anchor );
		}

		foreach ( var flight in building.ExteriorStairs )
		{
			flight.Anchor = map.Of( flight.Anchor );
		}

		foreach ( var cut in building.Cuts )
		{
			Cut( cut, map );
		}

		foreach ( var cutout in building.Cutouts )
		{
			Cutout( cutout, map );
		}
	}

	// Must carry the footprint cache too, or walls stand where the slab isn't - and the fittings with them,
	// or a moved room leaves its stairs standing in the dirt.
	public static void Room( ArchRoom room, ArchRemap map )
	{
		if ( room is null )
		{
			return;
		}

		foreach ( var wall in room.Walls )
		{
			wall.Start = map.Of( wall.Start );
			wall.End = map.Of( wall.End );
		}

		map.Loop( room.Footprint );

		foreach ( var stair in room.Stairs )
		{
			Stair( stair, map );
		}

		foreach ( var trim in room.Trims )
		{
			map.Path( trim.Path );
		}

		foreach ( var pillar in room.Pillars )
		{
			pillar.Origin = map.Of( pillar.Origin );
		}

		// A span re-reads the pier it names, so only the point it fell back to has to travel - a wall end whose
		// station was left behind reprojects onto the moved wall at the wrong place along it.
		foreach ( var span in room.PierSpans )
		{
			span.From.At = map.Of( span.From.At );
			span.To.At = map.Of( span.To.At );
		}

		foreach ( var beam in room.Beams )
		{
			var outline = beam.Outline();

			map.Loop( outline );
			beam.Reshape( outline );
		}

		foreach ( var porch in room.Porches )
		{
			Porch( porch, map );
		}

		foreach ( var approach in room.Approaches )
		{
			approach.Origin = map.Of( approach.Origin );

			map.Nodes( approach.Nodes );
		}
	}

	// The walls standing on the deck ride the deck. A parapet is filed on the roof rather than a room, so a
	// walk that stops at Min/Max and the outline leaves every one of them behind.
	public static void Roof( ArchRoofPart roof, ArchRemap map )
	{
		if ( roof is null )
		{
			return;
		}

		var min = roof.Min;
		var max = roof.Max;

		map.Box( ref min, ref max );

		roof.Min = min;
		roof.Max = max;

		map.Loop( roof.Footprint );

		foreach ( var wall in roof.Walls )
		{
			wall.Start = map.Of( wall.Start );
			wall.End = map.Of( wall.End );
		}

		foreach ( var light in roof.Lights )
		{
			var lightMin = light.Min;
			var lightMax = light.Max;

			map.Box( ref lightMin, ref lightMax );

			light.Min = lightMin;
			light.Max = lightMax;
		}
	}

	public static void Platform( ArchPlatformPart platform, ArchRemap map )
	{
		if ( platform is null )
		{
			return;
		}

		var min = platform.Min;
		var max = platform.Max;

		map.Box( ref min, ref max );

		platform.Min = min;
		platform.Max = max;

		map.Loop( platform.Footprint );

		foreach ( var stair in platform.Stairs )
		{
			Stair( stair, map );
		}
	}

	// A porch is a host, so its own children move with it - a walk that carries only the legs slides the deck
	// out from under its steps, columns and balustrade.
	public static void Porch( ArchPorchPart porch, ArchRemap map )
	{
		if ( porch is null )
		{
			return;
		}

		foreach ( var leg in porch.Legs )
		{
			var min = leg.Min;
			var max = leg.Max;

			map.Box( ref min, ref max );

			leg.Min = min;
			leg.Max = max;
		}

		foreach ( var stair in porch.Stairs )
		{
			Stair( stair, map );
		}

		foreach ( var pillar in porch.Pillars )
		{
			pillar.Origin = map.Of( pillar.Origin );
		}

		foreach ( var trim in porch.Trims )
		{
			map.Path( trim.Path );
		}
	}

	// Every flight is stated in the shaft's own frame, so carrying a stair is carrying its four corners: the
	// lanes inside it ride along with no coordinate of their own to forget.
	public static void Stair( ArchStairPart stair, ArchRemap map )
	{
		if ( stair?.Core is not { } core )
		{
			return;
		}

		var frame = core.Axes;
		var origin = map.Of( core.Origin );
		var along = map.Of( frame.Flat( core.Length, 0f ) ) - origin;
		var across = map.Of( frame.Flat( 0f, core.Width ) ) - origin;

		core.Origin = origin;
		core.Yaw = along.IsNearZeroLength ? core.Yaw : MathF.Atan2( along.y, along.x ).RadianToDegree();
		core.Length = MathF.Max( 1f, along.Length );
		core.Width = MathF.Max( 1f, across.Length );
	}

	public static void Cut( ArchCutPart cut, ArchRemap map )
	{
		if ( cut is null )
		{
			return;
		}

		foreach ( var segment in cut.Segments )
		{
			segment.Start = map.Of( segment.Start );
			segment.End = map.Of( segment.End );

			if ( segment.HasLoop )
			{
				map.Loop( segment.Loop );
			}
		}
	}

	public static void Cutout( ArchFloorCutout cutout, ArchRemap map )
	{
		if ( cutout is null )
		{
			return;
		}

		if ( cutout.HasLoop )
		{
			map.Loop( cutout.Loop );

			return;
		}

		var min = cutout.Min;
		var max = cutout.Max;

		map.Box( ref min, ref max );

		cutout.Min = min;
		cutout.Max = max;
	}

	// The other question: ONE authored point moved, and everything that shares it follows. A wall is a corner
	// of its shell rather than a line floating in front of one, so dragging an end carries the slab's footprint,
	// the neighbouring walls that meet there, the storeys stacked above and the roof outline over the lot - or
	// the wall walks off and takes the shell open with it. Scoped to the BUILDING, which is what makes it the
	// same reach the blueprint has.
	public static bool Corner( ArchBuilding building, Vector2 from, Vector2 to )
	{
		if ( building is null || (to - from).Length < 0.01f )
		{
			return false;
		}

		return Carrying( building, point => Coincident( point, from ), _ => to );
	}

	// The same walk pushed along an EDGE, because a party wall meets an exterior run anywhere along it: only the
	// anchors standing ON the run ride the push, endpoints included.
	public static bool Along( ArchBuilding building, Vector2 start, Vector2 end, Vector2 shift )
	{
		if ( building is null || shift.Length < 0.01f )
		{
			return false;
		}

		return Carrying( building, point => OnRun( point, start, end ), point => point + shift );
	}

	public static bool Coincident( Vector2 point, Vector2 corner ) => (point - corner).Length < CornerReach;

	static bool OnRun( Vector2 point, Vector2 start, Vector2 end )
	{
		var span = end - start;
		var length = span.Length;

		if ( length < 0.01f )
		{
			return Coincident( point, start );
		}

		var along = Vector2.Dot( point - start, span / length );

		if ( along < -CornerReach || along > length + CornerReach )
		{
			return false;
		}

		var nearest = start + span * Math.Clamp( along / length, 0f, 1f );

		return (point - nearest).Length < CornerReach;
	}

	// One walk over the same inventory Unit remaps, so a corner drag and a body drag bring the same parts. The
	// lists Unit carries whole are filtered here by what the gesture actually reached.
	static bool Carrying( ArchBuilding building, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		var moved = false;

		foreach ( var room in building.Rooms )
		{
			moved |= Loop( room.Footprint, reaches, landed );
			moved |= Ends( room.Walls, reaches, landed );

			foreach ( var pillar in room.Pillars.Where( pillar => reaches( pillar.Origin ) ) )
			{
				pillar.Origin = landed( pillar.Origin );
				moved = true;
			}

			// A following run traces its host and re-derives, so moving its baked path would only be undone.
			foreach ( var trim in room.Trims.Where( trim => !trim.Follows ) )
			{
				moved |= Path( trim.Path, reaches, landed );
			}

			foreach ( var stair in room.Stairs )
			{
				moved |= Shaft( stair, reaches, landed );
			}

			foreach ( var beam in room.Beams )
			{
				var outline = beam.Outline();

				if ( !Loop( outline, reaches, landed ) )
				{
					continue;
				}

				beam.Reshape( outline );
				moved = true;
			}

			foreach ( var porch in room.Porches )
			{
				moved |= Carrying( porch, reaches, landed );
			}

			foreach ( var approach in room.Approaches )
			{
				if ( reaches( approach.Origin ) )
				{
					approach.Origin = landed( approach.Origin );
					moved = true;
				}

				moved |= Nodes( approach.Nodes, reaches, landed );
			}
		}

		foreach ( var roof in building.Roofs )
		{
			var outline = roof.Outline();

			if ( Loop( outline, reaches, landed ) )
			{
				roof.Reshape( outline );
				moved = true;
			}

			moved |= Ends( roof.Walls, reaches, landed );
		}

		foreach ( var platform in building.Platforms )
		{
			var outline = platform.Outline();

			if ( Loop( outline, reaches, landed ) )
			{
				platform.Reshape( outline );
				moved = true;
			}

			foreach ( var stair in platform.Stairs )
			{
				moved |= Shaft( stair, reaches, landed );
			}
		}

		foreach ( var fence in building.Fences )
		{
			moved |= Nodes( fence.Nodes, reaches, landed );
		}

		foreach ( var pipe in building.Downpipes )
		{
			if ( reaches( pipe.Outlet ) )
			{
				pipe.Outlet = landed( pipe.Outlet );
				moved = true;
			}

			if ( reaches( pipe.Wall ) )
			{
				pipe.Wall = landed( pipe.Wall );
				moved = true;
			}
		}

		foreach ( var run in building.Pipes )
		{
			moved |= Nodes( run.Nodes, reaches, landed );
		}

		foreach ( var bracket in building.Brackets )
		{
			var min = bracket.Min;
			var max = bracket.Max;

			if ( !Box( ref min, ref max, reaches, landed ) )
			{
				continue;
			}

			bracket.Min = min;
			bracket.Max = max;
			moved = true;
		}

		foreach ( var ladder in building.Ladders.Where( ladder => reaches( ladder.Anchor ) ) )
		{
			ladder.Anchor = landed( ladder.Anchor );
			moved = true;
		}

		foreach ( var balcony in building.Balconies.Where( balcony => reaches( balcony.Anchor ) ) )
		{
			balcony.Anchor = landed( balcony.Anchor );
			moved = true;
		}

		foreach ( var flight in building.ExteriorStairs.Where( flight => reaches( flight.Anchor ) ) )
		{
			flight.Anchor = landed( flight.Anchor );
			moved = true;
		}

		// The run is derived from the loop, so it is refitted along the yaw the shape already stood at - taking
		// a fresh yaw from a corner that just moved would swing the frame the carve reads.
		foreach ( var segment in building.Cuts.SelectMany( cut => cut.Segments ).Where( segment => segment.HasLoop ) )
		{
			var yaw = segment.Yaw;

			if ( !Loop( segment.Loop, reaches, landed ) )
			{
				continue;
			}

			ArchCut.Fit( segment, yaw );
			moved = true;
		}

		foreach ( var cutout in building.Cutouts.Where( cutout => cutout.HasLoop ) )
		{
			var loop = cutout.Loop.ToList();

			if ( !Loop( loop, reaches, landed ) )
			{
				continue;
			}

			cutout.Reshape( loop );
			moved = true;
		}

		return moved;
	}

	static bool Carrying( ArchPorchPart porch, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		var moved = false;

		foreach ( var leg in porch.Legs )
		{
			var min = leg.Min;
			var max = leg.Max;

			if ( !Box( ref min, ref max, reaches, landed ) )
			{
				continue;
			}

			leg.Min = min;
			leg.Max = max;
			moved = true;
		}

		foreach ( var stair in porch.Stairs )
		{
			moved |= Shaft( stair, reaches, landed );
		}

		foreach ( var pillar in porch.Pillars.Where( pillar => reaches( pillar.Origin ) ) )
		{
			pillar.Origin = landed( pillar.Origin );
			moved = true;
		}

		foreach ( var trim in porch.Trims )
		{
			moved |= Path( trim.Path, reaches, landed );
		}

		return moved;
	}

	// A reached corner TRANSLATES the shaft, the way a box fitting travels: a stair is a rectangle, so pulling
	// one of its corners on its own would only be the shape coming apart rather than being edited.
	static bool Shaft( ArchStairPart stair, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		if ( stair?.Core is not { } core )
		{
			return false;
		}

		foreach ( var corner in core.Outline() )
		{
			if ( !reaches( corner ) )
			{
				continue;
			}

			core.Origin += landed( corner ) - corner;

			return true;
		}

		return false;
	}

	// A reached box corner TRANSLATES the box, because a bracket or a porch leg is a fitting standing at the
	// corner rather than a shape whose corner was authored there.
	static bool Box( ref Vector2 min, ref Vector2 max, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		foreach ( var corner in new[] { min, max, new Vector2( min.x, max.y ), new Vector2( max.x, min.y ) } )
		{
			if ( !reaches( corner ) )
			{
				continue;
			}

			var shift = landed( corner ) - corner;

			min += shift;
			max += shift;

			return true;
		}

		return false;
	}

	static bool Ends( IEnumerable<ArchWall> walls, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		var moved = false;

		foreach ( var wall in walls )
		{
			if ( reaches( wall.Start ) )
			{
				wall.Start = landed( wall.Start );
				moved = true;
			}

			if ( reaches( wall.End ) )
			{
				wall.End = landed( wall.End );
				moved = true;
			}
		}

		return moved;
	}

	static bool Loop( List<Vector2> loop, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		var moved = false;

		for ( var index = 0; index < loop.Count; index++ )
		{
			if ( !reaches( loop[index] ) )
			{
				continue;
			}

			loop[index] = landed( loop[index] );
			moved = true;
		}

		return moved;
	}

	static bool Path( List<Vector3> path, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		var moved = false;

		for ( var index = 0; index < path.Count; index++ )
		{
			var flat = new Vector2( path[index].x, path[index].y );

			if ( !reaches( flat ) )
			{
				continue;
			}

			var to = landed( flat );

			path[index] = new Vector3( to.x, to.y, path[index].z );
			moved = true;
		}

		return moved;
	}

	static bool Nodes( List<ArchCurveNode> nodes, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed )
	{
		var moved = false;

		foreach ( var node in nodes )
		{
			var flat = new Vector2( node.Position.x, node.Position.y );

			if ( !reaches( flat ) )
			{
				continue;
			}

			var to = landed( flat );

			node.Position = new Vector3( to.x, to.y, node.Position.z );
			moved = true;
		}

		return moved;
	}
}