Editor/Wall/ArchTrimAffector.cs
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// A run that follows another layer's shape is an affector on nothing, but it is a CONSUMER of one - so
// it obeys the same rule: its path is re-derived from where its host stands NOW, on every commit,
// through the one shape answer the host's own generator reads. Drag the cut and the run goes round it.
// A host that stops resolving lets the run keep the path it last had, because a run vanishing because a
// neighbour was deleted is worse than one left standing where it was.
public static class ArchTrimAffector {
	public static int Resolve( ArchPlan plan, ArchKit kit ) {
		var kinds = ArchKinds.Load();
		var followed = 0;

		foreach ( var room in plan.AllRooms() ) {
			foreach ( var trim in plan.Filed<ArchTrimPart>( room, kinds ).Where( trim => trim.Follows ) ) {
				if ( Refollow( plan, trim ) ) {
					followed++;
				}
			}
		}

		return followed;
	}

	public static bool Refollow( ArchPlan plan, ArchTrimPart trim ) {
		var loops = ArchLayerShape.Loops( plan, trim.FollowsId );

		if ( loops.Count == 0 ) {
			return false;
		}

		var loop = loops[System.Math.Clamp( trim.FollowsLoop, 0, loops.Count - 1 )];
		var height = ArchLayerShape.HeightOf( plan, trim.FollowsId, 0f ) + trim.Lift;
		var path = loop.Select( corner => new Vector3( corner.x, corner.y, height ) ).ToList();

		if ( Same( trim.Path, path ) ) {
			return false;
		}

		trim.Path = path;
		trim.Closed = true;

		return true;
	}

	// Rewriting an unchanged path every commit would make every rebuild look like an edit to undo.
	static bool Same( IReadOnlyList<Vector3> standing, IReadOnlyList<Vector3> wanted ) {
		if ( standing.Count != wanted.Count ) {
			return false;
		}

		for ( var index = 0; index < wanted.Count; index++ ) {
			if ( (standing[index] - wanted[index]).Length > 0.05f ) {
				return false;
			}
		}

		return true;
	}
}