🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (598 total matches found)
sunless.lib_architecture / Editor/Carve/ArchCut.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// One resolution for ghost, generator and report; a stepped cut's band travels its whole chain.
public static class ArchCut
{
	public const float MinRun = 18f;

	public static Vector2 WorldBand( ArchCutSegment segment, float lift )
	{
		return new Vector2( segment.BaseHeight + lift, segment.TopHeight + lift );
	}

	// Held for the build: every host a shaft reaches asked for the same volumes, and each ask walked the whole
	// chain and built a footprint per step.
	public static IReadOnlyList<ArchCarveVolume> Resolve( ArchCutPart cut, ArchKit kit )
	{
		return ArchBuildMemo.Held( memo => memo.Volumes, cut?.Id ?? 0, () => Resolved( cut, kit ).ToList() );
	}

	static IEnumerable<ArchCarveVolume> Resolved( ArchCutPart cut, ArchKit kit )
	{
		if ( cut is not { HasContent: true } )
		{
			yield break;
		}

		if ( cut.IsDamage )
		{
			foreach ( var volume in ArchDamage.Resolve( cut, kit ) )
			{
				yield return volume;
			}

			yield break;
		}

		var legs = cut.Segments.Where( segment => segment.HasLoop || segment.Length > 1f ).ToList();
		var breaking = Breaking( cut, kit );

		if ( cut.Profile != CutProfile.Steps )
		{
			foreach ( var leg in legs )
			{
				yield return Bite( cut, leg ).Breaking( breaking );
			}

			yield break;
		}

		var going = cut.StepGoing > 0.5f ? cut.StepGoing : kit.StepGoing;
		var rise = cut.StepRise > 0.5f ? cut.StepRise : kit.StepRise;

		foreach ( var leg in legs.Where( leg => !leg.HasLoop ) )
		{
			// The hole starts on the leg's own band, so it agrees with the walls round it.
			var climbed = leg.BaseHeight;
			var steps = Math.Max( 1, (int)MathF.Round( leg.Length / MathF.Max( 1f, going ) ) );
			var tread = leg.Length / steps;
			var axes = leg.Axes;
			var half = MathF.Max( 1f, leg.Width ) * 0.5f;

			for ( var step = 0; step < steps; step++ )
			{
				var footprint = axes.Rect( step * tread, (step + 1) * tread, -half, half );

				climbed += rise;

				yield return ArchCarveVolume.Over( footprint, climbed, leg.TopHeight ).Breaking( breaking );
			}
		}
	}

	// A box, or the wedge a ramped cut takes: its floor is the surface it leaves behind, so the head of the band
	// loses nothing and the foot loses all of it. Through ArchRamp, so the void the ghost draws is the void the
	// carve takes and no second slope is derived anywhere.
	public static ArchCarveVolume Bite( ArchCutPart cut, ArchCutSegment leg )
	{
		return ArchRamp.Rakes( cut )
			? ArchCarveVolume.Above( leg.Outline(), Floor( cut, leg ), leg.TopHeight )
			: ArchCarveVolume.Over( leg.Outline(), leg.BaseHeight, leg.TopHeight );
	}

	// The plane the bite leaves standing under it - read by the generator, the ghost, the handles and the section.
	public static ArchCarvePlane Floor( ArchCutPart cut, ArchCutSegment leg )
	{
		return ArchRamp.Rakes( cut )
			? ArchRamp.Deck( cut, leg.Outline(), leg.TopHeight, leg.TopHeight - leg.BaseHeight )
			: ArchCarvePlane.Level( leg.BaseHeight );
	}

	// The low end of a ramped bite, the way ArchRamp.Foot answers for a platform: the height the foot handle
	// stands at, and the one the fall is dragged by.
	public static float Toe( ArchCutPart cut, ArchCutSegment leg )
	{
		return leg.TopHeight - ArchRamp.Fall( cut, leg.TopHeight - leg.BaseHeight );
	}

	// Seeded from the cut's own id: a ruin that reshuffled itself on every hotload could not be reviewed, and
	// every emitted vertex is folded into the incremental build's key.
	static ArchCarveBreak Breaking( ArchCutPart cut, ArchKit kit )
	{
		return cut.BreakEdges ? new ArchCarveBreak { Seed = cut.Id, Jitter = kit?.BreakJitter ?? 0f } : default;
	}

	// Only legs whose band reaches this slab open it - the storey between stays whole.
	// Derived, never stored, as ArchFloorCutout - its arbitrary Loop already carries angled holes.
	// World-space: one shaft opens every slab it overlaps in every building it is allowed to reach.
	public static bool Affects( ArchCutPart cut, ArchCutAffects target ) => (cut.Affects & target) == target;

	// The wells a storey loses because the layer that pierced them is still enabled. Slabs, ceilings,
	// foundations and roofs all gather this the same way, so disabling a flight fills its stairwell back
	// in without the plan losing the well it would restore.
	public static IEnumerable<ArchFloorCutout> Stored( ArchBuilding building, int level )
	{
		return building.Cutouts.Where( cutout => cutout.Level == level && ArchLayerGate.Owned( cutout.OwnerId ) );
	}

	public static IEnumerable<ArchFloorCutout> Holes(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target = ArchCutAffects.Floors,
		int standing = 0 )
	{
		foreach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, target, standing ) )
		{
			var hole = new ArchFloorCutout { Level = level, Name = cut.Name, Break = volume.Break };

			hole.Reshape( volume.Footprint );

			yield return hole;
		}
	}

	// The loops a cut opened RIGHT THROUGH a slab and says it wants guarded. An edge a boolean left in a floor is
	// a drop exactly as the edge of a stairwell is, so the ring round the well walks these beside its own and every
	// rule it already keeps - the mouth left open, an edge against a wall left bare, the flight's own rail owning
	// what it reaches - applies to them unchanged.
	public static IEnumerable<List<Vector2>> Guarded( ArchPlan plan, int level, ArchKit kit, float bottom, float top, int hostId )
	{
		var low = MathF.Min( bottom, top );
		var high = MathF.Max( bottom, top );

		foreach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, ArchCutAffects.Floors ) )
		{
			if ( cut.GuardsOpenedEdges && Pierces( volume, low, high ) )
			{
				yield return volume.Footprint.ToList();
			}
		}
	}

	// The same shafts a slab loses, handed to a solid that carves in three dimensions rather than in plan.
	// A roof asks through here so the deck and the ceiling under it read one list.
	public static IEnumerable<ArchCarveVolume> Volumes(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0 )
	{
		return Reaching( plan, level, kit, bottom, top, hostId, target, standing ).Select( found => found.Volume );
	}

	public static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Damage(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0 )
	{
		return Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Damage );
	}

	// The zones asking their hosts to stand work PROUD rather than to break or to be carved. Same walk, same
	// reach and same order-of-operations answer - only the mode differs.
	public static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Extrusions(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0 )
	{
		return Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Extrude );
	}

	static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Reaching(
		ArchPlan plan,
		int level,
		ArchKit kit,
		float bottom,
		float top,
		int hostId,
		ArchCutAffects target,
		int standing = 0,
		ArchZoneMode mode = ArchZoneMode.Carve )
	{
		if ( plan is null )
		{
			yield break;
		}

		var low = MathF.Min( bottom, top );
		var high = MathF.Max( bottom, top );

		foreach ( var cut in Cuts( plan, level, hostId, target, standing, mode ) )
		{
			foreach ( var volume in Resolve( cut, kit ) )
			{
				if ( Reaches( volume, low, high ) )
				{
					yield return (cut, volume);
				}
			}
		}
	}

	// A cut opens the slabs of whatever its group holds, and of every building when it stands in none.
	// A disabled one is still authored and still selectable - it just stops being read.
	//
	// It also only reaches what was STANDING when it was made: order of operations is not a boolean feature,
	// it is how the whole stack evaluates, so the one answer to that lives in ArchLayerOrder and is asked
	// here - the single place a cut chooses what it may touch.
	static IEnumerable<ArchCutPart> Cuts( ArchPlan plan, int level, int hostId, ArchCutAffects target, int standing = 0, ArchZoneMode mode = ArchZoneMode.Carve )
	{
		foreach ( var building in plan.Buildings )
		{
			foreach ( var cut in Filed( plan, building.Cuts, building.Id, level, hostId, target, standing, mode ) )
			{
				yield return cut;
			}
		}

		// A road holds its own, so a bore and a carriageway are opened by the algebra a slab is.
		foreach ( var road in plan.Roads() )
		{
			foreach ( var cut in Filed( plan, road.Cuts, road.Id, level, hostId, target, standing, mode ) )
			{
				yield return cut;
			}
		}
	}

	static IEnumerable<ArchCutPart> Filed(
		ArchPlan plan,
		IReadOnlyList<ArchCutPart> cuts,
		int ownerId,
		int level,
		int hostId,
		ArchCutAffects target,
		int standing,
		ArchZoneMode mode )
	{
		foreach ( var cut in cuts )
		{
			if ( cut.Mode != mode || cut.Level > level || !cut.HasContent || !ArchLayerGate.On( cut ) || !Affects( cut, target ) )
			{
				continue;
			}

			if ( !ArchLayerOrder.Applies( plan, cut.Id, standing ) )
			{
				continue;
			}

			var reach = ArchLayerGroups.Reach( plan, cut.Id, ownerId );

			if ( hostId == 0 || reach is null || reach.Contains( hostId ) )
			{
				yield return cut;
			}
		}
	}

	// At the corners: a raked band reaches one end of a pitched deck long before the other.
	public static bool Reaches( ArchCarveVolume volume, float low, float high )
	{
		var floor = float.MaxValue;
		var ceiling = float.MinValue;

		foreach ( var corner in volume.Footprint )
		{
			floor = MathF.Min( floor, volume.Floor.At( corner ) );
			ceiling = MathF.Max( ceiling, volume.Ceiling.At( corner ) );
		}

		return ceiling > low + ArchCarve.Grain && floor < high - ArchCarve.Grain;
	}

	// A volume spanning the whole band it reaches takes that body out entirely - a well, and a well is
	// what gets an edge dressed. One that stops inside leaves material over or under it, which is a
	// recess, and a recess is trimmed by nothing: its faces ARE the body it was taken out of.
	public static bool Pierces( ArchCarveVolume volume, float low, float high )
	{
		var floor = float.MinValue;
		var ceiling = float.MaxValue;

		foreach ( var corner in volume.Footprint )
		{
			floor = MathF.Max( floor, volume.Floor.At( corner ) );
			ceiling = MathF.Min( ceiling, volume.Ceiling.At( corner ) );
		}

		return floor <= low + ArchCarve.Grain && ceiling >= high - ArchCarve.Grain;
	}

	public static IEnumerable<(float From, float To)> Outside(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		Vector2 from,
		Vector2 to,
		float bottom,
		float top,
		ArchCutAffects target = ArchCutAffects.WallFittings,
		int standing = 0 )
	{
		var blocked = new List<(float From, float To)>();

		if ( plan is not null && (to - from).Length > ArchCarve.Grain )
		{
			foreach ( var cut in Cuts( plan, level, hostId, target, standing ) )
			{
				foreach ( var volume in Resolve( cut, kit ).Where( volume => Reaches( volume, bottom, top ) ) )
				{
					blocked.AddRange( ArchFootprint.Inside( volume.Footprint, from, to ) );
				}
			}
		}

		var marks = new List<float> { 0f, 1f };

		foreach ( var range in blocked )
		{
			marks.Add( Math.Clamp( range.From, 0f, 1f ) );
			marks.Add( Math.Clamp( range.To, 0f, 1f ) );
		}

		marks = marks.Distinct().OrderBy( value => value ).ToList();

		for ( var index = 0; index + 1 < marks.Count; index++ )
		{
			var start = marks[index];
			var finish = marks[index + 1];
			var middle = (start + finish) * 0.5f;

			if ( finish - start > 0.001f && !blocked.Any( range => middle > range.From && middle < range.To ) )
			{
				yield return (start, finish);
			}
		}
	}

	public static IEnumerable<List<Vector3>> OutsidePath(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		IReadOnlyList<Vector3> path,
		ArchCutAffects target )
	{
		var runs = new List<List<Vector3>>();

		for ( var index = 0; index + 1 < (path?.Count ?? 0); index++ )
		{
			var from = path[index];
			var to = path[index + 1];
			var flatFrom = new Vector2( from.x, from.y );
			var flatTo = new Vector2( to.x, to.y );

			var spans = Outside( plan, kit, level, hostId, flatFrom, flatTo, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), target ).ToList();

			foreach ( var span in spans )
			{
				var start = Vector3.Lerp( from, to, span.From );
				var finish = Vector3.Lerp( from, to, span.To );
				var current = runs.LastOrDefault();

				if ( current is null || (current[^1] - start).Length > 0.05f )
				{
					current = new List<Vector3> { start };
					runs.Add( current );
				}

				if ( (current[^1] - finish).Length > 0.01f )
				{
					current.Add( finish );
				}
			}

			if ( spans.Count == 0 || spans.Any( span => span.From > 0.001f || span.To < 0.999f ) )
			{
				runs.Add( null );
			}
		}

		return runs.Where( run => run is { Count: >= 2 } );
	}

	// The one cut of a LEVEL run: a ring is closed before it is cut so its seam edge can break like any
	// other, then each surviving stretch is normalized (closure detected, duplicated closing point
	// dropped) and handed back as a run. Gutters, fascia, soffits, parapets and closures all read this
	// answer, so a hole reads the same to every band that crosses it.
	public static IEnumerable<ArchRunPath> Runs(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		ArchRunPath run,
		ArchCutAffects target )
	{
		var path = run.Raised();

		if ( run.Closed && path.Count >= 3 )
		{
			path.Add( path[0] );
		}

		foreach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) )
		{
			yield return ArchRunPath.Normalized( surviving, run.Height );
		}
	}

	// The 3D form of the same cut: a trim climbs its host's jambs, so its surviving stretches keep
	// their own per-point heights and only the closure is answered here. The author closes the ring
	// when it is one - ArchTrimFollow.Walked does - so no pre-close is asked for.
	public static IEnumerable<(List<Vector3> Points, bool Closed)> Surviving(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		IReadOnlyList<Vector3> path,
		ArchCutAffects target )
	{
		foreach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) )
		{
			var closed = ArchRunPath.IsClosed( surviving );

			yield return (closed ? surviving.Take( surviving.Count - 1 ).ToList() : surviving, closed);
		}
	}

	// Is one POINT inside a cut - the question a fitting too small to have a run of its own asks, like the
	// block that takes a ridge's corner. Its band is the fitting's own, so a cut passing under it leaves it.
	public static bool Covers( ArchPlan plan, ArchKit kit, int level, int hostId, Vector3 from, Vector3 to, ArchCutAffects target )
	{
		var at = new Vector2( (from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f );

		return Volumes( plan, level, kit, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), hostId, target )
			.Any( volume => volume.Covers( at ) );
	}

	// Asked by the solid's own generator, so nothing is told in advance it will be cut.
	public static IEnumerable<ArchCutPart> Over(
		ArchPlan plan,
		int level,
		IReadOnlyList<Vector2> outline,
		int hostId,
		ArchCutAffects target = ArchCutAffects.Platforms,
		int standing = 0 )
	{
		if ( plan is null || outline is not { Count: >= 3 } )
		{
			yield break;
		}

		foreach ( var cut in Cuts( plan, level, hostId, target, standing ) )
		{
			if ( cut.Outlines().Any( loop => ArchFootprint.Overlaps( loop, outline ) ) )
			{
				yield return cut;
			}
		}
	}

	// Each leg continues the last, lifted by the chain's Rise - one part can wind round a tower.
	// The first leg has nothing to continue from - it takes the band it was given.
	public static ArchCutSegment Extend( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight )
	{
		var segment = Next( cut, from, to, width, baseHeight, topHeight );

		if ( segment is null )
		{
			return null;
		}

		cut.Segments.Add( segment );

		return segment;
	}

	// Ghost and commit both go through here - the leg you are shown is the leg you get.
	public static ArchCutSegment Next( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool? snapAngle = null )
	{
		var last = cut?.Segments.Count > 0 ? cut.Segments[^1] : null;
		var lift = last is null ? 0f : cut.Rise;

		return Sketch(
			last?.End ?? from,
			to,
			width,
			(last?.BaseHeight ?? baseHeight) + lift,
			(last?.TopHeight ?? topHeight) + lift,
			snapAngle ?? cut?.SnapAngle ?? true );
	}

	// A LOOP leg is one shape described twice - the loop the carve takes, and the run the handles, the ghost and
	// the carve frame read off Start/End. So the run is DERIVED from the loop: centred on it, along the yaw it
	// already stands at, measured to its own extremes. Author the two separately and every drag drifts them a
	// rounding further apart, until the widget, the preview and the geometry are three different shapes.
	public static void Fit( ArchCutSegment segment, float yaw )
	{
		if ( segment is null || !segment.HasLoop )
		{
			return;
		}

		var axes = new ArchStairAxes { Yaw = yaw };
		var along = axes.Along;
		var centre = segment.Loop.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / segment.Loop.Count;
		var reach = segment.Loop.Select( point => Vector2.Dot( point - centre, along ) ).ToList();

		segment.Start = centre + along * reach.Min();
		segment.End = centre + along * reach.Max();
	}

	// Rigid: the loop and the run travel together and neither is re-derived, so a move cannot turn or stretch it.
	public static void Shift( ArchCutSegment segment, Vector2 by )
	{
		if ( segment is null || by.Length < ArchGridService.FinestSize )
		{
			return;
		}

		segment.Start += by;
		segment.End += by;

		if ( segment.HasLoop )
		{
			segment.Loop = segment.Loop.Select( point => point + by ).ToList();
		}
	}

	// The loop swings and the run is refitted to where it landed, so the yaw the frame reads is the yaw the
	// shape actually stands at. The SWING is what lands on the ladder, not the angle it arrives at: rounding the
	// total dragged a shape drawn at seven degrees onto the nearest fifteen the moment the ring was touched, and
	// an edit is supposed to leave where a shape already stands alone. The handle has already ratcheted, so this
	// is idempotent for a stepped drag and only guards a caller that has not.
	public static void Turn( ArchCutSegment segment, Vector2 about, float degrees, bool snapAngle )
	{
		if ( segment is null )
		{
			return;
		}

		var swing = snapAngle ? ArchGridService.Snap( degrees, ArchGridService.AngleStep ) : degrees;
		var wanted = segment.Yaw + swing;

		if ( MathF.Abs( swing ) < 0.001f )
		{
			return;
		}

		if ( segment.HasLoop )
		{
			segment.Loop = ArchFootprint.Turned( segment.Loop, about, swing );
			Fit( segment, wanted );

			return;
		}

		var ends = ArchFootprint.Turned( new[] { segment.Start, segment.End }, about, swing );

		segment.Start = ends[0];
		segment.End = ends[1];
	}

	// Redrawn through Sketch, so a handled leg is one the tool would have let you draw.
	public static bool Reshape( ArchCutSegment segment, Vector2 from, Vector2 to, bool snapAngle = true )
	{
		// A loop leg's run is the loop's, not the drag's - re-sketching it here is what let the two disagree.
		if ( segment is { HasLoop: true } )
		{
			Shift( segment, from - segment.Start );

			return true;
		}

		if ( segment is null || Sketch( from, to, segment.Width, segment.BaseHeight, segment.TopHeight, snapAngle ) is not { } redrawn )
		{
			return false;
		}

		segment.Start = redrawn.Start;
		segment.End = redrawn.End;

		return true;
	}

	// The chain invariant - each leg keeps its own vector, so this slides the chain after edits.
	public static void Relink( ArchCutPart cut )
	{
		if ( cut is null )
		{
			return;
		}

		for ( var index = 1; index < cut.Segments.Count; index++ )
		{
			var previous = cut.Segments[index - 1];
			var leg = cut.Segments[index];

			Reshape( leg, previous.End, previous.End + (leg.End - leg.Start), cut.SnapAngle );
		}
	}

	public static ArchCutSegment Sketch( Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool snapAngle = true )
	{
		var span = to - from;

		if ( span.Length < MinRun )
		{
			return null;
		}

		// Whole degrees stay clean; 15 degree steps are the ladder unless the part says otherwise.
		var yaw = ArchGridService.Snap( MathF.Atan2( span.y, span.x ).RadianToDegree(), snapAngle ? ArchGridService.AngleStep : 1f ).DegreeToRadian();
		var along = new Vector2( MathF.Cos( yaw ), MathF.Sin( yaw ) );

		return new ArchCutSegment
		{
			Start = from,
			End = from + along * ArchGridService.Fine( Vector2.Dot( span, along ) ),
			Width = MathF.Max( 1f, width ),
			BaseHeight = MathF.Min( baseHeight, topHeight ),
			TopHeight = MathF.Max( baseHeight, topHeight )
		};
	}
}
sunless.lib_architecture / Editor/Bool/ArchBoolUi.cs
Editor library
using System;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// Read by the Boolean Modifiers subtool and by Select, off the same records - a draft and a placed shape
// carry identical controls because they ARE the same records. Neither a platform nor a cut had a property
// sheet before this: selecting one showed an empty shelf.
public static class ArchBoolUi
{
	static bool AffectsExpanded
	{
		get => EditorCookie.Get( "arch.bool.affects.expanded", false );
		set => EditorCookie.Set( "arch.bool.affects.expanded", value );
	}

	// A tick that GATES the fields under it has to rebuild the sheet as well as commit, or the fields it reveals
	// never arrive and the ones it hides stay put. Handing these only `changed` is why ticking the edge dressing
	// did nothing you could see, and why the coping numbers could not be reached at all.
	static Action Both( Action changed, Action refresh )
	{
		return () =>
		{
			changed?.Invoke();
			refresh?.Invoke();
		};
	}

	public static void Platform( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed )
	{
		var slab = panel.AddGroup( "Solid" );

		slab.Add( ArchPartUi.Number( platform.Ramp ? "Head" : "Rise", platform.Rise, 24f,
			value => platform.TopHeight = platform.GradeHeight + MathF.Max( 4f, value ), changed ) );

		slab.Add( ArchPartUi.Check( "Ramp the deck", platform.Ramp, value => platform.Ramp = value, Both( changed, refresh ) ) );

		if ( platform.Ramp )
		{
			Ramp( panel, platform, "rise", changed );

			return;
		}

		Guard( panel, platform, refresh, changed );
		Coping( panel, platform, refresh, changed );
	}

	// The two groups the placement tool asks for as well, where the rise is the seed's own and the rake is the
	// kind already chosen - so it reads these rather than the whole sheet, and a change lands in one place.
	public static void Guard( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed )
	{
		var guard = panel.AddGroup( "Guardrail" );

		guard.Add( ArchPartUi.Check( "Guard rail round the edge", platform.Guardrail, value => platform.Guardrail = value, Both( changed, refresh ) ) );

		if ( !platform.Guardrail )
		{
			return;
		}

		guard.Add( ArchPartUi.Number( "Height", platform.GuardHeight, 42f,
			value => platform.GuardHeight = MathF.Max( 12f, value ), changed ) );

		ArchBarrierUi.GuardStyles( guard, platform.GuardStyle, style =>
		{
			platform.GuardStyle = style;
			changed?.Invoke();
		} );
	}

	public static void Coping( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed )
	{
		var coping = panel.AddGroup( "Coping" );

		coping.Add( ArchPartUi.Check( "Coping band", platform.Coping, value => platform.Coping = value, Both( changed, refresh ) ) );

		if ( !platform.Coping )
		{
			return;
		}

		coping.Add( ArchPartUi.Number( "Height", platform.CopingHeight, 4f, value => platform.CopingHeight = value, changed ) );
		coping.Add( ArchPartUi.Number( "Width", platform.CopingWidth, 8f, value => platform.CopingWidth = value, changed ) );
		coping.Add( ArchPartUi.Number( "Oversail", platform.CopingOversail, 1.5f, value => platform.CopingOversail = value, changed ) );
		coping.Add( ArchPartUi.Check( "Round the inside edges too", platform.CopingInside, value => platform.CopingInside = value, changed ) );
	}

	// Which way it climbs and how far it drops getting to its foot. The gradient is never authored - it falls
	// out of the run the footprint has - so a ramp dragged longer is a gentler one with no second edit. Shared
	// by the platform that rakes its top and the cut that rakes the floor it leaves: same three numbers, one form.
	// A coping is not offered: a run is level, and a raked deck has no one height to walk a band round.
	public static void Ramp( ToolSidebarWidget panel, IArchRamped part, string shelf, Action changed )
	{
		var ramp = panel.AddGroup( "Ramp" );

		ramp.Add( ArchPartUi.Number( "Climb angle", part.RampYaw, 0f,
			value => part.RampYaw = ArchShapeHandles.Facing( value ), changed ) );

		ramp.Add( ArchPartUi.Number( "Fall", part.RampFall, 0f,
			value => part.RampFall = MathF.Max( 0f, value ), changed ) );

		ramp.Add( ArchPartUi.Wrapped( $"Zero fall is the whole {shelf}, so the foot meets the bottom of the band. Type one to leave a level shelf." ) );
	}

	// One member or a field of them, and the field's own numbers. The drop is measured DOWN from the plane
	// it hangs on, so a deeper beam grows into the room rather than up through the ceiling.
	public static void Beam( ToolSidebarWidget panel, ArchBeamPart beam, Action refresh, Action changed )
	{
		using ( var grid = ArchIconGrid.In( panel.AddGroup( "Members" ) ) )
		{
			grid.Pick( "One member — exactly the shape that was dragged", "beam_one", "horizontal_rule", beam.Members == BeamMembers.One,
				() => { beam.Members = BeamMembers.One; changed?.Invoke(); refresh?.Invoke(); } );

			grid.Pick( "Filled — that same shape repeated as slats", "beam_field", "view_stream", beam.Members == BeamMembers.Field,
				() => { beam.Members = BeamMembers.Field; changed?.Invoke(); refresh?.Invoke(); } );
		}

		Section( panel, beam, beam.Members == BeamMembers.Field, changed );
	}

	// Read by the placement tool too, where the Members picker would only repeat the kind already chosen.
	public static void Section( ToolSidebarWidget panel, ArchBeamPart beam, bool field, Action changed )
	{
		var section = panel.AddGroup( "Section" );

		section.Add( ArchPartUi.Number( "Drop", beam.Drop, 12f, value => beam.Drop = MathF.Max( 1f, value ), changed ) );

		if ( !field )
		{
			return;
		}

		section.Add( ArchPartUi.Number( "Member width", beam.MemberWidth, 6f, value => beam.MemberWidth = MathF.Max( 1f, value ), changed ) );
		section.Add( ArchPartUi.Number( "Gap", beam.MemberGap, 10f, value => beam.MemberGap = MathF.Max( 0f, value ), changed ) );
		section.Add( ArchPartUi.Number( "Angle", beam.Yaw, 0f, value => beam.Yaw = value, changed ) );
		section.Add( ArchPartUi.Integer( "Sides", beam.Sides, 0, value => beam.Sides = Math.Max( 0, value ), changed ) );
	}

	public static void Damage( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed )
	{
		using ( var grid = ArchIconGrid.In( panel.AddGroup( "Damage type" ) ) )
		{
			Damage( grid, cut, ArchDamageKind.Masonry, "Masonry — staggered exposed bricks at wall and pillar corners", "damage_masonry", "view_module", refresh, changed );
			Damage( grid, cut, ArchDamageKind.SurfaceSpall, "Surface spall — a shallow chipped region", "damage_spall", "texture", refresh, changed );
			Damage( grid, cut, ArchDamageKind.MissingPanels, "Missing panels — selected ceiling cells are absent", "damage_missing_panels", "grid_off", refresh, changed );
			Damage( grid, cut, ArchDamageKind.DisplacedPanels, "Displaced panels — selected ceiling cells hang dropped and tilted", "damage_displaced_panels", "view_quilt", refresh, changed );
			Damage( grid, cut, ArchDamageKind.MixedPanels, "Mixed panels — selected ceiling cells are missing or displaced", "damage_mixed_panels", "dashboard", refresh, changed );
		}

		var pattern = panel.AddGroup( "Pattern" );

		if ( cut.ResolvedDamage == ArchDamageKind.Masonry )
		{
			MasonryPresets( pattern, cut, refresh, changed );

			ArchSidebarLayout.Form( pattern, nested =>
				ArchSidebarSection.Disclosure( nested, "Damage.masonry.advanced", "Advanced", true, advanced =>
				{
					advanced.Add( ArchPartUi.Number( "Course coverage", cut.DamageAmount, 1f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );
					advanced.Add( ArchPartUi.Number( "Brick width", cut.DamageCellWidth, 16f, value => cut.DamageCellWidth = MathF.Max( 2f, value ), changed ) );
					advanced.Add( ArchPartUi.Number( "Course height", cut.DamageCellLength, 8f, value => cut.DamageCellLength = MathF.Max( 2f, value ), changed ) );
					advanced.Add( ArchPartUi.Number( "Reveal depth", cut.DamageDepth, 6f, value => cut.DamageDepth = MathF.Max( 0.25f, value ), changed ) );
					advanced.Add( ArchPartUi.Check( "Carve wall finish", cut.MasonryCarves, value => cut.MasonryCarves = value, changed ) );
				} ) );
		}
		else if ( ArchDamage.Panels( cut.ResolvedDamage ) )
		{
			pattern.Add( ArchPartUi.Number( "Amount", cut.DamageAmount, 0.55f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );
			pattern.Add( ArchPartUi.Number( "Panel width", cut.DamageCellWidth, 24f, value => cut.DamageCellWidth = MathF.Max( 4f, value ), changed ) );
			pattern.Add( ArchPartUi.Number( "Panel length", cut.DamageCellLength, 48f, value => cut.DamageCellLength = MathF.Max( 4f, value ), changed ) );

			if ( cut.ResolvedDamage is ArchDamageKind.DisplacedPanels or ArchDamageKind.MixedPanels )
			{
				pattern.Add( ArchPartUi.Number( "Drop", cut.DamageDrop, 4f, value => cut.DamageDrop = MathF.Max( 0f, value ), changed ) );
				pattern.Add( ArchPartUi.Number( "Tilt", cut.DamageTilt, 12f, value => cut.DamageTilt = MathF.Max( 0f, value ), changed ) );
			}
		}
		else
		{
			pattern.Add( ArchPartUi.Number( "Amount", cut.DamageAmount, 0.55f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );
			pattern.Add( ArchPartUi.Number( "Damage depth", cut.DamageDepth, 4f, value => cut.DamageDepth = MathF.Max( 0.5f, value ), changed ) );
		}

		Affects( panel, cut, refresh, changed, ArchDamage.Targets( cut.ResolvedDamage ) );
	}

	// The proud sibling of the damage sheet. There is no Affects group: an extrude zone stands work on a wall face
	// and a wall face is the only host that can answer it, so offering the mask would be offering a way to break it.
	// The kind row is the caller's where the caller already asked for it: the placement tool answers it in its own
	// step, and two grids of one choice light independently.
	public static void Extrude( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed, bool kinds = true )
	{
		if ( kinds )
		{
			using var grid = ArchIconGrid.In( panel.AddGroup( "Extrude" ) );

			Extrude( grid, cut, ArchExtrudeKind.Face, refresh, changed );
			Extrude( grid, cut, ArchExtrudeKind.Quoins, refresh, changed );
			Extrude( grid, cut, ArchExtrudeKind.Courses, refresh, changed );
			Extrude( grid, cut, ArchExtrudeKind.Indent, refresh, changed );
			Extrude( grid, cut, ArchExtrudeKind.Pier, refresh, changed );
		}

		var recessing = cut.Extrude == ArchExtrudeKind.Indent;
		var standing = ArchExtrude.Stands( cut.Extrude );
		var block = panel.AddGroup( recessing ? "Recess" : standing ? "Pier" : "Block" );

		block.Add( ArchPartUi.Number( recessing ? "Depth" : "Proud", cut.ExtrudeDepth, 4f,
			value => cut.ExtrudeDepth = MathF.Max( ArchExtrude.LeastDepth, value ), changed ) );

		// A recess has no far side to offer: on both faces it would be a hole, which is Subtract's job.
		if ( !recessing )
		{
			block.Add( ArchPartUi.Check( "Both faces", cut.ExtrudeBothFaces, value => cut.ExtrudeBothFaces = value, changed ) );
		}

		if ( standing )
		{
			using ( var founding = ArchIconGrid.In( panel.AddGroup( "Footing" ) ) )
			{
				Footing( founding, cut, null, "Type's own — whatever the pillar type authored", "footing_type", "block", refresh, changed );
				Footing( founding, cut, PillarFooting.None, "None — the shaft runs straight into what it stands on", "footing_none", "remove", refresh, changed );
				Footing( founding, cut, PillarFooting.Square, "Square — a plain pad, buried", "footing_square", "crop_square", refresh, changed );
				Footing( founding, cut, PillarFooting.Bevelled, "Bevelled — a splayed pad, buried", "footing_bevelled", "change_history", refresh, changed );
				Footing( founding, cut, PillarFooting.Stepped, "Stepped — two courses stepping out, buried", "footing_stepped", "stairs", refresh, changed );
			}

			// Everything else about the column - footing, plinth, banded shaft, capital - is the TYPE's, so this is
			// the only other choice the zone makes about it.
			ArchAsks.PillarKinds( panel, ArchAsks.PillarTypes(), cut.ExtrudePillarType, type =>
			{
				cut.ExtrudePillarType = type.Name;
				changed?.Invoke();
				refresh?.Invoke();
			} );

			return;
		}

		if ( ArchExtrude.Coursed( cut.Extrude ) )
		{
			block.Add( ArchPartUi.Number( "Course height", cut.ExtrudeCourse, 16f, value => cut.ExtrudeCourse = MathF.Max( 0f, value ), changed ) );
			block.Add( ArchPartUi.Number( "Joint", cut.ExtrudeJoint, 0f, value => cut.ExtrudeJoint = MathF.Max( 0f, value ), changed ) );
		}
	}

	static void Footing( ArchIconGrid grid, ArchCutPart cut, PillarFooting? founding, string tooltip, string slug, string glyph, Action refresh, Action changed )
	{
		grid.Pick( tooltip, slug, glyph, cut.ExtrudeFooting == founding, () =>
		{
			cut.ExtrudeFooting = founding;
			changed?.Invoke();
			refresh?.Invoke();
		} );
	}

	static void Extrude( ArchIconGrid grid, ArchCutPart cut, ArchExtrudeKind kind, Action refresh, Action changed )
	{
		grid.Pick( ArchExtrude.Label( kind ), $"extrude_{kind.ToString().ToLowerInvariant()}", ArchExtrude.Glyph( kind ), cut.Extrude == kind, () =>
		{
			cut.Extrude = kind;
			cut.Affects = ArchExtrude.Targets( kind );
			changed?.Invoke();
			refresh?.Invoke();
		} );
	}

	static void Damage( ArchIconGrid grid, ArchCutPart cut, ArchDamageKind kind, string tooltip, string slug, string glyph, Action refresh, Action changed )
	{
		grid.Pick( tooltip, slug, glyph, cut.ResolvedDamage == kind, () =>
		{
			cut.Damage = kind;
			cut.BreakEdges = kind == ArchDamageKind.Masonry;
			cut.Affects = ArchDamage.Targets( kind );
			cut.DamageAmount = kind == ArchDamageKind.Masonry ? 1f : cut.DamageAmount;
			cut.DamageCellWidth = kind == ArchDamageKind.Masonry ? 16f : cut.DamageCellWidth;
			cut.DamageCellLength = kind == ArchDamageKind.Masonry ? 8f : cut.DamageCellLength;
			cut.DamageDepth = kind == ArchDamageKind.Masonry ? 6f : cut.DamageDepth;
			cut.MasonryPattern = kind == ArchDamageKind.Masonry ? ArchMasonryPattern.ExposedEdge : cut.MasonryPattern;
			cut.MasonryCarves = kind == ArchDamageKind.Masonry ? false : cut.MasonryCarves;
			changed?.Invoke();
			refresh?.Invoke();
		} );
	}

	static void MasonryPresets( Layout pattern, ArchCutPart cut, Action refresh, Action changed )
	{
		using var presets = ArchIconGrid.In( pattern );

		presets.Pick( "Bricks - simple one-brick boxes extruded from an intact corner", "masonry_bricks", "view_in_ar",
			MasonryMatches( cut, ArchMasonryPattern.ExposedEdge, 16f, 8f, 6f, false ), () => ApplyMasonry( cut, ArchMasonryPattern.ExposedEdge, 16f, 8f, 6f, false, refresh, changed ) );

		presets.Pick( "Edge — two bricks, a header and intact courses at a broken wall edge", "masonry_edge", "view_module",
			MasonryMatches( cut, ArchMasonryPattern.ExposedEdge, 12f, 4f, 1.25f, true ), () => ApplyMasonry( cut, ArchMasonryPattern.ExposedEdge, 12f, 4f, 1.25f, true, refresh, changed ) );
		presets.Pick( "Worn — a fuller broken bond with fewer intact courses", "masonry_worn", "texture",
			MasonryMatches( cut, ArchMasonryPattern.WornCorner, 12f, 4f, 1.75f, true ), () => ApplyMasonry( cut, ArchMasonryPattern.WornCorner, 12f, 4f, 1.75f, true, refresh, changed ) );
		presets.Pick( "Pier — dense alternating long and header courses around square pillars", "masonry_pier", "view_column",
			MasonryMatches( cut, ArchMasonryPattern.BrickPier, 12f, 4f, 0.75f, false ), () => ApplyMasonry( cut, ArchMasonryPattern.BrickPier, 12f, 4f, 0.75f, false, refresh, changed ) );
	}

	static void ApplyMasonry( ArchCutPart cut, ArchMasonryPattern pattern, float width, float course, float depth, bool carves, Action refresh, Action changed )
	{
		cut.MasonryPattern = pattern;
		cut.DamageAmount = 1f;
		cut.DamageCellWidth = width;
		cut.DamageCellLength = course;
		cut.DamageDepth = depth;
		cut.MasonryCarves = carves;

		changed?.Invoke();
		refresh?.Invoke();
	}

	static bool MasonryMatches( ArchCutPart cut, ArchMasonryPattern pattern, float width, float course, float depth, bool carves )
	{
		return cut.MasonryPattern == pattern
			&& cut.MasonryCarves == carves
			&& MathF.Abs( cut.DamageAmount - 1f ) < 0.001f
			&& MathF.Abs( cut.DamageCellWidth - width ) < 0.001f
			&& MathF.Abs( cut.DamageCellLength - course ) < 0.001f
			&& MathF.Abs( cut.DamageDepth - depth ) < 0.001f;
	}

	// A cut that only carves builds nothing, so what it WEARS is the whole form: the walls it carries past
	// the solid it started in, and whatever caps it where it comes out.
	public static void Cut( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed )
	{
		Affects( panel, cut, refresh, changed );

		var floor = panel.AddGroup( "Floor" );

		floor.Add( ArchPartUi.Check( "Ramp the floor it leaves", cut.Ramp, value => cut.Ramp = value, Both( changed, refresh ) ) );

		if ( cut.Ramp )
		{
			Ramp( panel, cut, "depth", changed );
		}

		var edge = panel.AddGroup( "Opening edge" );
		edge.Add( ArchPartUi.Check( "Dress the cut edge", cut.Edge, value => cut.Edge = value, Both( changed, refresh ) ) );

		if ( cut.Edge )
		{
			edge.Add( ArchPartUi.Number( "Edge width", cut.EdgeWidth, 0f,
				value => cut.EdgeWidth = MathF.Max( 0f, value ), changed ) );
		}

		edge.Add( ArchPartUi.Check( "Break the cut edge", cut.BreakEdges, value => cut.BreakEdges = value, changed ) );
		edge.Add( ArchPartUi.Check( "Rail the stair edges it opens", cut.GuardsOpenedEdges,
			value => cut.GuardsOpenedEdges = value, changed ) );

		using ( var grid = ArchIconGrid.In( panel.AddGroup( "Enclosure" ) ) )
		{
			grid.Pick( "Open — the shaft is the absence in what it passed through", "cut_open", "crop_free",
				cut.Enclosure == CutEnclosure.None, () => { cut.Enclosure = CutEnclosure.None; changed?.Invoke(); refresh?.Invoke(); } );

			grid.Pick( "Walled — it carries its own walls up past the solid it started in", "cut_walled", "crop_square",
				cut.Enclosure == CutEnclosure.Walls, () => { cut.Enclosure = CutEnclosure.Walls; changed?.Invoke(); refresh?.Invoke(); } );
		}

		var walled = cut.Enclosure == CutEnclosure.Walls;

		if ( walled )
		{
			var shell = panel.AddGroup( "Walls" );

			shell.Add( ArchPartUi.Number( "Thickness", cut.WallThickness, 8f, value => cut.WallThickness = value, changed ) );
		}

		using ( var grid = ArchIconGrid.In( panel.AddGroup( "Head" ) ) )
		{
			grid.Pick( "Open to the sky", "cut_head_none", "crop_free",
				cut.Head == CutHead.None, () => { cut.Head = CutHead.None; changed?.Invoke(); refresh?.Invoke(); } );

			grid.Pick( "Capped — a lid over the top", "cut_head_cap", "horizontal_rule",
				cut.Head == CutHead.Cap, () => { cut.Head = CutHead.Cap; changed?.Invoke(); refresh?.Invoke(); } );

			grid.Pick( "Coped — a band round the mouth, finished like an outside edge", "cut_head_coping", "border_top",
				cut.Head == CutHead.Coping, () => { cut.Head = CutHead.Coping; changed?.Invoke(); refresh?.Invoke(); } );

			grid.Pick( "Housing — a little walled box with its own roof", "cut_head_housing", "home",
				cut.Head == CutHead.Housing, () => { cut.Head = CutHead.Housing; changed?.Invoke(); refresh?.Invoke(); } );
		}

		if ( cut.Head == CutHead.Coping )
		{
			var coping = panel.AddGroup( "Coping" );

			coping.Add( ArchPartUi.Number( "Height", cut.CopingHeight, 4f, value => cut.CopingHeight = value, changed ) );
			coping.Add( ArchPartUi.Number( "Oversail", cut.CopingOversail, 1.5f, value => cut.CopingOversail = value, changed ) );

			// The band's width IS the shell's thickness, so a walled shaft has already been asked for it - two
			// controls on one field is the panel telling you they are two numbers.
			if ( !walled )
			{
				coping.Add( ArchPartUi.Number( "Band width", cut.WallThickness, 8f, value => cut.WallThickness = value, changed ) );
			}

			return;
		}

		if ( cut.Head != CutHead.Housing )
		{
			return;
		}

		var housing = panel.AddGroup( "Housing" );

		housing.Add( ArchPartUi.Number( "Height", cut.HeadHeight, 96f, value => cut.HeadHeight = value, changed ) );
		housing.Add( ArchPartUi.Check( "Roof over it", cut.HeadRoof, value => cut.HeadRoof = value, changed ) );
	}

	static void Affects( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed, ArchCutAffects allowed = ArchCutAffects.All )
	{
		var open = AffectsExpanded;
		var enabled = Enum.GetValues<ArchCutAffects>()
			.Count( target => target is not (ArchCutAffects.None or ArchCutAffects.All) && (allowed & target) == target && ArchCut.Affects( cut, target ) );
		var targets = Enum.GetValues<ArchCutAffects>().Count( target => target is not (ArchCutAffects.None or ArchCutAffects.All) && (allowed & target) == target );
		var toggle = new Button( $"Affects ({enabled}/{targets})", open ? "expand_less" : "expand_more" )
		{
			Clicked = () => { AffectsExpanded = !open; refresh?.Invoke(); }
		};

		panel.Layout.Add( toggle );

		if ( !open )
		{
			return;
		}

		var affects = panel.AddGroup( "Affects" );

		Affect( affects, cut, allowed, "Floors", ArchCutAffects.Floors, changed );
		Affect( affects, cut, allowed, "Ceilings", ArchCutAffects.Ceilings, changed );
		Affect( affects, cut, allowed, "Foundations", ArchCutAffects.Foundations, changed );
		Affect( affects, cut, allowed, "Walls and corners", ArchCutAffects.Walls, changed );
		Affect( affects, cut, allowed, "Wall fittings", ArchCutAffects.WallFittings, changed );
		Affect( affects, cut, allowed, "Windows and doors", ArchCutAffects.Windows, changed );
		Affect( affects, cut, allowed, "Roofs", ArchCutAffects.Roofs, changed );
		Affect( affects, cut, allowed, "Platforms", ArchCutAffects.Platforms, changed );
		Affect( affects, cut, allowed, "Pillars", ArchCutAffects.Pillars, changed );
		Affect( affects, cut, allowed, "Beams", ArchCutAffects.Beams, changed );
		Affect( affects, cut, allowed, "Stairs", ArchCutAffects.Stairs, changed );
		Affect( affects, cut, allowed, "Trims", ArchCutAffects.Trims, changed );
		Affect( affects, cut, allowed, "Gutters", ArchCutAffects.Gutters, changed );
		Affect( affects, cut, allowed, "Road strips", ArchCutAffects.Roadway, changed );
		Affect( affects, cut, allowed, "Tunnel lining", ArchCutAffects.Lining, changed );
	}

	static void Affect( Layout group, ArchCutPart cut, ArchCutAffects allowed, string label, ArchCutAffects target, Action changed )
	{
		if ( (allowed & target) == target )
		{
			Affect( group, cut, label, target, changed );
		}
	}

	static void Affect( Layout group, ArchCutPart cut, string label, ArchCutAffects target, Action changed )
	{
		group.Add( ArchPartUi.Check( label, ArchCut.Affects( cut, target ), enabled =>
		{
			cut.Affects = enabled ? cut.Affects | target : cut.Affects & ~target;
		}, changed ) );
	}
}
sunless.lib_architecture / Editor/Pillar/ArchPillarGen.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly record struct ArchPillarRun( int FromX, int FromY, int ToX, int ToY, bool AlongX );

public readonly record struct ArchPillarColumn( Vector3 At, int X, int Y );

public static class ArchPillarGen
{
	// How far a column may be adrift of the surface under it before it is a part that never re-seated when it was
	// moved rather than one deliberately stood off its deck.
	const float Adrift = 1f;

	// WHERE THE COLUMN ACTUALLY STANDS, resolved when it is drawn rather than when it was placed. A part dragged
	// across the plan - or one with a slab poured under it afterwards - kept the foot it was authored with and
	// came out buried in the floor it is standing on. Founded says the author set that foot by hand and means it.
	// The terrain is not re-read here: the plan has nothing to say about it, and the seat it was placed with is
	// the only honest answer where nothing is poured.
	public static float Seat( ArchPillarPart part, ArchKit kit, ArchPlan plan )
	{
		if ( part.Founded || plan is null )
		{
			return part.BaseHeight;
		}

		// Up to where the column is already buried, never past it: a slab overhead is what the column reaches for,
		// and taking it as a seat would stand the column on the very thing it was put there to hold up.
		var poured = ArchPillarSeat.Poured( plan, kit, part.Origin, part.BaseHeight + ArchPillarSeat.Sunk( kit, part ) );

		return poured is { } standing && MathF.Abs( standing - part.BaseHeight ) > Adrift ? standing : part.BaseHeight;
	}

	// One answer to how tall a pillar stands: its authored height where it has one, else up to the lowest thing
	// standing over it - and CLAMPED to what is solid over it either way, because a column that grew past a slab
	// stands with its capital inside the floor it is there to carry. The generator, the ghost, the handle and the
	// extents report all resolve the same way, so a part that climbs past its room reads the same to the shaft
	// that builds it and the box that selects it.
	//
	// Level with that soffit rather than lapped into it: the contact pass deletes a face its coplanar neighbour
	// covers whole, so a bite would only bury a surviving face inside the slab.
	public static float Height( ArchPillarPart part, ArchRoom room, ArchKit kit, ArchPlan plan = null )
	{
		var floor = Seat( part, kit, plan );
		var slab = ArchPillarSoffit.Slab( plan, kit, part, floor );

		if ( part.Height > 0f )
		{
			return slab < float.MaxValue ? MathF.Max( 8f, MathF.Min( part.Height, slab - floor ) ) : part.Height;
		}

		if ( room is null )
		{
			return slab < float.MaxValue ? MathF.Max( 8f, slab - floor ) : kit.WallHeight;
		}

		return MathF.Max( 8f, ArchPillarSoffit.Over( plan, kit, room, part, floor ) - floor );
	}

	// Computed once, so the spandrel and the run landing on it cannot disagree.
	public static List<ArchPillarRun> Runs( ArchPillarPart part )
	{
		var runs = new List<ArchPillarRun>();

		if ( part.Span == PillarSpan.None || part.Placement != PillarPlacement.Grid )
		{
			return runs;
		}

		var countX = Math.Max( 1, part.CountX );
		var countY = Math.Max( 1, part.CountY );
		var edge = part.PerimeterOnly;

		if ( part.SpanAlongX )
		{
			for ( var iy = 0; iy < countY; iy++ )
			{
				if ( edge && iy > 0 && iy < countY - 1 )
				{
					continue;
				}

				for ( var ix = 0; ix < countX - 1; ix++ )
				{
					runs.Add( new ArchPillarRun( ix, iy, ix + 1, iy, true ) );
				}
			}
		}

		if ( !part.SpanAlongY )
		{
			return runs;
		}

		for ( var ix = 0; ix < countX; ix++ )
		{
			if ( edge && ix > 0 && ix < countX - 1 )
			{
				continue;
			}

			for ( var iy = 0; iy < countY - 1; iy++ )
			{
				runs.Add( new ArchPillarRun( ix, iy, ix, iy + 1, false ) );
			}
		}

		return runs;
	}

	public static HashSet<(int X, int Y)> Carried( ArchPillarPart part )
	{
		var carried = new HashSet<(int X, int Y)>();

		foreach ( var run in Runs( part ) )
		{
			carried.Add( (run.FromX, run.FromY) );
			carried.Add( (run.ToX, run.ToY) );
		}

		return carried;
	}

	public static void Build(
		ArchMesh canvas,
		ArchPillarPart part,
		ArchRoom room,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		ArchStyle style )
	{
		var chain = new[] { part.Palette, room.Palette, building.Palette };
		var shaft = style.Brush( ArchSurface.Pillar, chain );
		var cap = style.Brush( ArchSurface.PillarCap, chain );

		var seat = Seat( part, kit, plan );
		var height = Height( part, room, kit, plan );

		if ( part.Placement == PillarPlacement.Pilaster )
		{
			Pilaster( canvas, part, room, height, shaft, cap, kit );
			return;
		}

		// A span bears ON the columns, so the shaft stops at the springing.
		var spanning = part.Span != PillarSpan.None && part.Placement == PillarPlacement.Grid;
		var shaftHeight = spanning ? MathF.Max( 8f, height - part.SpanBand ) : height;
		var carried = spanning ? Carried( part ) : new HashSet<(int X, int Y)>();

		// Only a column a run springs off gives up its top to the span.
		foreach ( var column in Columns( part, seat ) )
		{
			var columnHeight = carried.Contains( (column.X, column.Y) ) ? shaftHeight : height;

			Column( canvas, part, column.At, columnHeight, shaft, cap,
				Bites( plan, kit, part, room, building, column.At, columnHeight ) );

			ArchDamage.PillarCorners( canvas, plan, kit, building, room, part, column.At, columnHeight, style );
		}

		if ( spanning )
		{
			using ( canvas.Part( ArchPieces.Span ) )
			{
				Spans( canvas, part, kit, seat, height, cap );
			}
		}
	}

	// A section standing where something else already decided its seat - roof plant on a deck plane, not a column
	// in a room, so there is no wall height to fall back on and its own is the only answer.
	public static void Stand( ArchMesh canvas, ArchPillarPart part, ArchBrush shaft, ArchBrush cap )
	{
		foreach ( var column in Columns( part ) )
		{
			Column( canvas, part, column.At, MathF.Max( 1f, part.Height ), shaft, cap );
		}
	}

	public static List<Vector3> Positions( ArchPillarPart part ) => Columns( part ).Select( column => column.At ).ToList();

	public static List<ArchPillarColumn> Columns( ArchPillarPart part ) => Columns( part, part.BaseHeight );

	public static List<ArchPillarColumn> Columns( ArchPillarPart part, float seat )
	{
		var columns = new List<ArchPillarColumn>();
		var rotation = Rotation.FromYaw( part.Yaw );

		if ( part.Placement != PillarPlacement.Grid )
		{
			columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x, part.Origin.y, seat ), 0, 0 ) );
			return columns;
		}

		var countX = Math.Max( 1, part.CountX );
		var countY = Math.Max( 1, part.CountY );

		for ( var ix = 0; ix < countX; ix++ )
		{
			for ( var iy = 0; iy < countY; iy++ )
			{
				var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
				var world = rotation * local;

				columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x + world.x, part.Origin.y + world.y, seat ), ix, iy ) );
			}
		}

		return columns;
	}

	readonly record struct Course( float Bottom, float Top, Vector2 Lower, Vector2 Upper, bool Dressed );

	// What a Subtract standing over this column takes out of it, in the band the column actually occupies. Answered
	// through ArchCut like every other host's, so ArchLayerOrder decides whether a cut authored before the column
	// reaches it at all.
	public static List<ArchCarveVolume> Bites( ArchPlan plan, ArchKit kit, ArchPillarPart part, ArchRoom room, ArchBuilding building, Vector3 basePoint, float height )
	{
		var bites = new List<ArchCarveVolume>();

		if ( plan is null || room is null )
		{
			return bites;
		}

		var widest = part.Reach;
		var flat = new Vector2( basePoint.x, basePoint.y );
		var footprint = ArchFootprint.Rect( flat - new Vector2( widest, widest ), flat + new Vector2( widest, widest ) );
		var foot = basePoint.z - MathF.Max( 0f, part.FootingBuried );
		var head = basePoint.z + height;

		foreach ( var cut in ArchCut.Over( plan, room.Floor, footprint, building?.Id ?? 0, ArchCutAffects.Pillars, part.Id ) )
		{
			bites.AddRange( ArchCut.Resolve( cut, kit ).Where( volume => ArchCut.Reaches( volume, foot, head ) ) );
		}

		return bites;
	}

	static void Column( ArchMesh canvas, ArchPillarPart part, Vector3 basePoint, float height, ArchBrush shaft, ArchBrush cap, List<ArchCarveVolume> bites = null )
	{
		var courses = Courses( part, basePoint.z, height );

		if ( courses.Count == 0 )
		{
			return;
		}

		var flat = new Vector2( basePoint.x, basePoint.y );

		if ( bites is { Count: > 0 } )
		{
			Carved( canvas, part, flat, courses, bites, shaft, cap );

			return;
		}

		Hulled( canvas, part, flat, courses, shaft, cap );
	}

	// A column nothing carves stays ONE welded hull, which is what keeps a colonnade walkable and what stops a
	// stacked box sealing a face pair at every step.
	static void Hulled( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, ArchBrush shaft, ArchBrush cap )
	{
		var sides = part.Sides;

		using var welding = canvas.Welding();

		// ONE shape for the whole column, not one per course: the hull over every ring closes the inset at a plinth
		// or a capital step, which nothing can stand in anyway, and a colonnade stays walkable because each column
		// is its own hull rather than all of them being one mesh.
		using var solid = canvas.Solid( ArchSolid.Hull( Rings( courses, flat, sides, part.Yaw ) ) );

		canvas.Polygon( Ring( flat, courses[0].Lower, courses[0].Bottom, sides, part.Yaw ), courses[0].Dressed ? cap : shaft, true );

		for ( var index = 0; index < courses.Count; index++ )
		{
			var course = courses[index];
			var brush = course.Dressed ? cap : shaft;

			Faces( canvas, flat, course, sides, part.Yaw, brush );

			if ( index == courses.Count - 1 )
			{
				continue;
			}

			Step( canvas, flat, course, courses[index + 1], sides, part.Yaw, brush );
		}

		var last = courses[^1];

		canvas.Polygon( Ring( flat, last.Upper, last.Top, sides, part.Yaw ), last.Dressed ? cap : shaft );
	}

	// A column a cut reaches has no hull to be, so it goes through the same carve algebra a platform does - one
	// prism per course, so a bite's cheeks share the shaft's own edges. A course that TAPERS comes out straight
	// sided here: the prism algebra has no taper, and the only tapering course is a footing, which is buried.
	static void Carved( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, List<ArchCarveVolume> bites, ArchBrush shaft, ArchBrush cap )
	{
		using var welding = canvas.Welding();

		foreach ( var course in courses )
		{
			var ring = Ring( flat, course.Lower, 0f, part.Sides, part.Yaw )
				.Select( point => new Vector2( point.x, point.y ) )
				.ToList();

			var carve = ArchCarve.Prism( ring, course.Bottom, course.Top ).In( part.Yaw );

			foreach ( var bite in bites )
			{
				carve.Less( bite );
			}

			foreach ( var face in carve.Resolve().Faces )
			{
				canvas.Polygon( face.Points, course.Dressed ? cap : shaft );
			}
		}
	}

	// Every corner the column has, bottom ring to top ring, which is what its hull is taken over.
	static List<Vector3> Rings( List<Course> courses, Vector2 flat, int sides, float yaw )
	{
		var points = new List<Vector3>();

		foreach ( var course in courses )
		{
			points.AddRange( Ring( flat, course.Lower, course.Bottom, sides, yaw ) );
			points.AddRange( Ring( flat, course.Upper, course.Top, sides, yaw ) );
		}

		return points;
	}

	static List<Course> Courses( ArchPillarPart part, float baseHeight, float height )
	{
		var courses = new List<Course>();
		var section = part.Half;

		var plinth = part.Plinth ? MathF.Max( 0f, part.PlinthHeight ) : 0f;
		var capital = part.Capital ? MathF.Max( 0f, part.CapitalHeight ) : 0f;

		var top = baseHeight + height;
		var shaftBottom = baseHeight + plinth;
		var shaftTop = top - capital;

		Footing( part, courses, section, baseHeight );

		if ( plinth > 0.05f )
		{
			var spread = section + new Vector2( part.PlinthOversize, part.PlinthOversize );

			courses.Add( new Course( baseHeight, shaftBottom, spread, spread, true ) );
		}

		if ( shaftTop - shaftBottom > 0.05f )
		{
			courses.Add( new Course( shaftBottom, shaftTop, section, section, false ) );
		}

		if ( capital > 0.05f )
		{
			Head( part, courses, section, shaftTop, top );
		}

		return courses;
	}

	static void Footing( ArchPillarPart part, List<Course> courses, Vector2 section, float baseHeight )
	{
		if ( part.Footing == PillarFooting.None )
		{
			return;
		}

		var foot = baseHeight - MathF.Max( 0f, part.FootingBuried );

		courses.AddRange( Pad( part.Footing, section, part.FootingReach,
			foot, foot + MathF.Max( 1f, part.FootingHeight ) ) );
	}

	// THE SAME PAD, TURNED OVER. A course carries a lower and an upper section already, so mirroring one is
	// swapping those two and reflecting its band - which is why the head reuses the footing's shapes rather than
	// growing a taper of its own to keep in step with. It is never buried: level with the soffit it meets, because
	// the contact pass deletes a face its coplanar neighbour covers whole and a bite would only hide a survivor.
	static void Head( ArchPillarPart part, List<Course> courses, Vector2 section, float from, float top )
	{
		var mirrored = new List<Course>();

		foreach ( var course in Pad( part.CapitalShape, section, MathF.Max( 0f, part.CapitalOversize ), from, top ) )
		{
			mirrored.Add( new Course( from + top - course.Top, from + top - course.Bottom,
				course.Upper, course.Lower, course.Dressed ) );
		}

		// ASCENDING, because the list's order is load-bearing: courses[0] caps the foot, courses[^1] caps the head,
		// and Step bridges each pair as neighbours. Mirroring reverses z, so the list has to be turned back.
		mirrored.Reverse();
		courses.AddRange( mirrored );
	}

	// One pad, read bottom up: a plain block, a bevel dying into the shaft, or two steps.
	static IEnumerable<Course> Pad( PillarFooting kind, Vector2 section, float spread, float foot, float head )
	{
		var wide = section + new Vector2( spread, spread );
		var depth = MathF.Max( 1f, head - foot );

		switch ( kind )
		{
			case PillarFooting.Bevelled:
				// A third stays square under the taper, or the pad reads as a cone.
				var shelf = foot + depth * 0.34f;

				yield return new Course( foot, shelf, wide, wide, true );
				yield return new Course( shelf, foot + depth, wide, section, true );
				break;

			case PillarFooting.Stepped:
				var middle = foot + depth * 0.5f;
				var upper = section + new Vector2( spread * 0.5f, spread * 0.5f );

				yield return new Course( foot, middle, wide, wide, true );
				yield return new Course( middle, foot + depth, upper, upper, true );
				break;

			default:
				yield return new Course( foot, foot + depth, wide, wide, true );
				break;
		}
	}

	static void Faces( ArchMesh canvas, Vector2 centre, Course course, int sides, float yaw, ArchBrush brush )
	{
		var lower = Ring( centre, course.Lower, course.Bottom, sides, yaw );
		var upper = Ring( centre, course.Upper, course.Top, sides, yaw );

		for ( var index = 0; index < lower.Length; index++ )
		{
			var next = (index + 1) % lower.Length;

			canvas.Quad( lower[index], lower[next], upper[next], upper[index], brush );
		}
	}

	// Equal sections continue instead - a plain pillar stays six faces.
	static void Step( ArchMesh canvas, Vector2 centre, Course below, Course above, int sides, float yaw, ArchBrush brush )
	{
		if ( MathF.Abs( below.Upper.x - above.Lower.x ) < 0.01f && MathF.Abs( below.Upper.y - above.Lower.y ) < 0.01f )
		{
			return;
		}

		// The two wind opposite ways - the wrong one leaves a hole in the half-edge mesh.
		var stepsIn = below.Upper.x >= above.Lower.x;

		var wide = Ring( centre, stepsIn ? below.Upper : above.Lower, below.Top, sides, yaw );
		var tight = Ring( centre, stepsIn ? above.Lower : below.Upper, below.Top, sides, yaw );

		for ( var index = 0; index < wide.Length; index++ )
		{
			var next = (index + 1) % wide.Length;

			if ( stepsIn )
			{
				canvas.Quad( wide[index], wide[next], tight[next], tight[index], brush );
				continue;
			}

			canvas.Quad( tight[index], tight[next], wide[next], wide[index], brush );
		}
	}

	// The section, whatever number of sides it has: the rectangle by hand so a plain pier keeps its exact
	// corners, and anything rounder through ArchFootprint's own ellipse so a bool's circle and a column's
	// circle are the same loop. Turned about its own centre - a yaw that only marched the grid and left every
	// section square was a turn handle that did nothing at all to a lone column.
	static Vector3[] Ring( Vector2 centre, Vector2 half, float height, int sides, float yaw )
	{
		var corners = sides < 5
			? new[]
			{
				new Vector2( centre.x - half.x, centre.y - half.y ),
				new Vector2( centre.x + half.x, centre.y - half.y ),
				new Vector2( centre.x + half.x, centre.y + half.y ),
				new Vector2( centre.x - half.x, centre.y + half.y )
			}
			: ArchFootprint.Ellipse( centre - half, centre + half, sides ).ToArray();

		var turned = MathF.Abs( yaw % 360f ) < 0.01f ? corners : ArchFootprint.Turned( corners, centre, yaw ).ToArray();

		return turned.Select( point => new Vector3( point.x, point.y, height ) ).ToArray();
	}

	// Seated so its top finishes on the column top, not proud of the capital.
	static void Spans( ArchMesh canvas, ArchPillarPart part, ArchKit kit, float seat, float height, ArchBrush brush )
	{
		var rotation = Rotation.FromYaw( part.Yaw );
		var top = seat + height;
		var springing = MathF.Max( seat + 8f, top - part.SpanBand );
		var half = part.Half;
		var lap = ArchContact.Bite( kit );

		foreach ( var run in Runs( part ) )
		{
			var inset = (run.AlongX ? half.x : half.y) - lap;

			Connect( canvas, part,
				Point( part, rotation, run.FromX, run.FromY ), Point( part, rotation, run.ToX, run.ToY ),
				run.AlongX, springing, top, inset, brush );
		}

		// Laps a bite down into the shaft; flush leaves cap and block in one plane.
		foreach ( var (ix, iy) in Carried( part ) )
		{
			var centre = Point( part, rotation, ix, iy );

			canvas.Box(
				new Vector3( centre.x - half.x, centre.y - half.y, ArchContact.Bury( kit, springing, 1f ) ),
				new Vector3( centre.x + half.x, centre.y + half.y, top ),
				brush );
		}
	}

	static Vector2 Point( ArchPillarPart part, Rotation rotation, int ix, int iy )
	{
		var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
		var world = rotation * local;

		return new Vector2( part.Origin.x + world.x, part.Origin.y + world.y );
	}

	// Inset is the pier's half-section less a bite, so the run runs INTO the spandrel. The solid itself comes off
	// ArchSpanGen, which is what makes an arcade and a hand-picked arch the same geometry.
	static void Connect( ArchMesh canvas, ArchPillarPart part, Vector2 from, Vector2 to, bool alongX, float springing, float top, float inset, ArchBrush brush )
	{
		ArchSpanGen.Build( canvas, new ArchSpanRun
		{
			From = from,
			To = to,
			Springing = springing,
			Top = top,
			Thickness = part.SpanThickness( alongX ),
			InsetFrom = inset,
			InsetTo = inset,
			Segments = part.ArchSegments,
			Ring = part.ArchRing,
			Form = part.Span
		}, brush );
	}

	static void Pilaster( ArchMesh canvas, ArchPillarPart part, ArchRoom room, float height, ArchBrush shaft, ArchBrush cap, ArchKit kit )
	{
		var wall = room.Walls.Find( candidate => candidate.Id == part.WallId ) ?? room.Walls.FirstOrDefault();

		if ( wall is null )
		{
			return;
		}

		var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
		var half = thickness * 0.5f;
		var halfWidth = part.PilasterWidth * 0.5f;
		var offset = Math.Clamp( part.WallOffset, halfWidth, Math.Max( halfWidth, wall.Length - halfWidth ) );

		var direction = wall.Direction;
		var normal = wall.Normal;
		var centre = wall.PointAt( offset );

		Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, false );

		if ( part.PilasterBothFaces )
		{
			Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, true );
		}
	}

	static void Face(
		ArchMesh canvas,
		Vector2 centre,
		Vector2 direction,
		Vector2 normal,
		float halfWidth,
		float wallHalf,
		ArchPillarPart part,
		float height,
		ArchBrush shaft,
		ArchBrush cap,
		bool inner )
	{
		// The primary face is the EXTERIOR one, and that is the -normal side: it is where ArchWallGen lays the
		// street skin and where a wall modifier's Outside stands, so a pilaster on +normal came out inside the
		// room it was put there to dress the outside of. Flipping the normal alone would reverse the loop's
		// winding, so the run is flipped with it - the block does not move, its corners are walked the other way.
		var sign = inner ? 1f : -1f;
		var run = direction * sign;
		var near = wallHalf * sign;
		var far = (wallHalf + part.PilasterProtrusion) * sign;

		var bottom = part.BaseHeight;
		var plinth = part.Plinth ? Math.Max( 0f, part.PlinthHeight ) : 0f;
		var capital = part.Capital ? Math.Max( 0f, part.CapitalHeight ) : 0f;

		Slab( canvas, centre, run, normal, halfWidth, near, far, bottom + plinth, bottom + height - capital, shaft );

		if ( plinth > 0.05f )
		{
			Slab( canvas, centre, run, normal, halfWidth + part.PlinthOversize, near, far + part.PlinthOversize * sign, bottom, bottom + plinth, cap );
		}

		if ( capital > 0.05f )
		{
			Slab( canvas, centre, run, normal, halfWidth + part.CapitalOversize, near, far + part.CapitalOversize * sign, bottom + height - capital, bottom + height, cap );
		}
	}

	static void Slab(
		ArchMesh canvas,
		Vector2 centre,
		Vector2 direction,
		Vector2 normal,
		float halfWidth,
		float near,
		float far,
		float bottom,
		float top,
		ArchBrush brush )
	{
		if ( top - bottom < 0.05f )
		{
			return;
		}

		var a = centre - direction * halfWidth + normal * near;
		var b = centre + direction * halfWidth + normal * near;
		var c = centre + direction * halfWidth + normal * far;
		var d = centre - direction * halfWidth + normal * far;

		var lower = new List<Vector3>
		{
			new( a.x, a.y, bottom ),
			new( b.x, b.y, bottom ),
			new( c.x, c.y, bottom ),
			new( d.x, d.y, bottom )
		};

		var upper = new List<Vector3>();

		foreach ( var point in lower )
		{
			upper.Add( point.WithZ( top ) );
		}

		canvas.Prism( lower, upper, brush );
	}
}
sunless.lib_architecture / Editor/Services/ArchGridService.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// The grid is the EDITOR'S — the one drawn in the viewport, with the spacing and the snap toggle already sitting
// on the scene view's own bar. The tool held a second one of its own, which meant two answers to one question and
// a cursor that could land on a rung nothing had drawn.
//
// Sizes always answer, because a reach or a minimum run is a measurement and does not stop being one when snapping
// is off. Only a COORDINATE asks whether to snap, and it asks here rather than at any call site.
public sealed class ArchGridService
{
	public const float FinestSize = 0.25f;

	// The angle ladder every authored turn ratchets onto. It stands beside the size ladder because stepping and
	// snapping are one invariant, and a module choosing its own would author off the grid with nothing to say so.
	// Authored like the size ladder the scene view owns, and ZERO is the free swing - which is why every reader
	// asks for it rather than testing a flag of its own.
	public static float AngleStep
	{
		get => Math.Clamp( EditorCookie.Get( "arch.grid.anglestep", 15f ), 0f, 90f );
		set => EditorCookie.Set( "arch.grid.anglestep", Math.Clamp( value, 0f, 90f ) );
	}

	public static bool Snapping => EditorScene.GizmoSettings.SnapToGrid;

	public float BaseSize => Math.Clamp( EditorScene.GizmoSettings.GridSpacing, FinestSize, 128f );

	public float Base( float value )
	{
		return Snapping ? Snap( value, BaseSize ) : value;
	}

	public Vector2 Base( Vector2 point )
	{
		return Snapping ? Snap( point, BaseSize ) : point;
	}

	public Vector3 Base( Vector3 point )
	{
		return Snapping ? Snap( point, BaseSize ) : point;
	}

	public float Subgrid( float value, int divisions = 8 )
	{
		return Snapping ? Snap( value, SubgridSize( divisions ) ) : value;
	}

	public Vector2 Subgrid( Vector2 point, int divisions = 8 )
	{
		return Snapping ? Snap( point, SubgridSize( divisions ) ) : point;
	}

	public Vector3 Subgrid( Vector3 point, int divisions = 8 )
	{
		return Snapping ? Snap( point, SubgridSize( divisions ) ) : point;
	}

	public Vector3 CurveControl( Vector3 point )
	{
		var flat = Base( new Vector2( point.x, point.y ) );

		return new Vector3( flat.x, flat.y, Height( point.z ) );
	}

	// A control DRAPED on ground: the flat lands on the ladder and the height does not, because a height read off
	// the terrain is measured rather than authored - rung it and the run sinks into the ground it was laid on.
	public Vector3 Draped( Vector3 point )
	{
		var flat = Base( new Vector2( point.x, point.y ) );

		return new Vector3( flat.x, flat.y, point.z );
	}

	// No second grid for z - the same ladder a plan coordinate lands on.
	public float Height( float value )
	{
		return Base( value );
	}

	public List<Vector2> Base( IEnumerable<Vector2> points )
	{
		return points.Select( Base ).ToList();
	}

	public (Vector2 Min, Vector2 Max) Rectangle( Vector2 first, Vector2 second )
	{
		var min = Vector2.Min( first, second );
		var max = Vector2.Max( first, second );

		return (Base( min ), Base( max ));
	}

	public float SubgridSize( int divisions = 8 )
	{
		var count = PowerOfTwo( Math.Max( 1, divisions ) );

		return MathF.Max( FinestSize, BaseSize / count );
	}

	public ArchDivision DivideAtMost( float span, float spacing, int divisions = 8 )
	{
		var length = MathF.Abs( Subgrid( span, divisions ) );

		if ( length < FinestSize )
		{
			return new ArchDivision { Span = 0f, Count = 0 };
		}

		var unit = SubgridSize( divisions );
		var units = Math.Max( 1, (int)MathF.Round( length / unit ) );
		var minimum = Math.Max( 1, (int)MathF.Ceiling( length / MathF.Max( unit, spacing ) ) );
		var count = minimum;

		while ( count < units && units % count != 0 )
		{
			count++;
		}

		return new ArchDivision { Span = length, Count = Math.Min( count, units ) };
	}

	public static float Fine( float value )
	{
		return Snap( value, FinestSize );
	}

	public static Vector2 Fine( Vector2 point )
	{
		return Snap( point, FinestSize );
	}

	public static Vector3 Fine( Vector3 point )
	{
		return Snap( point, FinestSize );
	}

	public static float Snap( float value, float size )
	{
		return MathF.Round( value / size ) * size;
	}

	// Where a DRAGGED edit lands: the step is what goes on the ladder, never the coordinate. An existing shape is
	// wherever it was authored - a turned loop's corners are nowhere near a rung, and a cut's band is wherever it
	// was pulled to - so snapping the absolute coordinate yanks the whole thing onto the nearest one the moment a
	// handle touches it. On a base grid of 64 a nudge along x dropped a cut standing at -40 straight to 0, which
	// is the shape jumping a storey for a gesture that never touched its height. Placement still snaps outright:
	// a NEW shape is authored on the grid, and an edit keeps the offset it already had.
	public static float Stepped( float from, float to, float size )
	{
		return from + Snap( to - from, size );
	}

	public static Vector2 Stepped( Vector2 from, Vector2 to, float size )
	{
		return new Vector2( Stepped( from.x, to.x, size ), Stepped( from.y, to.y, size ) );
	}

	static Vector2 Snap( Vector2 point, float size )
	{
		return new Vector2( Snap( point.x, size ), Snap( point.y, size ) );
	}

	static Vector3 Snap( Vector3 point, float size )
	{
		return new Vector3( Snap( point.x, size ), Snap( point.y, size ), Snap( point.z, size ) );
	}

	static int PowerOfTwo( int value )
	{
		var result = 1;

		while ( result < value )
		{
			result *= 2;
		}

		return result;
	}
}
sunless.lib_architecture / Editor/Tool/ArchSubtool.cs
Editor library
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public abstract class ArchSubtool : EditorTool
{
	ToolSidebarWidget sidebar;

	protected ArchSubtool( ArchTool owner )
	{
		Owner = owner;
	}

	protected ArchTool Owner { get; }

	static readonly ArchViewAxis[] PlanOnly = { ArchViewAxis.Top };

	ArchViewAxis served = (ArchViewAxis)(-1);

	protected bool Dragging { get; private set; }
	protected Vector2 DragStart { get; private set; }
	protected Vector2 DragCurrent { get; private set; }
	protected float DragStartHeight { get; private set; }
	protected float DragHeight { get; private set; }

	// The deck the gesture began on, so a placement files itself on the roof it was drawn against rather than
	// looking one up again from a point the camera has since moved past.
	protected ArchRoofPart DragDeck { get; private set; }

	protected ArchCursor Pointer { get; private set; }

	protected virtual bool UsesDrag => true;

	// Off the grid, the cursor is read where the ray actually landed instead. The grid that answers is the
	// editor's own, so this follows the scene view's snap toggle rather than one of the tool's own.
	protected virtual bool Snapped => ArchGridService.Snapping;

	// The snap band THIS gesture wants, off the reach the author set on the bar. Tight by default, because most
	// gestures are drags and a face snap sets the point flush across the line: both ends inside the band and the
	// drag has no across component left to place anything with. A gesture whose whole purpose is to set something
	// flush against what already stands answers ArchWallSnap.Flush instead.
	public virtual ArchSnapReach SnapReach( float authored ) => ArchWallSnap.Reach( authored );

	// A footprint has no meaning in an elevation, and a tool that quietly placed at the origin would be worse than one that refuses.
	public virtual ArchViewAxis[] Works => PlanOnly;

	public bool Serves( ArchViewAxis axis ) => Works.Contains( axis );

	// The work-plane grid is scaffolding for placing; an unfocused view is not placing either.
	public virtual bool Placing => Manager?.IsCurrentViewFocused == true;

	public virtual ArchSurface[] Surfaces => Array.Empty<ArchSurface>();

	// One answer for the palette tile and the sidebar header: the authored art, else the type's own [Icon].
	public string Icon => ArchIcons.Get( ArchIcons.SubtoolSlug( this ), EditorTypeLibrary.GetType( GetType() )?.Icon ?? "category" );

	// A gesture the author is actually in the middle of: the mouse held through a drag, or a chained run with an
	// end already taken. Merely having a placement tool up is not one, and that is the whole difference between a
	// stack that says what you are doing and one that says "placing" from the moment you pick the tool.
	bool Gesturing => Dragging || Run().Active;

	public override void OnUpdate()
	{
		activeDraft?.Show( Gesturing );

		if ( Manager?.IsCurrentViewFocused != true )
		{
			return;
		}

		var axis = Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis;

		if ( axis != served )
		{
			served = axis;
			Refresh();
		}

		// The selection's own widgets do not depend on where the cursor lands - the gizmo hit-tests itself - so
		// they are drawn before the work plane is asked for anything, and before a free-click tool takes the
		// frame. Behind those gates, aiming at the sky took the widgets off the part that was selected, a drag
		// already under way died halfway through the gesture, and a subtool reading faces showed no widget at
		// all - which is a layer selected in the stack with nothing to drag it by.
		if ( Serves( axis ) && Adjusting() )
		{
			return;
		}

		// A pick that is not on the work plane at all - a face overhead - has to be taken BEFORE the cursor
		// gate. Looking up at a ceiling never crosses that plane, so the frame stopped here and the click
		// simply went missing.
		if ( TakesFreeClick )
		{
			using ( ArchGhost.Begin() )
			{
				DrawFreeHover();
			}

			if ( Gizmo.WasLeftMousePressed )
			{
				OnFreeClick();
			}

			return;
		}

		if ( !Serves( axis ) || !Owner.Cursor( out var cursor ) )
		{
			// A gesture with nowhere to land still has to END, or the preview follows the cursor for ever and
			// the next release dispatches a drag from a start the author left behind minutes ago.
			Dragging &= !Gizmo.WasLeftMouseReleased;

			if ( !Dragging )
			{
				Owner.StepOff();
			}

			return;
		}

		var point = Snapped ? cursor.Plan : cursor.Free;

		Pointer = cursor;
		DragCurrent = point;
		DragHeight = cursor.Height;

		if ( !UsesDrag )
		{
			using ( ArchGhost.Begin() )
			{
				DrawHover( point );
			}

			if ( Gizmo.WasLeftMousePressed )
			{
				Owner.EnsureTarget();
				OnClick( point );
			}

			return;
		}

		if ( Gizmo.WasLeftMousePressed )
		{
			DragStart = point;
			DragStartHeight = cursor.Height;
			DragDeck = cursor.OnDeck;
			Dragging = true;

			// The whole gesture works whatever it began on. Without this the ray is re-projected onto the storey's
			// plane every frame, so a drag begun on a deck 128 inches up ran off across the yard the moment the
			// camera was not looking straight down at it.
			Owner.StandOn( cursor.Height, cursor.OnDeck );

			return;
		}

		using ( ArchGhost.Begin() )
		{
			if ( Dragging )
			{
				DrawPreview();
			}
			else
			{
				DrawHover( point );
			}
		}

		if ( !Gizmo.WasLeftMouseReleased || !Dragging )
		{
			return;
		}

		Dragging = false;
		drawn = null;

		Owner.StepOff();
		Owner.EnsureTarget();
		OnDrag( DragStart, point );
	}

	// Off by default: most tools place and move on. A tool that owns the thing it just placed turns it on
	// so the shape can be pulled about without leaving the tool that drew it.
	protected virtual bool Adjusts => false;

	// Whoever draws the selection's widgets, they get the frame HERE - true while they hold the mouse, so the
	// tool's own gesture stands down. One slot, or a subtool that draws its handles somewhere further down is a
	// subtool whose handles are gated behind whatever it does first.
	protected virtual bool Adjusting() => Adjust();

	bool adjusting;

	object drawn;

	// The shape just drawn keeps the sidebar - its own numbers are what you reach for next - but NOT its
	// widgets, until a gesture has been and gone. A box dragger standing over the thing you just placed covers
	// the very surface you place the next one ON, and the gizmo takes the press first, so every drag after the
	// first went into the widget and nothing was ever placed again.
	protected void Drew( object placed ) => drawn = placed;

	// A deliberate SELECT is the author asking to edit that shape, which is the one thing the memo must not
	// outlast: placing a bool files it as drawn, and nothing else cleared that, so picking it again in the Plan
	// Layers stack gave a selected part with the properties open and no widget anywhere until another shape had
	// been dragged over it. Placement sets Picked directly and does NOT come through here, so the shape you just
	// drew still waits for its gesture.
	internal void Forget() => drawn = null;

	// True while the selection's own widgets have the mouse, so the placement gesture stands down. The
	// commit waits for the release, or a drag would stack one undo entry per pixel.
	//
	// The drawn shape is forgotten where the placement is DISPATCHED, never here: clearing it on the release
	// frame put the widgets back a frame early, and the engine still reports its pressed path on that frame,
	// so they took the release the drag was about to place on. Every gesture after the first was read as an
	// edit of the last shape - nothing was ever placed again and the preview never stood down.
	bool Adjust()
	{
		// A HELD WIDGET IS A GESTURE, AND A GESTURE ENDS WITH THE BUTTON. This latched true and was cleared only on a
		// release the three gates below let through, so anything that moved between the grab and the release - the
		// selection changing, the picked part becoming the one just drawn, the kind going out of this tool's reach -
		// left it set for the life of the subtool. CapturesPlacement then answered true on every frame afterwards,
		// Adjusting took the whole frame, and the tool never saw another press: a driveway that would not drag, for
		// ever, with nothing on screen to say why. The release frame is spared so the edit below still commits.
		if ( !Gizmo.IsLeftMouseDown && !Gizmo.WasLeftMouseReleased )
		{
			adjusting = false;
		}

		if ( !Adjusts || Owner.Picked is not { } picked || !ArchShapeHandles.ShowsHandles( picked.Item, drawn ) )
		{
			return false;
		}

		if ( ArchHandles.Draw( Owner, picked ) )
		{
			adjusting = true;
			Owner.Preview();
		}

		if ( !ArchShapeHandles.CapturesPlacement( ArchShapeHandles.HandlePressed, adjusting ) )
		{
			return false;
		}

		if ( Gizmo.WasLeftMouseReleased )
		{
			adjusting = false;
			Owner.Commit( $"Edit {picked.Describe()}" );
		}

		return true;
	}

	protected virtual void OnDrag( Vector2 from, Vector2 to ) { }

	protected virtual void OnClick( Vector2 point ) { }

	// For a subtool whose click is a ray into the scene rather than a point on the plan.
	protected virtual bool TakesFreeClick => false;

	protected virtual void OnFreeClick() { }

	protected virtual void DrawFreeHover() { }

	// Overrides run inside an already-configured ArchGhost scope - no gizmo setup of their own.
	protected virtual void DrawHover( Vector2 point )
	{
		ArchGhost.Cursor( point, DragHeight, Owner.Kit.GridSize );
	}

	protected virtual void DrawPreview()
	{
		ArchGhost.Cursor( DragCurrent, DragHeight, Owner.Kit.GridSize );

		Gizmo.Draw.Line(
			new Vector3( DragStart.x, DragStart.y, DragStartHeight ),
			new Vector3( DragCurrent.x, DragCurrent.y, DragHeight ) );
	}

	protected void DrawRectPreview()
	{
		ArchGhost.Plate( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), Owner.LevelHeight );
	}

	protected static Vector2 Min( Vector2 a, Vector2 b ) => new( MathF.Min( a.x, b.x ), MathF.Min( a.y, b.y ) );

	protected static Vector2 Max( Vector2 a, Vector2 b ) => new( MathF.Max( a.x, b.x ), MathF.Max( a.y, b.y ) );

	// A refresh rebuilds the whole sidebar, so a section can appear or disappear with the choice above it.
	public override Widget CreateToolSidebar()
	{
		// This tool is up in its own right now, so a refresh belongs to this panel again - the loan ended
		// whenever the shelf that borrowed it went away.
		lent = null;

		sidebar = new ToolSidebarWidget();
		Populate();

		return sidebar;
	}

	// Finish and Cancel exist only while a chained operation is live, and they ride the footer so a long
	// form cannot push the way out of a run off the bottom of the sidebar.
	public override Widget CreateToolFooter() => new ArchRunBar( Run, FinishRun, CancelRun );

	protected virtual ArchRunState Run() => default;

	protected virtual void FinishRun() { }

	protected virtual void CancelRun() { }

	// A noun, not a gesture: the gesture belongs in the advice line.
	protected virtual string Title() => "Options";

	protected virtual string Shortcut() => null;

	// Disclosures, searches and browser heights survive a refresh by being keyed to the tool, not the widget.
	protected string Scope( string key ) => ArchSidebarState.Scope( this, key );

	// A placement tool names the kind its draft previews; the draft lives in the tree for the
	// tool's whole life and FinishDraft binds the part the placement just committed.
	protected virtual ArchKind? DraftKind => null;

	ArchDraft activeDraft;

	// The insertion target wins; without one the draft names the active building.
	protected ArchLayerRef? PlacementParent()
	{
		if ( Owner.InsertionTarget is { } target )
		{
			return target;
		}

		return Owner.LayerTree.Find( Owner.ActiveBuildingId )?.Ref;
	}

	public override void OnEnabled()
	{
		base.OnEnabled();

		BeginDraft();
	}

	// Also reached by a tool whose gesture changes the kind it is about to place: left alone, the row the
	// draft opened still names the old kind, and the next placement arrives under the wrong heading.
	protected void BeginDraft()
	{
		if ( DraftKind is { } kind )
		{
			activeDraft ??= Owner.BeginPlacement( kind, PlacementParent(), null, null, this );
		}
	}

	public override void OnDisabled()
	{
		Owner.StepOff();
		CancelDraft();
		base.OnDisabled();
	}

	protected void CancelDraft()
	{
		if ( activeDraft is not null )
		{
			Owner.CancelPlacement( activeDraft );
			activeDraft = null;
		}
	}

	protected void FinishDraft() => FinishDraft( 0 );

	protected void FinishDraft( int itemId )
	{
		if ( activeDraft is not null )
		{
			Owner.FinishPlacement( activeDraft, itemId );
			activeDraft = null;
		}
	}

	// ONE LINE, and the line is the gesture. Anything an author would otherwise be told in a paragraph standing
	// over the controls belongs in this tool's guide, behind the header's HOW TO chip, where it can be as long
	// as it needs to be and nobody has to read past it to reach step one.
	protected virtual string Advice() => null;

	protected virtual void BuildOptions( ToolSidebarWidget panel ) { }

	// Whether this tool's own controls can be pointed at a SELECTION rather than at what it last placed.
	// Off unless Adopt is overridden: borrowed without one, every control on the panel writes the seed for the
	// next drag while the author watches the picked part not move.
	// Whether this subtool takes the turn key for itself. A placement that follows the cursor does; anything
	// else leaves it for the building already standing.
	public virtual bool Turns() => false;

	public virtual bool Adopts => false;

	// Whether this tool's panel is already the right one for what was picked, which decides whether selecting a
	// layer in the stack KEEPS the tool the author was drawing with. The manifest's own answer by default: a tool
	// that authors the kind and adopts a selection has nothing to gain from being swapped for Select.
	public virtual bool Hosts( object payload )
	{
		if ( !Adopts || payload is null )
		{
			return false;
		}

		return ArchKinds.Load().Claiming( payload )?.Kind is { } claimed && ReferenceEquals( Authoring( claimed ), this );
	}

	// Selection borrows this tool's own options: picking a walkway in the stack should put the
	// walkway's roof, finish and pier controls in the shelf, not make you re-place one to reach them.
	// Adopt binds them to what is selected instead of to whatever this tool last created.
	//
	// The refresh comes from the BORROWER, and it is kept: the controls laid out here call back long after this
	// has returned, and this tool's own sidebar is not the one on screen while its shelf is being lent out.
	public void BuildAdopted( ToolSidebarWidget panel, ArchSelection picked, Action refresh )
	{
		lent = refresh;

		Adopt( picked );
		BuildOptions( panel );
	}

	Action lent;

	// Whether this panel is standing in another tool's shelf right now. A tool whose own form authors the NEXT
	// gesture needs to know, or lent out it tunes the seed while the author watches the picked part not move.
	protected bool Lent => lent is not null;

	// A placement tool edits the thing it just made; adopting points that at the selection instead.
	protected virtual void Adopt( ArchSelection picked ) { }

	// The shelf tool that authors what is selected, so its controls can be borrowed.
	public ArchSubtool Authoring( ArchKind kind )
	{
		var wanted = ArchKindsAsked.Subtool( kind );

		if ( wanted.Length == 0 )
		{
			return null;
		}

		return Owner.Tools.OfType<ArchSubtool>().FirstOrDefault( tool => tool.GetType().Name == wanted );
	}

	protected void Refresh()
	{
		if ( lent is not null )
		{
			lent();

			return;
		}

		if ( !sidebar.IsValid() )
		{
			return;
		}

		sidebar.Layout.Clear( true );
		Populate();
	}

	// The Plan Layers dock selects outside the subtool, so it needs the one refresh entry point.
	public void RefreshSidebar() => Refresh();

	void Populate()
	{
		var serves = Serves( Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis );

		ArchSidebarLayout.Header( sidebar, Title(), Icon, Shortcut(), serves ? Advice() : Refusal(), Guide() );

		// In a view it cannot honour the whole form goes, not just its enabled state.
		if ( !serves )
		{
			sidebar.Layout.AddStretchCell();
			return;
		}

		BuildOptions( sidebar );

		sidebar.Layout.AddStretchCell();
	}

	Action Guide()
	{
		if ( ArchGuides.For( this ) is not { } written )
		{
			return null;
		}

		return () => ArchGuideWindow.Open( written );
	}

	string Refusal()
	{
		var views = string.Join( ", ", Works.Select( axis => axis == ArchViewAxis.Top ? "the plan" : $"the {axis} elevation" ) );

		return $"Nothing to place from here — this tool works in {views}.";
	}

	protected static Widget Wrapped( string text ) => ArchPartUi.Wrapped( text );

}

public static class ArchPaletteUi
{
	public static Widget Row( Widget parent, ArchPalette palette, ArchSurface surface, Action changed )
	{
		var holder = new Widget( parent );
		holder.Layout = Layout.Row();
		holder.Layout.Spacing = 4;

		var label = new Label( surface.ToString() );
		label.MinimumWidth = 92;
		holder.Layout.Add( label );

		palette.TryGet( surface, out var path );

		var value = new Label( string.IsNullOrWhiteSpace( path ) ? "(inherited)" : System.IO.Path.GetFileNameWithoutExtension( path ) );
		value.MinimumWidth = 110;
		holder.Layout.Add( value );

		holder.Layout.Add( new Button( "", "image" )
		{
			Clicked = () =>
			{
				var picker = AssetPicker.Create( parent, AssetType.Material );
				picker.Window.Title = $"Material for {surface}";
				picker.OnAssetPicked = assets =>
				{
					var asset = assets.FirstOrDefault();
					if ( asset is null ) return;

					palette.Set( surface, asset.Path );
					ArchStyle.InvalidateCache();
					changed?.Invoke();
				};
				picker.Show();
			}
		} );

		// Blank = no override; the generator falls back to ArchMesh.TexelScale.
		var scale = palette.ScaleFor( surface );
		var density = new LineEdit( scale > 0f ? scale.ToString( "0.####" ) : "" )
		{
			PlaceholderText = ArchMesh.TexelScale.ToString( "0.####" ),
			MaximumWidth = 64,
			ToolTip = "Units per texel for this role. Blank inherits."
		};

		density.TextEdited += text =>
		{
			palette.SetScale( surface, float.TryParse( text, out var parsed ) ? parsed : 0f );
			ArchStyle.InvalidateCache();
			changed?.Invoke();
		};

		holder.Layout.Add( density );

		holder.Layout.Add( new Button( "", "close" )
		{
			Clicked = () =>
			{
				palette.Set( surface, null );
				density.Text = "";
				ArchStyle.InvalidateCache();
				changed?.Invoke();
			}
		} );

		holder.Layout.AddStretchCell();

		return holder;
	}
}
sunless.lib_architecture / Editor/Tool/Subtools/ArchBuildingSubtool.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public enum BuildingMode
{
	New,
	Extend,
	Prefab,
	Row,
	Saved
}

[Title( "Building" ), Icon( "domain_add" ), Group( "02" )]
public sealed class ArchBuildingSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Building;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.Floor, ArchSurface.Roof, ArchSurface.Soffit, ArchSurface.WallCap };

	BuildingMode mode;
	string archetype = "house";
	bool flipX;
	bool flipY;
	bool withRoof = true;
	bool withFloor = true;
	bool withGutters = true;
	bool withFoundation = true;
	RoofStyle roofStyle = RoofStyle.Hip;
	RidgeRun ridge = RidgeRun.Auto;
	SectionRoof wingRoof = SectionRoof.Continue;
	// Off: a dropped eave opens a void under the storey grid above it.
	float eaveDrop;

	// Seeded from the type then editable - an invisible number can't be matched by hand.
	float wallHeight;
	float roofPitch;
	bool withFrame;
	bool withFascia = true;
	bool withSoffit = true;
	bool withCeiling;

	bool seeded;

	// The saved building being stamped, its outlines worked out once, and how far round it has been turned.
	ArchBuildingAsset saved;
	ArchAssetGhost savedGhost;
	List<ArchPresetItem<ArchBuildingAsset>> savedOffered;
	int savedStamp = -1;
	int quarters;

	// What the next placement is turned by. R adds quarters on top of it, so a stamp aimed at 20 degrees still
	// turns square corners from there.
	float placementAngle;

	// Re-dresses while still the newest in the plan; anything else authored moves the counter.
	int placedRoom;
	int placedRoof;
	int placedStamp;
	bool placedMerged;

	bool Extending => mode == BuildingMode.Extend;

	bool Rowing => mode == BuildingMode.Row;

	bool Stamping => mode == BuildingMode.Saved;

	// A stamp has nothing to size, so it takes a point rather than a rectangle.
	protected override bool UsesDrag => !Stamping;

	ArchArchetype Chosen => Owner.FindArchetype( archetype );

	// One bearing for the drag and the stamp alike, so the ghost, the note and what is stood all read the same.
	float Facing => placementAngle + quarters * 90f;

	// A wing takes the bearing of the building it abuts and a row takes the street's, so neither has one to name.
	bool Aimable => !Extending && !Rowing;

	protected override string Title() => Stamping ? "Saved Building" : "Building";

	// Whether the section this drag stands will carry a roof at all - a wing answers through its own rules.
	bool Roofed => Extending ? wingRoof != SectionRoof.None : withRoof;

	protected override string Advice() => mode switch
	{
		BuildingMode.Extend => "Drag a wing onto the active building. It shares the wall it abuts, and inherits its type.",
		BuildingMode.Prefab => "Drag the plot. The type's canned layout is laid out across it in one go.",
		BuildingMode.Row => "Drag the frontage and how far back it reaches. Along a street it takes that road's verge and the units step with the curve.",
		BuildingMode.Saved => "Pick a saved building and it follows the cursor. R turns it a quarter, the Bearing box aims it at anything else; click stands it there.",
		_ => "Drag the outer shell. The chosen type decides its heights, roof and trims."
	};

	// Picking the tool starts the sequence over, so the first step is open and asking what this drag will be.
	public override void OnEnabled()
	{
		base.OnEnabled();

		ArchWorkflow.Restart( Scope( "flow" ) );
	}

	public override void OnUpdate()
	{
		base.OnUpdate();

		// The save that fills this shelf happens in the Plan Layers stack, which has no way to reach back
		// into the sidebar - so the shelf watches the count instead of waiting to be told.
		if ( Stamping && savedOffered is not null && savedStamp != ArchLayerAssets.Revision )
		{
			Refresh();
		}
	}

	// Only while a stamp is actually following the cursor: with the shelf up and nothing chosen the key belongs
	// to whatever is picked in the plan.
	public override bool Turns()
	{
		if ( !Stamping || !Placing || saved is null )
		{
			return false;
		}

		quarters = (quarters + 1) % 4;

		return true;
	}

	protected override void DrawHover( Vector2 point )
	{
		base.DrawHover( point );

		if ( !Stamping || savedGhost is not { Shapes.Count: > 0 } ghost )
		{
			return;
		}

		var map = ArchBuildingStamp.Landing( ghost, point, Facing );

		foreach ( var shape in ghost.Shapes )
		{
			ArchGhost.Prism( shape.Loop.Select( corner => map.Of( corner ) ).ToList(), shape.Bottom, shape.Top );
		}

		var span = map.Swapped( ghost.Max - ghost.Min );

		ArchGhost.Note( new Vector3( point.x + span.x, point.y + span.y, Owner.LevelHeight ),
			$"{saved.Title} — {span.x:0} x {span.y:0}, turned {Facing:0.##}°" );
	}

	protected override void OnClick( Vector2 point )
	{
		if ( !Stamping )
		{
			return;
		}

		if ( saved is null )
		{
			Log.Info( "Architecture: pick a saved building before clicking - there is nothing to stand yet." );
			return;
		}

		if ( ArchBuildingStamp.Place( Owner.Plan, saved, point, Facing ) is not { } placed )
		{
			Log.Info( $"Architecture: {saved.Title} could not be stamped there." );
			return;
		}

		Owner.ActiveBuildingId = placed.Id;
		Owner.ActiveRoomId = placed.Rooms[0].Id;

		FinishDraft( placed.Id );
		BeginDraft();
		Owner.Commit( $"Place {saved.Title}" );
	}

	protected override void DrawPreview()
	{
		if ( mode == BuildingMode.Prefab )
		{
			DrawPrefabPreview();
			return;
		}

		if ( Rowing )
		{
			DrawRowPreview();
			return;
		}

		var standard = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );
		var height = Extending ? MathF.Max( 32f, standard - MathF.Max( 0f, eaveDrop ) ) : standard;
		var placement = Resolve( DragStart, DragCurrent );

		if ( !placement.IsUsable || Extending && !placement.TouchesHost )
		{
			DrawRectPreview();
			return;
		}

		var aimed = Aimable && MathF.Abs( Facing ) > 0.001f;

		if ( aimed )
		{
			ArchGhost.Prism( Turned( placement.Min, placement.Max ), Owner.LevelHeight, Owner.LevelHeight + height );
		}
		else
		{
			ArchGhost.Volume( placement.Min, placement.Max, Owner.LevelHeight, Owner.LevelHeight + height );
		}

		ArchGhost.Note( new Vector3( placement.Max.x, placement.Max.y, Owner.LevelHeight + height ),
			aimed
				? $"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}, turned {Facing:0.##}°"
				: $"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}" );

		// The pitch ghost is drawn off a box, so a turned shell shows its footprint and stands its roof on release.
		if ( !withRoof && !Extending || aimed )
		{
			return;
		}

		var min = placement.Min;
		var max = placement.Max;
		var style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
		var pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;

		ArchGhost.Pitch( min, max, Owner.LevelHeight + height, pitch, style, ArchBuild.Ridged( ridge, min, max ) );
	}

	// From the layout's resolved rectangles, so growth and placement show before release.
	void DrawPrefabPreview()
	{
		var recipe = Owner.FindArchetype( archetype );

		if ( recipe is null || !recipe.HasLayout )
		{
			DrawRectPreview();
			return;
		}

		var plot = ResolvedPrefabPlot( recipe, DragStart, DragCurrent, out var usable );

		if ( !usable )
		{
			DrawRectPreview();
			return;
		}

		ArchGhost.Plate( plot.Min, plot.Max, Owner.LevelHeight, 8 );

		foreach ( var step in recipe.Steps )
		{
			if ( step.Kind is not (ArchStepKind.Shell or ArchStepKind.Wing or ArchStepKind.Canopy) )
			{
				continue;
			}

			plot.Resolve( step.Rect, out var min, out var max );
			ArchGhost.Volume( min, max, Owner.LevelHeight, Owner.LevelHeight + Standing( recipe, step ) );
		}

		ArchGhost.Note( new Vector3( plot.Max.x, plot.Max.y, Owner.LevelHeight + 264f ),
			$"{recipe.Title} — {plot.Size.x:0} x {plot.Size.y:0}" );
	}

	// From the one resolve the commit uses, so the units drawn are the units stood.
	void DrawRowPreview()
	{
		var row = ResolvedRow( DragStart, DragCurrent );

		if ( !row.IsUsable )
		{
			DrawRectPreview();
			return;
		}

		var standing = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );
		var storey = standing + Owner.Kit.FloorThickness;
		var pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;

		foreach ( var bay in row.Bays )
		{
			var plate = Owner.LevelHeight + standing + storey * (bay.Storeys - 1);

			ArchGhost.Volume( bay.Min, bay.Max, Owner.LevelHeight, plate );

			if ( withRoof )
			{
				ArchGhost.Pitch( bay.Min, bay.Max, plate, pitch, roofStyle, ArchBuild.Ridged( ridge, bay.Min, bay.Max ) );
			}
		}

		var last = row.Bays[^1];

		ArchGhost.Note( new Vector3( last.Max.x, last.Max.y, Owner.LevelHeight + standing ),
			row.RoadId > 0
				? $"{row.Bays.Count} units on the verge — {row.Frontage:0} x {row.Depth:0}"
				: $"{row.Bays.Count} units — {row.Frontage:0} x {row.Depth:0}" );
	}

	protected override void OnDrag( Vector2 from, Vector2 to )
	{
		if ( mode == BuildingMode.Prefab )
		{
			PlacePrefab( from, to );
			return;
		}

		if ( Rowing )
		{
			PlaceRow( from, to );
			return;
		}

		var placement = Resolve( from, to );
		var min = placement.Min;
		var max = placement.Max;

		if ( !placement.IsUsable || Extending && !placement.TouchesHost )
		{
			Log.Info( Extending
				? "Architecture: an extension must finish against an empty edge of the active building."
				: "Architecture: that drag is fully occupied. Start or finish the drag in empty grid space." );
			return;
		}

		var rules = Authored();

		// The drag names the building it joins, not the target picker - whose placeholder extends nothing.
		if ( Extending && placement.Host is { Rooms.Count: > 0 } active )
		{
			active.Archetype = Chosen?.Name ?? active.Archetype;

			Owner.ActiveBuildingId = active.Id;

			var wing = ArchBuild.Extend(
				Owner.Plan, active, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max,
				wingRoof, eaveDrop, rules.Floor ?? true, rules.Gutters ?? true, ridge );

			if ( wing is null )
			{
				Log.Info( "Architecture: that wing has no empty grid space beside the building." );
				return;
			}

			ArchArchetypeRules.Apply( wing, rules, Owner.Kit );

			Owner.ActiveRoomId = wing.Room.Id;
			CancelDraft();
			Owner.Commit();
			Remember( wing );

			return;
		}

		var building = new ArchBuilding
		{
			Id = Owner.Plan.AllocateId(),
			Name = $"{Chosen?.Title ?? "Building"}{Owner.Plan.Buildings.Count + 1}",
			Archetype = Chosen?.Name ?? "",
			GuttersEnabled = rules.Gutters ?? true
		};

		var shell = ArchBuild.Shell(
			Owner.Plan, building, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max, rules.Floor ?? true,
			withRoof ? roofStyle : null, rules.Gutters ?? true, ridge );

		if ( shell is null )
		{
			Log.Info( "Architecture: that building footprint has no empty grid space." );
			return;
		}

		ArchArchetypeRules.Apply( shell, rules, Owner.Kit );

		Owner.Plan.Units.Add( building );

		if ( Aimable && ArchHandles.Pivot( building, out var about ) )
		{
			ArchCarry.Turn( building, about, Facing );
		}

		Owner.ActiveBuildingId = building.Id;
		Owner.ActiveRoomId = shell.Room.Id;
		FinishDraft( building.Id );
		Owner.Commit();
		Remember( shell );
	}

	// The drag rectangle swung about its own centre, which is the pivot the placement turns on too - two
	// centres would show the shell in one place and stand it in another.
	static List<Vector2> Turned( Vector2 min, Vector2 max, float degrees ) => ArchFootprint.Turned( ArchFootprint.Rect( min, max ), (min + max) * 0.5f, degrees );

	List<Vector2> Turned( Vector2 min, Vector2 max ) => Turned( min, max, Facing );

	ArchRectanglePlacement Resolve( Vector2 from, Vector2 to )
	{
		return new ArchBoundaryPlacementService( Owner.Plan, Owner.Kit )
			.Outside( Owner.Level, from, to, Extending ? Owner.ActiveBuilding() : null );
	}

	void Remember( ArchSection section )
	{
		placedRoom = section.Room?.Id ?? 0;
		placedRoof = section.Roof?.Id ?? 0;
		placedStamp = Owner.Plan.NextId;
		placedMerged = section.Merged;

		Refresh();
	}

	// Shown in the panel - an untold live edit reads as the tool acting alone.
	ArchRoom Editing => placedRoom != 0 && Owner.Plan.NextId == placedStamp ? Owner.Plan.FindRoom( placedRoom ) : null;

	// Edits the last drag's room and roof in place - nothing is re-created.
	void Restyle()
	{
		if ( placedRoom == 0 || Owner.Plan.NextId != placedStamp )
		{
			return;
		}

		if ( Owner.Plan.FindRoom( placedRoom ) is not { } room )
		{
			placedRoom = 0;
			return;
		}

		var building = Owner.Plan.OwnerOf( room );
		var roof = building?.Roofs.FirstOrDefault( part => part.Id == placedRoof );

		if ( building is not null && Chosen is { } chosen )
		{
			building.Archetype = chosen.Name;
		}

		// A merged wing shares the section it folded into; style and ridge follow the host.
		if ( roof is not null && !placedMerged )
		{
			roof.Style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
			roof.RidgeAlongX = ArchBuild.Ridged( ridge, roof.Min, roof.Max );
		}

		ArchArchetypeRules.Apply( new ArchSection { Room = room, Roof = roof, Merged = placedMerged }, Authored(), Owner.Kit );

		Owner.Touch( "Restyle Section" );
	}

	void Set( Action change )
	{
		change();
		Restyle();
	}

	// A choice that decides which rows exist re-lays out only after the standing section has been re-dressed;
	// refreshing from inside the change tears down the widget still handling the click.
	void Relayout( Action change )
	{
		Set( change );
		Refresh();
	}

	// The type only seeds these - every deciding number is visible and editable.
	ArchSectionRules Authored()
	{
		var type = Chosen is { } chosen ? (Extending ? chosen.Wing : chosen.Shell) : new ArchSectionRules();

		return new ArchSectionRules
		{
			WallHeight = wallHeight,
			Ridge = ridge,
			RoofPitch = roofPitch,
			Overhang = type.Overhang,
			FrameSpacing = type.FrameSpacing,
			Floor = withFloor,
			Foundation = withFoundation,
			Ceiling = withCeiling,
			Gutters = withGutters,
			Fascia = withFascia,
			Soffit = withSoffit,
			Frame = withFrame,
			FloorBoards = type.FloorBoards,
			FloorBoardYaw = type.FloorBoardYaw,
			Palette = type.Palette
		};
	}

	float Standing( ArchArchetype recipe, ArchArchetypeStep step )
	{
		if ( step.Kind == ArchStepKind.Canopy )
		{
			return step.HeadHeight;
		}

		var type = step.Kind == ArchStepKind.Wing ? recipe.Wing : recipe.Shell;
		var height = step.Rules.WallHeight > 1f ? step.Rules.WallHeight : type.WallHeight;

		return ArchArchetypeRules.Standing( height, Owner.Kit );
	}

	void PlacePrefab( Vector2 from, Vector2 to )
	{
		var recipe = Owner.FindArchetype( archetype );

		if ( recipe is null || !recipe.HasLayout )
		{
			Log.Info( $"Architecture: '{archetype}' has no canned layout - use New with the type chosen instead. Types live in Assets/{ArchStorage.ArchetypeDirectory}." );
			return;
		}

		var plot = ResolvedPrefabPlot( recipe, from, to, out var usable );

		if ( !usable )
		{
			Log.Info( $"Architecture: {recipe.Title} has no empty grid space on that plot." );
			return;
		}

		var placed = ArchArchetypeBuild.Place( Owner.Plan, Owner.Kit, recipe, plot, Owner.Level, Owner.LevelHeight, Owner.PillarTypes );

		if ( placed is null )
		{
			Log.Info( $"Architecture: {recipe.Title} laid out nothing on that plot." );
			return;
		}

		foreach ( var note in placed.Skipped )
		{
			Log.Info( $"Architecture: {recipe.Title} skipped {note}." );
		}

		Owner.ActiveBuildingId = placed.Building.Id;
		Owner.ActiveRoomId = placed.Building.Rooms[0].Id;
		Owner.Commit( $"Place {recipe.Title}" );
	}

	ArchPlot ResolvedPrefabPlot( ArchArchetype recipe, Vector2 from, Vector2 to, out bool usable )
	{
		return new ArchBoundaryPlacementService( Owner.Plan, Owner.Kit )
			.PlotOutside( Owner.Level, from, to, recipe.MinimumPlot, flipX, flipY, out usable );
	}

	void PlaceRow( Vector2 from, Vector2 to )
	{
		if ( Chosen is not { } type )
		{
			Log.Info( $"Architecture: a row takes its unit width from a building type, and none are in Assets/{ArchStorage.ArchetypeDirectory}." );
			return;
		}

		var row = ResolvedRow( from, to );

		if ( !row.IsUsable )
		{
			Log.Info( $"Architecture: that drag names no frontage a {type.Title} unit fits along. Drag further along the street, or deeper back from it." );
			return;
		}

		var placed = ArchRowPlacement.Stand(
			Owner.Plan, Owner.Kit, row, type, Authored(), Owner.Level, Owner.LevelHeight,
			withRoof ? roofStyle : null, ridge );

		if ( placed is null )
		{
			Log.Info( "Architecture: every bay of that row landed on occupied ground." );
			return;
		}

		if ( row.Blocked > 0 )
		{
			Log.Info( $"Architecture: {row.Blocked} of the row's bays stood on occupied ground and were left out." );
		}

		Owner.ActiveBuildingId = placed.Buildings[0].Id;
		Owner.ActiveRoomId = placed.Buildings[0].Rooms[0].Id;
		CancelDraft();
		Owner.Commit( $"Place Street Row of {placed.Buildings.Count}" );
	}

	// The road comes from the tool's own memoised lookup - Nearest walks a whole dense curve, and the ghost asks per frame.
	ArchRowShape ResolvedRow( Vector2 from, Vector2 to )
	{
		var road = Owner.RoadAt( from, ArchRowPlacement.Verge, out _ ) ?? Owner.RoadAt( to, ArchRowPlacement.Verge, out _ );

		return ArchRowPlacement.Resolve(
			Owner.Plan, Owner.Kit, Chosen, Owner.Level, from, to,
			road, road is null ? null : Owner.Resolved( road.Id, road.Curve ),
			withRoof && roofStyle == RoofStyle.Flat );
	}

	// The layout drawing leads the panel, because the shape of a shell already standing is the one thing this tool's
	// drag cannot go back and fix. Nothing to work on until a building stands, so it says so rather than opening empty.
	void Layout( ToolSidebarWidget panel )
	{
		var building = Owner.ActiveBuilding();
		var button = new Button.Primary( "Toggle Building Layout", "architecture" )
		{
			Clicked = () => ArchBuildingWindow.Toggle( Owner, building )
		};

		button.Enabled = building is not null;
		button.ToolTip = building is null
			? "Drag a shell first — the layout drawing works on a building that stands"
			: $"The plan of {building.Name}, storey by storey: move a corner, push a run, cut a corner off at an angle";

		panel.AddGroup( "Layout" ).AddRow().Add( button );
	}

	// The gesture as a sequence: which placement, what is being placed, what the shell carries, what covers it,
	// its numbers, and the bearing it stands at. All of those grids standing open at once is what made the first
	// tool on the shelf a wall of icons.
	//
	// The layout drawing leads it from OUTSIDE the sequence, because it works on a shell already standing -
	// the one shape the next drag cannot go back and fix.
	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		Layout( panel );

		Inherit();

		// Asked whether or not the shelf is the step being shown: the stamp following the cursor is read off
		// this list, and a closed step never builds.
		if ( Stamping && (savedOffered is null || savedStamp != ArchLayerAssets.Revision) )
		{
			RereadSaved();
		}

		if ( Editing is { } room )
		{
			panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {room.Name} live. Place anything else and these go back to seeding the next drag." ) );
		}

		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		flow.Step( "Mode", ModeName, ModeGlyph, PickMode );

		if ( Stamping )
		{
			flow.Step( "Saved building", saved?.Title ?? "None", "inventory_2", step => Stamps( panel, step ) );
		}
		else
		{
			flow.Step( "Type", Chosen?.Title ?? "None", "domain", step => Types( panel, step ) );
		}

		// A canned layout decides its own steps and would be contradicted by the shell's, and a stamp stands
		// exactly what it was saved as - so neither is asked anything past what it is and which way it faces.
		if ( mode == BuildingMode.Prefab )
		{
			flow.Step( "Mirror", MirrorName, "flip", PickMirror );
		}
		else if ( !Stamping )
		{
			flow.Step( "Include", IncludeName, "checklist", PickInclude );

			if ( Covered )
			{
				flow.Step( Extending ? "Wing roof" : "Roof", RoofName, RoofGlyph, PickRoof );
			}

			if ( Wants( ArchOptionGroup.Section ) )
			{
				flow.Step( "Structure", StructureName, "straighten", BuildStructure );
			}
		}

		// Last wherever it is asked. A bearing is typed rather than picked, so the step can never say it has
		// been answered - anywhere but the end it stops the sequence dead on a box nobody has to fill in.
		if ( Aimable )
		{
			flow.Step( "Bearing", BearingName, "explore", BuildBearing );
		}
	}

	// Whether a roof is asked about at all - a wing answers through its type's own rules, and a shell whose
	// roof was switched off in Include has nothing left to style.
	bool Covered => Extending ? Wants( ArchOptionGroup.WingRoof ) : withRoof;

	bool Wants( ArchOptionGroup group ) => Chosen?.Wants( group ) ?? true;

	string ModeName => mode switch
	{
		BuildingMode.Extend => "Extend",
		BuildingMode.Prefab => "Canned layout",
		BuildingMode.Row => "Street row",
		BuildingMode.Saved => "Saved building",
		_ => "New building"
	};

	string ModeGlyph => mode switch
	{
		BuildingMode.Extend => "add_home_work",
		BuildingMode.Prefab => "auto_awesome_motion",
		BuildingMode.Row => "view_column",
		BuildingMode.Saved => "inventory_2",
		_ => "domain_add"
	};

	void PickMode( ArchWorkflowStep step )
	{
		using var grid = ArchIconGrid.In( step.Layout );

		Mode( grid, step, BuildingMode.New, "mode_new_building", "New building — drag the outer shell", "domain_add" );
		Mode( grid, step, BuildingMode.Extend, "mode_extend_building", "Extend the active building — drag a wing onto it", "add_home_work" );
		Mode( grid, step, BuildingMode.Prefab, "mode_archetype", "Place the type's canned layout — one drag lays the whole unit out", "auto_awesome_motion" );
		Mode( grid, step, BuildingMode.Row, "mode_street_row", "Street row — drag the frontage and its depth; one party-walled unit per bay, at the type's own unit width", "view_column" );
		Mode( grid, step, BuildingMode.Saved, "mode_saved_building", "Saved building — stamp one you authored earlier back down, exactly as it stands", "inventory_2" );
	}

	void Mode( ArchIconGrid grid, ArchWorkflowStep step, BuildingMode choice, string slug, string tooltip, string fallback )
	{
		grid.Pick( tooltip, slug, fallback, mode == choice, () =>
		{
			mode = choice;
			// Cleared, not reseeded - Extend picks up the active building's own type first.
			seeded = false;

			step.Chose();
		} );
	}

	string MirrorName => flipX && flipY ? "X and Y" : flipX ? "Across X" : flipY ? "Across Y" : "None";

	void PickMirror( ArchWorkflowStep step )
	{
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Toggle( "Mirror the layout across X", "flip_x", "swap_horiz", flipX, value => flipX = value );
		grid.Toggle( "Mirror the layout across Y", "flip_y", "swap_vert", flipY, value => flipY = value );
	}

	string IncludeName
	{
		get
		{
			var carried = new List<string>();

			if ( Wants( ArchOptionGroup.Foundation ) )
			{
				carried.Add( withFoundation ? "Raised" : "Nested" );
			}

			if ( withFloor )
			{
				carried.Add( "floor" );
			}

			if ( Roofed && withGutters )
			{
				carried.Add( "gutters" );
			}

			if ( !Extending && !withRoof )
			{
				carried.Add( "no roof" );
			}

			return carried.Count > 0 ? string.Join( ", ", carried ) : "Shell only";
		}
	}

	// Foundation and the include flags are one question - what this shell is made of - and were two boxes of
	// icons asking it. Two grids inside one step, because a Pick is exclusive within its own grid.
	void PickInclude( ArchWorkflowStep step )
	{
		if ( Wants( ArchOptionGroup.Foundation ) )
		{
			using var footing = ArchIconGrid.In( step.Layout );

			footing.Pick( "Raised foundation — the plinth stands proud of grade and the building sits up on it",
				"foundation_raised", "vertical_align_top", withFoundation, () => Answer( () => withFoundation = true, step ) );

			footing.Pick( "Nested foundation — the footing is buried and the floor sits on grade",
				"foundation_nested", "vertical_align_bottom", !withFoundation, () => Answer( () => withFoundation = false, step ) );
		}

		using var grid = ArchIconGrid.In( step.Layout );

		grid.Toggle( "Floor slab", "opt_floor_slab", "layers", withFloor, value => Set( () => withFloor = value ) );

		if ( Roofed )
		{
			grid.Toggle( "Gutters and downpipes", "opt_gutters", "water_damage", withGutters, value => Set( () => withGutters = value ) );
		}

		if ( !Extending )
		{
			grid.Toggle( "Roof", "opt_roof", "roofing", withRoof, value => Relayout( () => withRoof = value ) );
		}
	}

	string RoofName => Extending ? wingRoof.ToString() : roofStyle.ToString();

	string RoofGlyph => Extending ? Fallback( wingRoof ) : ArchIcons.RoofStyleGlyph( roofStyle );

	void PickRoof( ArchWorkflowStep step )
	{
		if ( Extending )
		{
			PickWingRoof( step );

			return;
		}

		if ( Wants( ArchOptionGroup.RoofStyle ) )
		{
			using var grid = ArchIconGrid.In( step.Layout );

			foreach ( var value in Enum.GetValues<RoofStyle>() )
			{
				var captured = value;

				grid.Pick( captured.ToString(), ArchIcons.RoofStyleSlug( captured ), ArchIcons.RoofStyleGlyph( captured ), roofStyle == captured,
					() => Answer( () => roofStyle = captured, step ) );
			}
		}

		Ridge( step.Layout );
	}

	void PickWingRoof( ArchWorkflowStep step )
	{
		using ( var grid = ArchIconGrid.In( step.Layout ) )
		{
			foreach ( var value in Enum.GetValues<SectionRoof>() )
			{
				var captured = value;

				grid.Pick( Describe( captured ), $"wing_{captured}".ToLowerInvariant(), Fallback( captured ), wingRoof == captured,
					() => Answer( () => wingRoof = captured, step ) );
			}
		}

		// Continue takes the host's eave, and no roof has no eave to drop.
		if ( wingRoof is not (SectionRoof.Continue or SectionRoof.None) )
		{
			step.Layout.Add( ArchPartUi.Number( "Eave drop", eaveDrop, 0f, value => Set( () => eaveDrop = value ) ) );
		}

		Ridge( step.Layout );
	}

	// Advancing IS the re-lay-out: the answer just given decides which rows exist below it, and refreshing from
	// inside the change would tear down the widget still handling the click.
	void Answer( Action change, ArchWorkflowStep step )
	{
		Set( change );

		step.Chose();
	}

	string StructureName => $"{ArchArchetypeRules.Standing( wallHeight, Owner.Kit ):0} high";

	// The type's own numbers, editable - a wing must match the shell it joins.
	void BuildStructure( ArchWorkflowStep step )
	{
		step.Layout.Add( ArchPartUi.Number( "Wall height", wallHeight, 0f, value => Set( () => wallHeight = value ) ) );

		// Pitch describes a deck; with no roof over the section there is none.
		if ( Roofed )
		{
			step.Layout.Add( ArchPartUi.Number( "Roof pitch", roofPitch, 0f, value => Set( () => roofPitch = value ) ) );
		}

		using var grid = ArchIconGrid.In( step.Layout );

		grid.Toggle( "Ceiling under the roof", "opt_ceiling", "square", withCeiling, value => Set( () => withCeiling = value ) );

		if ( !Roofed )
		{
			return;
		}

		grid.Toggle( "Exposed rafters and purlins under the deck", "opt_roof_frame", "reorder", withFrame, value => Set( () => withFrame = value ) );
		grid.Toggle( "Fascia along the eaves", "opt_fascia", "border_bottom", withFascia, value => Set( () => withFascia = value ) );
		grid.Toggle( "Lined soffit under the overhang", "opt_soffit", "flip_to_back", withSoffit, value => Set( () => withSoffit = value ) );
	}

	string BearingName => MathF.Abs( Facing ) < 0.001f ? "Square on" : $"{Facing:0.##}°";

	// What the next drag stands at. R still turns quarters on top of it while a stamp follows the cursor, so
	// the box is the bearing and the key is the corner.
	void BuildBearing( ArchWorkflowStep step )
	{
		step.Layout.Add( ArchPartUi.Angle( "Angle", placementAngle, value => placementAngle = value ) );
		step.Layout.Add( ArchPartUi.Increment( Refresh ) );
	}

	// Picking re-types it - being unable to change your mind is worse than a mismatch. The designer rides the
	// browser's own header, where every other authored kit lives; a full-width button of its own put the way
	// into the designer above the thing it designs.
	//
	// The shelf and the designer hang off the SIDEBAR rather than off the step, because a step's widget is torn
	// down on the next refresh and a modal parented to one would go with it.
	void Types( ToolSidebarWidget panel, ArchWorkflowStep step )
	{
		var offered = Offered();
		var removed = ArchStorage.HiddenArchetypes();

		if ( offered.Count == 0 && removed.Count == 0 )
		{
			step.Layout.Add( ArchSidebarLayout.Advice( mode == BuildingMode.Prefab
				? "No building type carries a canned layout. Use New with a type chosen instead."
				: $"No building types. Authored ones live in Assets/{ArchStorage.ArchetypeDirectory}." ) );

			return;
		}

		var shelf = new ArchPresetBrowser<ArchArchetype>( panel, Owner.Kit, Scope( "types" ), "Building types" )
		{
			Chosen = type => string.Equals( archetype, type.Name, StringComparison.OrdinalIgnoreCase ),
			Choose = type => Adopt( type, step ),
			Design = () => new ArchBuildingDesigner( panel, Owner, Chosen, designed => ApplyDesigned( designed, step ) ).Show(),
			Reload = Reread,
			Remove = item => Remove( item.Value ),
			Restore = () =>
			{
				ArchStorage.RestoreArchetypes();
				Reread();
			}
		};

		shelf.DesignButton.Visible = true;
		shelf.DesignButton.ToolTip = "Design a building type";
		shelf.ReloadButton.Visible = true;
		shelf.RestoreButton.Visible = removed.Count > 0;
		shelf.RestoreButton.ToolTip = $"Bring back {string.Join( ", ", removed )}";
		shelf.Set( offered );

		step.Add( shelf );
	}

	void Stamps( ToolSidebarWidget panel, ArchWorkflowStep step )
	{
		if ( savedOffered.Count == 0 )
		{
			step.Layout.Add( ArchSidebarLayout.Advice( "No saved buildings yet. Right-click a building in the Plan Layers stack and Save, and it turns up here." ) );

			return;
		}

		var shelf = new ArchPresetBrowser<ArchBuildingAsset>( panel, Owner.Kit, Scope( "stamps" ), "Saved buildings" )
		{
			Chosen = asset => saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ),
			Choose = asset =>
			{
				Hold( asset );

				step.Chose();
			},
			Reload = RereadSaved,
			Remove = item => Discard( item.Value )
		};

		shelf.ReloadButton.Visible = true;
		shelf.Set( savedOffered );

		step.Add( shelf );
	}

	List<ArchPresetItem<ArchBuildingAsset>> Kept()
	{
		return ArchLayerAssets.Buildings()
			.Select( asset => new ArchPresetItem<ArchBuildingAsset>
			{
				Value = asset,
				Name = asset.Title,
				Detail = ArchBuildingStamp.Describe( asset ),
				Identity = ArchBuildingStamp.Identity( asset ),
				Category = "Saved",
				Glyph = "home_work",
				Tags = asset.Name,
				View = ArchPresetPreview.Quarter,
				Focus = ArchPreviewFocus.Whole,
				Recipe = () => ArchBuildingStamp.Stage( asset )
			} )
			.ToList();
	}

	void Discard( ArchBuildingAsset asset )
	{
		if ( !ArchLayerAssets.RemoveBuilding( asset.Name ) )
		{
			Log.Warning( $"Architecture: could not remove the saved building '{asset.Title}'." );

			return;
		}

		if ( saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ) )
		{
			Hold( null );
		}

		RereadSaved();
		Refresh();
	}

	// The outlines are worked out here and nowhere else, because the hover asks for them every frame.
	void Hold( ArchBuildingAsset asset )
	{
		saved = asset;
		savedGhost = asset is null ? null : ArchBuildingStamp.Ghost( asset );
	}

	void RereadSaved()
	{
		var chosen = saved?.Name;

		savedStamp = ArchLayerAssets.Revision;
		savedOffered = Kept();

		Hold( savedOffered.Select( item => item.Value ).FirstOrDefault( asset => string.Equals( asset.Name, chosen, StringComparison.OrdinalIgnoreCase ) ) );
	}

	void Reread()
	{
		Owner.ReloadArchetypes();
		Seed();
		Refresh();
	}

	void Remove( ArchArchetype type )
	{
		if ( !ArchStorage.RemoveArchetype( type.Name ) )
		{
			Log.Warning( $"Architecture: could not remove the building type '{type.Name}'." );
			return;
		}

		Owner.ReloadArchetypes();

		// A drag reads its heights off the chosen type, so removing that one has to leave another standing.
		if ( Owner.FindArchetype( archetype ) is null )
		{
			archetype = Owner.Archetypes.FirstOrDefault()?.Name ?? "";
		}

		Seed();
		Refresh();
	}

	// A canned layout is the whole gesture in Prefab mode, so a type without one is not a choice there.
	List<ArchPresetItem<ArchArchetype>> Offered()
	{
		return Owner.Archetypes
			.Where( type => mode != BuildingMode.Prefab || type.HasLayout )
			.Select( type => new ArchPresetItem<ArchArchetype>
			{
				Value = type,
				Name = type.Title,
				Detail = ArchArchetypeStage.Describe( type ),
				Identity = ArchArchetypeStage.Identity( type ),
				Category = type.HasLayout ? "Has layout" : "Shell",
				Badge = type.HasLayout ? "layout" : null,
				Glyph = type.Icon,
				Tags = $"{type.Name} {type.Description}",
				View = ArchPresetPreview.Quarter,
				Focus = ArchPreviewFocus.Whole,
				Recipe = () => ArchArchetypeStage.Of( type, Owner.Kit )
			} )
			.ToList();
	}

	// Shown once - re-reading it every rebuild would undo the re-type pick.
	void Inherit()
	{
		if ( !Extending || seeded )
		{
			return;
		}

		if ( Owner.ActiveBuilding()?.Archetype is { Length: > 0 } existing && Owner.FindArchetype( existing ) is not null )
		{
			archetype = existing;
		}

		Seed();
	}

	// Seeds every switch the type has an opinion about, so the sidebar matches the drag.
	void Adopt( ArchArchetype type, ArchWorkflowStep step )
	{
		archetype = type.Name;

		Seed();
		Restyle();

		step.Chose();
	}

	// Adopts like a grid pick, then re-dresses the standing section.
	void ApplyDesigned( ArchArchetype designed, ArchWorkflowStep step )
	{
		archetype = designed.Name;

		Owner.RegisterArchetype( designed );
		Seed();
		Restyle();

		step.Chose();
	}

	void Seed()
	{
		if ( Chosen is not { } type )
		{
			return;
		}

		var rules = Extending ? type.Wing : type.Shell;

		if ( rules.Roof is { } style )
		{
			roofStyle = style;
		}

		wingRoof = type.WingRoof;
		eaveDrop = type.WingEaveDrop;
		wallHeight = rules.WallHeight;
		roofPitch = rules.RoofPitch;
		withGutters = rules.Gutters ?? true;
		withFloor = rules.Floor ?? true;
		withFoundation = rules.Foundation ?? true;
		withFascia = rules.Fascia ?? true;
		withSoffit = rules.Soffit ?? true;
		withCeiling = rules.Ceiling ?? false;
		withFrame = rules.Frame ?? false;

		seeded = true;
	}

	// Its own grid inside whichever roof section called it - a ridge direction and a roof style are two
	// exclusive choices, and sharing one grid would unlight the style when a direction was picked.
	void Ridge( Layout roof )
	{
		if ( !Wants( ArchOptionGroup.Ridge ) || !Roofed
			|| !ArchRoofPlane.Ridged( Extending ? ArchBuild.Winged( wingRoof ) : roofStyle ) )
		{
			return;
		}

		using var grid = ArchIconGrid.In( roof );

		foreach ( var value in Enum.GetValues<RidgeRun>() )
		{
			var captured = value;

			grid.Pick( ArchIcons.RidgeAdvice( captured ), ArchIcons.RidgeSlug( captured ), ArchIcons.RidgeGlyph( captured ), ridge == captured,
				() => Set( () => ridge = captured ) );
		}
	}

	static string Fallback( SectionRoof choice ) => choice switch
	{
		SectionRoof.Continue => "merge_type",
		SectionRoof.Hip => "roofing",
		SectionRoof.Gable => "change_history",
		SectionRoof.Capped => "crop_din",
		_ => "block"
	};

	static string Describe( SectionRoof choice ) => choice switch
	{
		SectionRoof.Continue => "Continue the roof (one hip, valleys)",
		SectionRoof.Hip => "Own hip, stepped down",
		SectionRoof.Gable => "Own gable, stepped down",
		SectionRoof.Capped => "Flat with a parapet cap",
		_ => "No roof"
	};
}
sunless.lib_architecture / Editor/Tool/Subtools/ArchOpeningUi.cs
Editor library
using System;
using System.Linq;
using Editor;

namespace Sunless.Architecture;

// The drag options both opening tools share, laid out once so the two sidebars cannot drift apart.
public static class ArchOpeningUi
{
	// The whole furniture control: one exclusive pick in the grid the kind's other options already stand in.
	// What a kind may carry comes from ArchOpeningKinds and the pitch, proud and frame are kit stock, so there
	// is nothing else here to offer.
	public static void Furniture( ArchIconGrid grid, OpeningKind kind, OpeningFurniture fitted, Action<OpeningFurniture> chosen )
	{
		var offered = kind.Furnishings().ToList();

		if ( offered.Count == 0 )
		{
			return;
		}

		grid.Pick( Advice( OpeningFurniture.None ), Slug( OpeningFurniture.None ), Glyph( OpeningFurniture.None ),
			fitted == OpeningFurniture.None, () => chosen( OpeningFurniture.None ) );

		foreach ( var furniture in offered )
		{
			var picked = furniture;

			grid.Pick( Advice( picked ), Slug( picked ), Glyph( picked ), fitted == picked, () => chosen( picked ) );
		}
	}

	// What stands OVER the head, offered in the same grid the furniture is: both are things a hole wears, and a
	// second panel for the one above it would only be the same control twice.
	public static void Hood( ArchIconGrid grid, OpeningKind kind, OpeningHood worn, Action<OpeningHood> chosen )
	{
		var offered = kind.Hoods().ToList();

		if ( offered.Count == 0 )
		{
			return;
		}

		grid.Pick( Advice( OpeningHood.None ), Slug( OpeningHood.None ), Glyph( OpeningHood.None ),
			worn == OpeningHood.None, () => chosen( OpeningHood.None ) );

		foreach ( var hood in offered )
		{
			var picked = hood;

			grid.Pick( Advice( picked ), Slug( picked ), Glyph( picked ), worn == picked, () => chosen( picked ) );
		}
	}

	static string Slug( OpeningHood hood ) => $"opt_hood_{hood}".ToLowerInvariant();

	static string Advice( OpeningHood hood ) => hood switch
	{
		OpeningHood.Cornice => "Moulded hood on corbels over the head",
		OpeningHood.Pediment => "Hood under a gable over the head",
		_ => "Bare head"
	};

	static string Glyph( OpeningHood hood ) => hood switch
	{
		OpeningHood.Cornice => "horizontal_split",
		OpeningHood.Pediment => "change_history",
		_ => "block"
	};

	// Named for the answer line a workflow row reads back, so the tile and the closed step agree.
	public static string Name( OpeningFurniture furniture ) => furniture switch
	{
		OpeningFurniture.Bars => "Burglar bars",
		OpeningFurniture.Boarded => "Boarded over",
		OpeningFurniture.Shutter => "Roller shutter",
		OpeningFurniture.Gate => "Security gate",
		OpeningFurniture.Leaves => "Louvred shutters",
		_ => "Nothing"
	};

	static string Slug( OpeningFurniture furniture ) => $"opt_furniture_{furniture}".ToLowerInvariant();

	static string Advice( OpeningFurniture furniture ) => furniture switch
	{
		OpeningFurniture.Bars => "Burglar bars across it",
		OpeningFurniture.Boarded => "Boarded over with planks",
		OpeningFurniture.Shutter => "Roller shutter down over it",
		OpeningFurniture.Gate => "Security gate across it",
		OpeningFurniture.Leaves => "Louvred shutters hinged either side",
		_ => "Nothing over it"
	};

	// Classic Material Icons only - a glyph the editor's older set does not know draws an empty box.
	public static string Glyph( OpeningFurniture furniture ) => furniture switch
	{
		OpeningFurniture.Bars => "fence",
		OpeningFurniture.Boarded => "table_rows",
		OpeningFurniture.Shutter => "density_small",
		OpeningFurniture.Gate => "grid_on",
		OpeningFurniture.Leaves => "menu_open",
		_ => "block"
	};
}
sunless.lib_architecture / Editor/Tool/Subtools/ArchPorchSubtool.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

[Title( "Porch" ), Icon( "deck" ), Group( "13" )]
public sealed class ArchPorchSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Porch;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.Deck, ArchSurface.Railing, ArchSurface.Baseboard, ArchSurface.Roof, ArchSurface.Soffit };

	readonly ArchPorchPart draft = new() { GradeHeight = -24f };

	protected override string Title() => "Porch";

	protected override string Advice() => "Drag the deck over the wall it hangs off.";

	public override void OnEnabled()
	{
		base.OnEnabled();

		ArchWorkflow.Restart( Scope( "flow" ) );
	}

	// Resolved from the shape, not the drag, so the ghost matches the generator.
	protected override void DrawPreview()
	{
		var min = Min( DragStart, DragCurrent );
		var max = Max( DragStart, DragCurrent );
		var room = Owner.RoomAt( (min + max) * 0.5f, out var building );
		var deck = Owner.LevelHeight;
		var legs = ArchPorch.Preview( building, room, Owner.Kit, min, max, draft.Standing, out var standing, out var joining );

		if ( legs.Count == 0 )
		{
			ArchGhost.Plate( min, max, deck, 8 );
			ArchGhost.Note( new Vector3( max.x, max.y, deck + 60f ), "porch — nowhere to stand a deck there" );
			return;
		}

		var sketch = Sketch( legs, standing );
		var shape = ArchPorchShape.Resolve( sketch, room, building, Owner.Kit );
		var head = shape.Head;
		var rail = deck + Owner.Kit.PorchRailHeight;

		foreach ( var leg in legs )
		{
			if ( draft.Deck )
			{
				ArchGhost.Volume( leg.Min, leg.Max, deck - draft.DeckDrop, deck );
			}

			ArchGhost.Plate( leg.Min, leg.Max, deck, 8 );
		}

		foreach ( var bay in ArchPorchGen.Bays( shape, sketch, Owner.Kit ) )
		{
			if ( draft.Posts )
			{
				ArchGhost.Post( bay.From.Point, Owner.Kit.PorchPostSize, deck, head );

				if ( bay.Last )
				{
					ArchGhost.Post( bay.To.Point, Owner.Kit.PorchPostSize, deck, head );
				}
			}

			if ( draft.Railings && !bay.Gated )
			{
				Gizmo.Draw.Line( bay.From.Raised.WithZ( rail ), bay.To.Raised.WithZ( rail ) );
			}
		}

		if ( draft.Roofed )
		{
			ArchGhost.Ring( shape.Footprint, head + Owner.Kit.PorchBeamDepth );
		}

		ArchGhost.Note( new Vector3( max.x, max.y, head ), Note( legs, standing, joining ) );
	}

	static string Note( IReadOnlyList<ArchPorchLeg> legs, PorchStanding standing, ArchPorchPart joining )
	{
		if ( joining is not null )
		{
			return $"porch — joining {joining.Name}";
		}

		if ( standing == PorchStanding.Free )
		{
			return "porch — free-standing, no wall to lean on";
		}

		return legs.Count > 1 ? $"porch — {legs.Count} legs, wrapping the corner" : "porch";
	}

	// Unattached to the plan, so the ghost resolves exactly what Attach will - the gates in its railing
	// included, which is why it carries the flight the placement is about to seed.
	ArchPorchPart Sketch( List<ArchPorchLeg> legs, PorchStanding standing )
	{
		var deck = Owner.LevelHeight;

		var sketch = new ArchPorchPart
		{
			Legs = legs,
			Standing = standing,
			BaseHeight = deck,
			GradeHeight = deck - draft.DeckDrop,
			HeadHeight = MathF.Max( 60f, draft.HeadHeight ),
			PostSpacing = MathF.Max( 32f, Owner.Kit.PorchPostSize * 8f ),
			Deck = draft.Deck,
			Posts = draft.Posts,
			Rafters = draft.Rafters,
			Braces = draft.Braces,
			Pitch = draft.Pitch,
			Railings = draft.Railings,
			Steps = draft.Steps,
			Roofed = draft.Roofed
		};

		return sketch;
	}

	protected override void OnDrag( Vector2 from, Vector2 to )
	{
		var min = Min( from, to );
		var max = Max( from, to );

		if ( (max - min).Length < 24f )
		{
			return;
		}

		var room = Owner.RoomAt( (min + max) * 0.5f, out var building );
		var placement = ArchPorch.Attach( Owner.Plan, building, room, Owner.Kit, min, max, draft );

		if ( placement is null )
		{
			Log.Info( "Architecture: no room under that drag for a porch to stand in." );
			return;
		}

		// A porch is a cluster of legs, not one part - the draft's work is done either way.
		FinishDraft();
		Owner.Commit( "Create Porch" );
	}

	// One list: what the deck leans on and whether anything covers it - both picks - and then the three typed rows
	// that can never answer. A porch is a cluster of legs rather than one part, so nothing here is ever pointed at
	// a selection: a standing porch is edited from its own sheet in Select.
	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		flow.Step( "Standing", ArchPorchUi.StandingName( draft ), ArchPorchUi.StandingGlyph( draft ), step =>
			ArchPorchUi.Standing( step.Layout, draft, step.Chose, null ) );

		flow.Step( "Roof", draft.Roofed ? "Roofed" : "Open", draft.Roofed ? "roofing" : "deck", step =>
			ArchPorchUi.Roof( step.Layout, draft, step.Chose, null ) );

		flow.Step( "Include", IncludeName, "checklist", step => ArchPorchUi.Include( step.Layout, draft, null ) );

		flow.Step( "Frame", FrameName, "view_column", step => ArchPorchUi.Frame( step.Layout, draft, null ) );

		flow.Step( "Placement", $"{draft.DeckDrop:0} drop", "height", step => ArchPorchUi.Placement( step.Layout, draft, null ) );
	}

	string IncludeName => ArchPartUi.Listed(
		(draft.Deck, "Deck"), (draft.Rafters, "Rafters"), (draft.Braces, "Braces"), (draft.Railings, "Railing"), (draft.Door, "Door") );

	string FrameName => ArchPartUi.Listed( (draft.Plinth, "Plinth"), (draft.Posts, "Posts"), (draft.Beam, "Beam") );
}
sunless.lib_architecture / Editor/Tool/Subtools/ArchSpanSubtool.cs
Editor library
using System;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// A span crosses between two piers, so it belongs to the tool that stands them: this is the Pillars subtool's
// third gesture, composed the way ArchFixtureSubtool is composed by the bool tool, and it holds no shelf entry
// of its own. Keeping the form in its own file keeps the two-click bridge out of the placement tool it rides in.
[Title( "Spans" ), Icon( "bridge" ), Group( "09" )]
public sealed class ArchSpanSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected override ArchKind? DraftKind => ArchKind.Span;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.PillarCap };

	ArchSpanPart draft = ArchSpanMemo.Recall();

	ArchSpanEnd taken;

	public ArchSpanPart LastPlaced { get; private set; }

	public ArchKind? MergedDraftKind => DraftKind;

	public string MergedAdvice => Advice();

	public ArchRunState MergedRun => Run();

	public bool MergedAdjusts => Adjusts;

	public void MergedCancel() => CancelRun();

	public void MergedHover( Vector2 point ) => DrawHover( point );

	// The host does the draft bookkeeping and the commit, exactly as the bool tool does for a fixture: this
	// returns what it stood and nothing else, so there is one place a placement is finished.
	public ArchSpanPart MergedClick( Vector2 point )
	{
		LastPlaced = null;

		OnClick( point );

		return LastPlaced;
	}

	protected override bool UsesDrag => false;

	// Only over a span. The selection outlives the tool that made it, so an unconditional yes put the LAST
	// pillar's box dragger on screen the moment this tool was picked - and the gizmo takes the press first, so
	// every click meant for a pier went into a widget belonging to something else.
	protected override bool Adjusts => Owner.Picked?.Item is ArchSpanPart;

	protected override string Title() => "Spans";

	protected override string Advice() => taken is null
		? "Click a column or a wall to take one end. The nearest column within reach wins, so an eyeballed click still lands on the pier."
		: "Click the far column or wall and it bridges them. An end that lands on nothing stays where it was dropped.";

	protected override ArchRunState Run() => new( taken is not null, "one end taken" );

	protected override void CancelRun() => taken = null;

	protected override void DrawHover( Vector2 point )
	{
		var room = Owner.RoomOnLevel( point );

		if ( room is null )
		{
			base.DrawHover( point );

			return;
		}

		var landing = ArchSpanShape.Anchored( Owner.Plan, room, point );

		Pier( room, landing, false );

		if ( taken is null )
		{
			return;
		}

		Pier( room, taken, true );
		Reaching( room, taken, landing );
	}

	protected override void OnClick( Vector2 point )
	{
		var room = Owner.RoomOnLevel( point );

		if ( room is null )
		{
			return;
		}

		var landing = ArchSpanShape.Anchored( Owner.Plan, room, point );

		if ( taken is null )
		{
			taken = landing;

			return;
		}

		Bridge( room, taken, landing );
	}

	void Bridge( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )
	{
		if ( Standing( room, from, to ) is not { } span || !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out _ ) )
		{
			Log.Info( "Architecture: nothing to bridge there — a span needs two ends apart, each under a pier tall enough to spring off." );

			return;
		}

		span.Id = Owner.Plan.AllocateId();
		span.Name = $"Span{room.PierSpans.Count + 1}";

		room.PierSpans.Add( span );
		Owner.Picked = new ArchSelection { Item = span, Room = room };

		LastPlaced = span;
		taken = null;
	}

	// A ring on the pier's HEAD and a stem down to the floor: where the end will spring from and which column it
	// grabbed, drawn as a mark on something rather than as a post, which is a pillar ghost by another name.
	void Pier( ArchRoom room, ArchSpanEnd end, bool held )
	{
		var pier = ArchSpanShape.Pier( Owner.Plan, Owner.Kit, room, end );
		var lift = Lift( room );
		var head = new Vector3( pier.At.x, pier.At.y, pier.Head + lift );

		Gizmo.Draw.LineThickness = held ? 4f : 3f;
		Gizmo.Draw.Color = held ? ArchGhost.Accent : ArchGhost.Line;
		Gizmo.Draw.LineCircle( head, Vector3.Up, held ? 14f : 10f );
		Gizmo.Draw.Line( head, head.WithZ( room.BaseHeight + lift ) );
		Gizmo.Draw.LineThickness = 2f;

		if ( !pier.Standing )
		{
			ArchGhost.Cursor( pier.At, room.BaseHeight + lift, ArchSpanShape.Grab );
		}

		Gizmo.Draw.Color = ArchGhost.Line;
	}

	// The plan is authored flat and the building is poured onto its grade, so a ghost drawn at plan height stands
	// a foundation below the span it is previewing.
	float Lift( ArchRoom room )
	{
		var host = Owner.Plan.OwnerOf( room ) ?? Owner.ActiveBuilding();

		return host is null ? 0f : ArchAsks.Lift( Owner.Plan, host, Owner.Kit );
	}

	void Reaching( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )
	{
		var lift = Lift( room );

		if ( Standing( room, from, to ) is not { } span
			|| !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out var run ) )
		{
			Gizmo.Draw.Line(
				new Vector3( from.At.x, from.At.y, room.BaseHeight + lift ),
				new Vector3( to.At.x, to.At.y, room.BaseHeight + lift ) );

			return;
		}

		var start = new Vector3( run.From.x, run.From.y, run.Springing + lift );
		var end = new Vector3( run.To.x, run.To.y, run.Springing + lift );

		var top = run.Top + lift;

		Gizmo.Draw.LineThickness = 4f;
		Gizmo.Draw.Color = ArchGhost.Accent;

		Gizmo.Draw.Line( start, end );
		Gizmo.Draw.Line( start.WithZ( top ), end.WithZ( top ) );
		Gizmo.Draw.Line( start, start.WithZ( top ) );
		Gizmo.Draw.Line( end, end.WithZ( top ) );

		Gizmo.Draw.LineThickness = 2f;
		Gizmo.Draw.Color = ArchGhost.Line;

		ArchGhost.Note( Vector3.Lerp( start.WithZ( top ), end.WithZ( top ), 0.5f ),
			$"{span.Form} — {(run.To - run.From).Length:0} across, {run.Top - run.Springing:0} deep, springs at {run.Springing - room.BaseHeight:0}" );
	}

	// ONE resolution the ghost and the placed part both read, so what the gesture showed is what the mesh comes out as.
	ArchSpanPart Standing( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )
	{
		if ( room is null )
		{
			return null;
		}

		var span = draft.Dressing();

		span.Level = room.Floor;
		span.From = from;
		span.To = to;

		return span;
	}

	// The host lays the rows: a bridge is the Pillars tool's third gesture, so its two questions belong in that
	// tool's one sequence rather than in a second list numbered from 1 underneath it.
	public void MergedSteps( ArchWorkflow flow )
	{
		var span = Edited;
		var changed = Changed( span );

		flow.Step( "Form", span.Form.ToString(), ArchSpanUi.Glyph( span.Form ), step =>
			ArchSpanUi.Form( step.Layout, span, step.Chose, changed ) );

		flow.Step( "Section", ArchSpanUi.Measured( span ), "straighten", step =>
			ArchSpanUi.Section( step.Layout, span, changed ) );
	}

	// The picked span if there is one, else the seed for the next bridge - the same two steps either way, which is
	// what keeps a tuned span and the next one drawn from agreeing.
	public ArchSpanPart Edited => Editing() ?? draft;

	public string PickedName => Editing()?.Name;

	Action Changed( ArchSpanPart span )
	{
		return Editing() is null
			? () => ArchSpanMemo.Remember( draft )
			: () => { Seed( span ); Owner.Touch( "Edit Span" ); };
	}

	// Tuning the span that was just placed IS the author saying how the next one should look - the panel points at
	// the selection, so without this every adjustment was spent on one part and the seed never moved.
	void Seed( ArchSpanPart span )
	{
		draft = span.Dressing();

		ArchSpanMemo.Remember( draft );
	}

	ArchSpanPart Editing() => Owner.Picked?.Item as ArchSpanPart;
}
sunless.lib_architecture / Editor/Data/ArchPlan.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchPlan
{
	public int Version { get; set; } = 3;
	public int NextId { get; set; } = 1;
	public string KitName { get; set; } = "default";

	// Every top-level thing standing on the map, in stack order, whatever kind it is.
	public List<ArchUnit> Units { get; set; } = new();

	// Version 2 filed the two kinds in lists of their own. Read back under those names and folded into Units by
	// Normalize, so an old plan opens whole and is written back carrying one list.
	[JsonPropertyName( "Buildings" )]
	public List<ArchBuilding> LegacyBuildings { get; set; }

	[JsonPropertyName( "Roads" )]
	public List<ArchRoadPart> LegacyRoads { get; set; }

	// A view over the one list, so a pass that only cares about buildings still reads naturally. Filed through
	// Units - adding to a view would go nowhere, which is why it is not a List. Every other kind's view is shipped
	// by the module that owns it, so core names one type here and no more.
	[JsonIgnore]
	public IReadOnlyList<ArchBuilding> Buildings => Units.OfType<ArchBuilding>().ToList();

	// Cross-building relationships and placed reusable assets.
	public List<ArchSiteAssembly> Assemblies { get; set; } = new();
	public List<ArchAssetInstance> Instances { get; set; } = new();
	// Explicit layer metadata - written only when a legacy item is reparented, disabled or locked.
	public List<ArchLayerRecord> Layers { get; set; } = new();
	// What an author said about one named PIECE of a layer - a roof's fascia, a porch's balustrade.
	public List<ArchPartRecord> Parts { get; set; } = new();
	public List<ArchLayerLink> Links { get; set; } = new();

	public int AllocateId() => NextId++;

	public IEnumerable<ArchRoom> AllRooms() => Buildings.SelectMany( building => building.Rooms );

	public ArchBuilding FindBuilding( int id ) => Buildings.FirstOrDefault( building => building.Id == id );

	public ArchRoom FindRoom( int id ) => AllRooms().FirstOrDefault( room => room.Id == id );

	public ArchBuilding OwnerOf( ArchRoom room ) => Buildings.FirstOrDefault( building => building.Rooms.Contains( room ) );

	// Buildings first, then roads, the order version 2 wrote them in. Run before anything counts ids, and it
	// clears the legacy lists so a plan opened and saved again carries Units alone.
	void Adopt()
	{
		if ( LegacyBuildings is { Count: > 0 } )
		{
			Units.AddRange( LegacyBuildings );
		}

		if ( LegacyRoads is { Count: > 0 } )
		{
			Units.AddRange( LegacyRoads );
		}

		LegacyBuildings = null;
		LegacyRoads = null;
	}

	// Empty can never be a count - a placement seeds a target before it knows whether it will fill it. A road being
	// drawn holds one node and no content yet, and blanking the file under it is how an authored plan is lost.
	[JsonIgnore]
	public bool HasContent => this.Roads().Count > 0 || Units.Any( unit => unit.HasContent );

	// A placement target that was seeded and never filled is not authored content, so it must not survive
	// the commit - it would stand in the layer stack as an empty Building/Level/Room nobody drew.
	public int DiscardEmptyTargets()
	{
		var dropped = 0;

		foreach ( var building in Buildings )
		{
			dropped += building.Rooms.RemoveAll( room => !room.HasContent );
		}

		return dropped + Units.RemoveAll( unit => !unit.HasContent );
	}

	public void Normalize()
	{
		Adopt();

		NextId = System.Math.Max( 1, HighestId() + 1 );

		foreach ( var building in Buildings )
		{
			// Plans saved before fences carried a curve - lift the old path onto nodes once.
			foreach ( var fence in building.Fences.Where( entry => entry.Path.Count >= 2 && entry.Nodes.Count == 0 ) )
			{
				fence.Nodes = fence.Path.Select( ArchCurveNode.At ).ToList();
				fence.Path = new List<Vector3>();
			}

			// Plans saved before a stair was a shaft: the chain of overlapping boxes is read once into the core
			// that bounds them and the flights that stood in it, then let go of. Everything downstream has only
			// ever seen the resolve, so a converted stair builds exactly what it built before.
			foreach ( var room in building.Rooms )
			{
				foreach ( var stair in room.Stairs )
				{
					Reshaft( stair );
				}
			}

			foreach ( var platform in building.Platforms )
			{
				foreach ( var stair in platform.Stairs )
				{
					Reshaft( stair );
				}
			}

			foreach ( var porch in building.Rooms.SelectMany( room => room.Porches ) )
			{
				foreach ( var stair in porch.Stairs )
				{
					Reshaft( stair );
				}
			}
		}

		foreach ( var building in Buildings )
		{
			foreach ( var room in building.Rooms )
			{
				room.Walls.RemoveAll( wall => wall.Length < 1f );
			}

			foreach ( var roof in building.Roofs )
			{
				roof.Walls.RemoveAll( wall => wall.Length < 1f );
			}

			foreach ( var roof in building.Roofs.Where( roof => roof.HasFootprint ) )
			{
				roof.Reshape( roof.Footprint );
			}

			SeparateSingleSpanRoofs( building );
		}

		// And whatever a KIND says its own parts need fixing up, which is how a contributed kind migrates a plan
		// written before it changed shape. Every pass must be idempotent: Normalize runs on every load and again
		// after every connection resolve.
		foreach ( var fixup in ArchKinds.Load().All.OfType<IArchNormalizes>() )
		{
			fixup.Normalize( this );
		}
	}

	// Two migrations, each of which runs exactly once per plan however many times Normalize is called, because
	// each lets go of what it read.
	//
	// A stair authored as a chain of overlapping boxes is read into the shaft that bounds them - a stair saved
	// before there were legs at all still gets a shaft, because a core of nothing builds nothing. Then a stair
	// authored when a landing was DERIVED has that derive run one last time and left behind as real landing
	// steps, so nothing downstream ever works a pad out again.
	void Reshaft( ArchStairPart stair )
	{
		if ( stair.Legs is { Count: > 0 } legs )
		{
			var (core, lanes) = ArchStairLanes.FromLegs( legs );

			core.Rise = MathF.Max( 4f, stair.StoredRise > 1f ? stair.StoredRise : stair.Core?.Rise ?? 0f );

			stair.Core = core;
			stair.Lanes = lanes;
			stair.Legs = null;
		}

		stair.StoredRise = 0f;

		ArchStairLanes.Settle( this, stair );
		ArchStairLanes.Number( this, stair );
	}

	void SeparateSingleSpanRoofs( ArchBuilding building )
	{
		for ( var roofIndex = building.Roofs.Count - 1; roofIndex >= 0; roofIndex-- )
		{
			var roof = building.Roofs[roofIndex];
			var outline = roof.Outline();

			if ( roof.Style is not (RoofStyle.Gable or RoofStyle.Shed or RoofStyle.Sawtooth) ||
				!roof.HasFootprint ||
				outline.Count == 4 )
			{
				continue;
			}

			var rooms = building.Rooms
				.Where( room => room.Floor == roof.Level )
				.Select( room => (Room: room, Footprint: ArchFloorGen.Footprint( room )) )
				.Where( candidate => candidate.Footprint.Count == 4 )
				.Where( candidate => ArchRegion.Covers( new[] { outline }, candidate.Footprint ) )
				.ToList();

			rooms.RemoveAll( candidate => rooms.Any( other =>
				!ReferenceEquals( candidate.Room, other.Room ) &&
				MathF.Abs( ArchFootprint.SignedArea( other.Footprint ) ) > MathF.Abs( ArchFootprint.SignedArea( candidate.Footprint ) ) &&
				ArchRegion.Covers( new[] { other.Footprint }, candidate.Footprint ) ) );

			var occupied = ArchFootprint.Union( rooms.Select( candidate => candidate.Footprint ).ToList() );

			if ( rooms.Count < 2 ||
				!ArchRegion.Covers( occupied, outline ) ||
				occupied.Any( loop => !ArchRegion.Covers( new[] { outline }, loop ) ) )
			{
				continue;
			}

			building.Roofs.RemoveAt( roofIndex );

			for ( var roomIndex = rooms.Count - 1; roomIndex >= 0; roomIndex-- )
			{
				var section = roof.Duplicate( roomIndex == 0 ? roof.Id : AllocateId(), rooms[roomIndex].Footprint );
				building.Roofs.Insert( roofIndex, section );
			}
		}
	}

	int HighestId()
	{
		var ids = new List<int> { 0 };
		var roads = this.Roads();

		ids.AddRange( Assemblies.Select( assembly => assembly.Id ) );
		ids.AddRange( Instances.Select( instance => instance.Id ) );
		ids.AddRange( roads.Select( road => road.Id ) );
		ids.AddRange( roads.SelectMany( road => road.Crossings ).Select( crossing => crossing.Id ) );
		ids.AddRange( roads.SelectMany( road => road.Bridges ).Select( bridge => bridge.Id ) );
		ids.AddRange( roads.SelectMany( road => road.Tunnels ).Select( tunnel => tunnel.Id ) );
		ids.AddRange( roads.SelectMany( road => road.Cuts ).Select( cut => cut.Id ) );

		foreach ( var building in Buildings )
		{
			ids.Add( building.Id );
			ids.AddRange( building.Rooms.Select( room => room.Id ) );
			ids.AddRange( building.Rooms.SelectMany( room => room.Stairs ).Select( stair => stair.Id ) );
			ids.AddRange( building.Rooms.SelectMany( room => room.Trims ).Select( trim => trim.Id ) );
			ids.AddRange( building.Rooms.SelectMany( room => room.Pillars ).Select( pillar => pillar.Id ) );
			ids.AddRange( building.Rooms.SelectMany( room => room.PierSpans ).Select( span => span.Id ) );
			ids.AddRange( building.Rooms.SelectMany( room => room.Beams ).Select( beam => beam.Id ) );
			// The porch is a host, so its own children are in here too - miss them and an id is handed out
			// twice, which in the scene is one object standing where two were meant to.
			var porches = building.Rooms.SelectMany( room => room.Porches ).ToList();

			ids.AddRange( porches.Select( porch => porch.Id ) );
			ids.AddRange( porches.SelectMany( porch => porch.Stairs ).Select( stair => stair.Id ) );
			ids.AddRange( porches.SelectMany( porch => porch.Pillars ).Select( pillar => pillar.Id ) );
			ids.AddRange( porches.SelectMany( porch => porch.Trims ).Select( trim => trim.Id ) );
			ids.AddRange( building.Rooms.SelectMany( room => room.Approaches ).Select( approach => approach.Id ) );
			ids.AddRange( building.Roofs.Select( roof => roof.Id ) );
			ids.AddRange( building.Roofs.SelectMany( roof => roof.Lights ).Select( light => light.Id ) );
			ids.AddRange( building.Fences.Select( fence => fence.Id ) );
			ids.AddRange( building.Downpipes.Select( pipe => pipe.Id ) );
			ids.AddRange( building.Pipes.Select( run => run.Id ) );
			ids.AddRange( building.Pipes.SelectMany( run => run.Nodes ).Select( node => node.Id ) );
			ids.AddRange( building.Brackets.Select( bracket => bracket.Id ) );
			ids.AddRange( building.Ladders.Select( ladder => ladder.Id ) );
			ids.AddRange( building.Balconies.Select( balcony => balcony.Id ) );
			ids.AddRange( building.ExteriorStairs.Select( flight => flight.Id ) );
			ids.AddRange( building.Cutouts.Select( cutout => cutout.Id ) );
			ids.AddRange( building.Cuts.Select( cut => cut.Id ) );
			ids.AddRange( building.Platforms.Select( platform => platform.Id ) );
			ids.AddRange( building.Platforms.SelectMany( platform => platform.Stairs ).Select( stair => stair.Id ) );
		}

		// Through AllWalls, or a parapet's id is handed out again the next time a build normalizes, and two
		// walls sharing an id are one object in the scene - the second stands where the first stood.
		var walls = this.AllWalls().ToList();

		ids.AddRange( walls.Select( wall => wall.Id ) );
		ids.AddRange( walls.SelectMany( wall => wall.Openings ).Select( opening => opening.Id ) );
		ids.AddRange( walls.SelectMany( wall => wall.Modifiers ).Select( modifier => modifier.Id ) );

		// Every step of a climb carries an id so a railing can name the one it guards, exactly as a pipe's nodes
		// do - and through Parts, because a stair stands in a room, on a porch and on a platform, and the three
		// lists that hold them are three chances to forget one.
		var steps = this.Parts<ArchStairPart>().SelectMany( stair => stair.Lanes ).ToList();

		ids.AddRange( steps.Select( lane => lane.Id ) );
		ids.AddRange( steps.SelectMany( lane => lane.Guards ).Select( guard => guard.Id ) );

		// And through the bytes no type here claims, or the parts of a kind this build cannot read are invisible
		// to the allocator and the next id handed out is one something already holds.
		ids.AddRange( Units.Select( unit => unit.Id ) );
		ids.AddRange( Units.Select( unit => ArchPlanStore.HighestIdIn( unit.Payloads ) ) );
		ids.AddRange( AllRooms().Select( room => ArchPlanStore.HighestIdIn( room.Payloads ) ) );

		return ids.Max();
	}
}

public sealed class ArchPalette
{
	public Dictionary<string, string> Materials { get; set; } = new();
	public Dictionary<string, float> TexelScales { get; set; } = new();
	public Dictionary<string, Vector2> TextureOffsets { get; set; } = new();
	public Dictionary<string, ArchFaceUvSet> FaceMappings { get; set; } = new();

	public bool TryGet( ArchSurface surface, out string path )
	{
		return Materials.TryGetValue( surface.ToString(), out path ) && !string.IsNullOrWhiteSpace( path );
	}

	// The density belongs to the material, so re-pointing a role drops it.
	public void Set( ArchSurface surface, string path )
	{
		TexelScales.Remove( surface.ToString() );

		if ( string.IsNullOrWhiteSpace( path ) )
		{
			Materials.Remove( surface.ToString() );
			TextureOffsets?.Remove( surface.ToString() );
			return;
		}

		Materials[surface.ToString()] = path;
	}

	public void Set( ArchSurface surface, string path, float texelScale )
	{
		Set( surface, path );

		if ( !string.IsNullOrWhiteSpace( path ) )
		{
			SetScale( surface, texelScale );
		}
	}

	// Zero means "no override" - the generator falls back to ArchMesh.TexelScale.
	public void SetScale( ArchSurface surface, float texelScale )
	{
		if ( texelScale <= 0f )
		{
			TexelScales.Remove( surface.ToString() );
			return;
		}

		TexelScales[surface.ToString()] = texelScale;
	}

	public float ScaleFor( ArchSurface surface )
	{
		return TexelScales.TryGetValue( surface.ToString(), out var scale ) ? scale : 0f;
	}

	public void SetOffset( ArchSurface surface, Vector2 offset )
	{
		if ( offset.IsNearZeroLength )
		{
			TextureOffsets?.Remove( surface.ToString() );
			return;
		}

		TextureOffsets ??= new();
		TextureOffsets[surface.ToString()] = offset;
	}

	public Vector2 OffsetFor( ArchSurface surface )
	{
		return TextureOffsets is not null && TextureOffsets.TryGetValue( surface.ToString(), out var offset ) ? offset : Vector2.Zero;
	}
}

public interface IArchPainted
{
	ArchPalette Palette { get; set; }
}

public sealed class ArchFaceUvSet
{
	public List<ArchFaceUvVariation> Variations { get; set; } = new();
}

public sealed class ArchFaceUvVariation
{
	public string Signature { get; set; }
	public List<ArchFaceUvFace> Faces { get; set; } = new();
}

public sealed class ArchFaceUvFace
{
	public List<Vector2> Coordinates { get; set; } = new();
}

// One top-level thing standing on the map. A house and a street are not the same shape and never will be, but
// everything the STACK does to one it does to the other - name it, disable it, group it, order it, carve it - so
// they share a base and the plan holds one list. Walking two lists is how a road quietly stopped being reached
// by half the passes that reach a building.
[JsonConverter( typeof( ArchUnitConverter ) )]
public abstract class ArchUnit : IArchCollides, IArchNamed
{
	// The whole unit at once - what a far-off silhouette is set to None from.
	public ArchCollisionMode? Collision { get; set; }

	// Which module's unit this is. Written as an ordinary field rather than a polymorphic discriminator, because
	// System.Text.Json throws on a discriminator it does not recognise and an unknown kind is the not-installed case.
	public ArchKind Kind { get; set; }

	public int Id { get; set; }
	// The author's, not the kind's: a unit is renamed in the Plan Layers stack like any other layer.
	public string Name { get; set; } = "Unit";
	public ArchPalette Palette { get; set; } = new();
	// One list - a stairwell, a passage and a service bay are the same part with a different profile.
	public List<ArchCutPart> Cuts { get; set; } = new();

	// Whatever a module filed here that this editor has no type for. Written back exactly as it was read, so a plan
	// opened without the library that authored it saves whole instead of losing that library's work.
	[JsonExtensionData]
	public Dictionary<string, JsonElement> Payloads { get; set; } = new();

	[JsonIgnore]
	public abstract bool HasContent { get; }
}

// A unit whose kind no installed module claims. It carries nothing this editor can read and everything the file
// gave it, so it round-trips byte for byte - and it always reports content, or DiscardEmptyTargets would delete
// the one thing in the plan nobody here is able to see.
public sealed class ArchOpaqueUnit : ArchUnit
{
	public override bool HasContent => true;
}

public sealed class ArchBuilding : ArchUnit, IArchPainted
{
	public ArchBuilding()
	{
		Name = "Building";
		Kind = ArchKind.Building;
	}

	// A wing extended later still comes out the same type, not kit defaults.
	public string Archetype { get; set; } = "";
	public List<ArchRoom> Rooms { get; set; } = new();
	public List<ArchRoofPart> Roofs { get; set; } = new();
	public List<ArchDownpipePart> Downpipes { get; set; } = new();
	// Service corridors and the hangers under them. Filed on the unit rather than a room, because a run
	// crosses partitions the way a fence crosses a yard - the volume is world-space and answers to no floor.
	public List<ArchPipePart> Pipes { get; set; } = new();
	public List<ArchPipeBracketPart> Brackets { get; set; } = new();
	public List<ArchFencePart> Fences { get; set; } = new();
	// Platforms stand in the yard, so they hang off the building like a fence.
	public List<ArchPlatformPart> Platforms { get; set; } = new();
	public List<ArchLadderPart> Ladders { get; set; } = new();
	// Standing outside a room, so they hang off the building for the same reason a ladder does.
	public List<ArchBalconyPart> Balconies { get; set; } = new();
	public List<ArchExteriorStairPart> ExteriorStairs { get; set; } = new();
	public bool GuttersEnabled { get; set; } = true;
	// How far round the shell has been turned since it was drawn. The coordinates are still the truth - this is
	// the LEDGER of the turns applied to them, so an angle can be named absolutely instead of only nudged.
	public float Facing { get; set; }
	public float StoreyHeight { get; set; }
	public List<ArchFloorCutout> Cutouts { get; set; } = new();

	[JsonIgnore]
	public override bool HasContent => Roofs.Count > 0 || Downpipes.Count > 0 || Fences.Count > 0 || Platforms.Count > 0
		|| Ladders.Count > 0 || Cuts.Count > 0 || Balconies.Count > 0 || ExteriorStairs.Count > 0
		|| Pipes.Count > 0 || Brackets.Count > 0
		|| Rooms.Any( room => room.HasContent );
}

public sealed class ArchRoom : IArchCollides, IArchPainted, IArchNamed
{
	// Overrides the kit for the room's own shell, slab and ceiling. A part standing IN it carries its own.
	public ArchCollisionMode? Collision { get; set; }

	public int Id { get; set; }
	public string Name { get; set; } = "Room";
	public int Floor { get; set; }
	public float BaseHeight { get; set; }
	public float WallHeight { get; set; }
	public bool HasFloor { get; set; } = true;
	public bool HasCeiling { get; set; } = true;
	// A shell with nothing behind it: every wall it holds is single-sided, so the far face is never emitted and
	// ArchCull stands a lined box behind each window instead. Its floor and ceiling are still its own to turn off.
	public bool Facade { get; set; }
	public float CeilingDepth { get; set; }
	public bool FloorBoards { get; set; }
	public float FloorBoardYaw { get; set; }
	public bool RaisedFoundation { get; set; } = true;
	// A link across a gap, carried by its own piers - not an overhang.
	public bool Spans { get; set; }
	// A row of posts under whatever this storey oversails. Off by default: an upper storey that steps out over
	// the one below reads as a cantilever, and propping every one of them is a look, not a rule.
	public bool OverhangPosts { get; set; }
	// The section every one of those posts is cut from - a pillar with no position, exactly as a pillar TYPE
	// holds one, so a prop under an overhang is dressed by the Pillars tool's own forms rather than a second set.
	public ArchPillarPart OverhangPost { get; set; } = new();
	// A rising walkway: the far end's floor height. Equal to BaseHeight on a flat link.
	public float WalkwayTop { get; set; }
	public WalkwayInterior Interior { get; set; }
	// The link draws its own boards so they die square at the mouth corners.
	public bool WalkwaySkirting { get; set; } = true;
	public ArchPalette Palette { get; set; } = new();
	public List<ArchWall> Walls { get; set; } = new();
	public List<ArchStairPart> Stairs { get; set; } = new();
	public List<ArchTrimPart> Trims { get; set; } = new();
	public List<ArchPillarPart> Pillars { get; set; } = new();
	// Not "Spans" - that is already the walkway's own flag, and a slot name is a json property.
	public List<ArchSpanPart> PierSpans { get; set; } = new();
	public List<ArchBeamPart> Beams { get; set; } = new();
	public List<ArchPorchPart> Porches { get; set; } = new();
	public List<ArchApproachPart> Approaches { get; set; } = new();
	public List<Vector2> Footprint { get; set; } = new();

	[JsonExtensionData]
	public Dictionary<string, JsonElement> Payloads { get; set; } = new();

	[JsonIgnore]
	public bool HasFootprint => Footprint.Count >= 3;

	[JsonIgnore]
	public bool HasContent => HasFootprint || Walls.Count > 0 || Stairs.Count > 0 || Pillars.Count > 0
		|| Trims.Count > 0 || Porches.Count > 0 || Approaches.Count > 0 || Beams.Count > 0 || PierSpans.Count > 0;
}
sunless.lib_architecture / Editor/Layers/ArchLayerTree.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

// One authored thing in the plan, shown as a tree row. Payload is the typed part itself; virtual
// nodes (story groups) carry none. Building/Room carry the selection context so any row can become
// an ArchSelection without re-hunting ownership.
public sealed class ArchLayerNode
{
	public ArchLayerRef? Ref { get; init; }
	public ArchKind Kind { get; init; }
	public ArchLayerStage Stage { get; init; }
	public ArchLayerDomain Domain { get; init; }
	public object Payload { get; init; }
	public string Name { get; init; } = "";
	// Records may exclude a layer from generation; absent a record, everything is enabled.
	public bool Enabled { get; init; } = true;
	// Locked rows still select and still generate; they refuse every edit.
	public bool Locked { get; init; }
	// Sibling order within a stage, for the kinds that care. Absent a record it is authoring order.
	public int Order { get; init; }
	// Story headers and room layers name the storey they stand on.
	public int Floor { get; init; } = int.MinValue;
	public ArchLayerNode Parent { get; internal set; }
	public List<ArchLayerNode> Children { get; } = new();
	// Selection context: every row knows which building and room it belongs to.
	public ArchBuilding Building { get; init; }
	public ArchRoom Room { get; init; }

	public bool Virtual => Payload is null;

	public string DisplayName => ArchLayerNames.DisplayName( this );
}

public sealed class ArchLayerDomainGroup
{
	public ArchLayerDomain Domain { get; init; }
	public string Name { get; init; } = "";
	public List<ArchLayerNode> Children { get; } = new();
}

// The projected layer tree: a metadata-only view of the plan's authored ownership. Building it must
// never resolve generator shapes - only ids, kinds, names and floors.
public sealed class ArchLayerTree
{
	public List<ArchLayerDomainGroup> Domains { get; } = new();

	readonly Dictionary<int, ArchLayerNode> byId = new();
	readonly Dictionary<object, ArchLayerNode> byPayload = new();
	readonly Dictionary<(int Building, int Floor), ArchLayerNode> stories = new();

	public IReadOnlyList<ArchLayerLink> Links { get; private set; } = Array.Empty<ArchLayerLink>();

	public ArchLayerNode Find( int id ) => byId.TryGetValue( id, out var node ) ? node : null;

	public ArchLayerNode Find( object payload )
	{
		return payload is null ? null : byPayload.TryGetValue( payload, out var node ) ? node : null;
	}

	public object Resolve( ArchLayerRef layer ) => byId.TryGetValue( layer.ItemId, out var node ) ? node.Payload : null;

	// "House 2 / Level 1 / Walkway 1" - the row's authored path, not its object path.
	public string Breadcrumb( ArchLayerNode node )
	{
		if ( node is null )
		{
			return "";
		}

		var parts = new List<string>();

		for ( var current = node; current is not null; current = current.Parent )
		{
			parts.Add( current.DisplayName );
		}

		parts.Reverse();

		return string.Join( " / ", parts );
	}

	// A stable identity for tree widgets: payload rows key by the payload itself (which survives
	// commits), story rows by their building and floor.
	public object StableKey( ArchLayerNode node )
	{
		return node.Payload ?? $"story:{node.Building?.Id}:{node.Floor}";
	}

	// The explicit metadata for a layer, written the first time anything is asked of it that typed
	// ownership cannot answer - a parent, an order, a disabled state, a lock.
	public ArchLayerRecord Record( ArchPlan plan, ArchLayerNode node )
	{
		if ( node?.Ref is not { } layer )
		{
			return null;
		}

		var record = plan.Layers.FirstOrDefault( entry => entry.ItemId == layer.ItemId );

		if ( record is null )
		{
			record = new ArchLayerRecord
			{
				ItemId = layer.ItemId,
				ParentId = layer.ParentId,
				Kind = layer.Kind,
				Stage = node.Stage,
				Order = node.Order,
				Enabled = node.Enabled,
				Locked = node.Locked,
			};

			plan.Layers.Add( record );
		}

		return record;
	}

	// Drag-reorder writes an explicit order for the whole sibling run, so the arrangement survives a
	// later addition landing at the end of its owner's typed list.
	public bool Reorder( ArchPlan plan, ArchLayerRef moving, int anchorId, bool below )
	{
		if ( !byId.TryGetValue( moving.ItemId, out var node ) || !byId.TryGetValue( anchorId, out var anchor ) )
		{
			return false;
		}

		if ( ReferenceEquals( node, anchor ) || !ReferenceEquals( node.Parent, anchor.Parent ) )
		{
			return false;
		}

		var siblings = (node.Parent?.Children ?? Domains.FirstOrDefault( domain => domain.Domain == node.Domain )?.Children)
			?.Where( child => child.Ref is not null )
			.ToList();

		if ( siblings is null )
		{
			return false;
		}

		siblings.Remove( node );

		var at = siblings.IndexOf( anchor );

		if ( at < 0 )
		{
			return false;
		}

		siblings.Insert( below ? at + 1 : at, node );

		for ( var index = 0; index < siblings.Count; index++ )
		{
			Record( plan, siblings[index] ).Order = index;
		}

		return true;
	}

	// Validated reparent: the capability matrix approves, the payload actually moves between its
	// ownership lists, and a layer record is written so the projection keeps the new parent.
	public bool Reparent( ArchPlan plan, ArchLayerRef layer, int newParentId )
	{
		if ( !byId.TryGetValue( layer.ItemId, out var node ) || node.Payload is null )
		{
			return false;
		}

		// Dropping onto the domain header takes the layer out of whatever group held it.
		if ( newParentId == 0 )
		{
			ArchLayerGroups.Leave( plan, layer.ItemId );

			return true;
		}

		if ( layer.ItemId == newParentId || !byId.TryGetValue( newParentId, out var parentNode ) || parentNode.Payload is null )
		{
			return false;
		}

		// A folder takes anything: membership is a scope, not an ownership claim, so the payload stays
		// exactly where the generator reads it and only the layer it belongs to changes.
		if ( parentNode.Payload is ArchSiteAssembly group )
		{
			ArchLayerGroups.Join( plan, group, layer.ItemId );

			return true;
		}

		if ( !ArchLayerRules.CanParent( parentNode.Kind, layer.Kind ).Allowed )
		{
			return false;
		}

		ArchLayerGroups.Leave( plan, layer.ItemId );

		if ( !MovePayload( plan, node.Payload, parentNode.Payload ) )
		{
			return false;
		}

		var record = plan.Layers.FirstOrDefault( entry => entry.ItemId == layer.ItemId );

		if ( record is null )
		{
			record = new ArchLayerRecord { ItemId = layer.ItemId, ParentId = newParentId, Kind = layer.Kind, Stage = node.Stage };
			plan.Layers.Add( record );
		}

		record.ParentId = newParentId;

		return true;
	}

	// A wall lives on a room or on a deck, so it is taken out of whichever holds it before being filed anywhere.
	static bool Unfile( ArchPlan plan, ArchWall wall )
	{
		foreach ( var room in plan.AllRooms() )
		{
			if ( room.Walls.Remove( wall ) )
			{
				return true;
			}
		}

		foreach ( var roof in plan.AllRoofs() )
		{
			if ( roof.Walls.Remove( wall ) )
			{
				return true;
			}
		}

		return false;
	}

	// A flight stands in a room, on a platform or on a porch deck, so it is taken out of whichever holds it
	// before being filed anywhere. The same three-homed lookup covers columns and runs.
	static bool Unfile( ArchPlan plan, ArchStairPart stair )
	{
		foreach ( var room in plan.AllRooms() )
		{
			if ( room.Stairs.Remove( stair ) || room.Porches.Any( porch => porch.Stairs.Remove( stair ) ) )
			{
				return true;
			}
		}

		return plan.Buildings.SelectMany( building => building.Platforms ).Any( platform => platform.Stairs.Remove( stair ) );
	}

	static bool Unfile( ArchPlan plan, ArchPillarPart pillar )
	{
		return plan.AllRooms().Any( room => room.Pillars.Remove( pillar ) || room.Porches.Any( porch => porch.Pillars.Remove( pillar ) ) );
	}

	static bool Unfile( ArchPlan plan, ArchTrimPart trim )
	{
		return plan.AllRooms().Any( room => room.Trims.Remove( trim ) || room.Porches.Any( porch => porch.Trims.Remove( trim ) ) );
	}

	// The plan's typed lists are the payloads' real homes - a reparent that only rewrote the record
	// would show a new tree while the generator read the old ownership.
	static bool MovePayload( ArchPlan plan, object payload, object newParent )
	{
		switch ( payload, newParent )
		{
			case (ArchRoom room, ArchBuilding building):
				if ( plan.OwnerOf( room ) is not { } fromRoom ) return false;
				fromRoom.Rooms.Remove( room );
				building.Rooms.Add( room );
				return true;

			case (ArchWall wall, ArchRoom room):
				if ( !Unfile( plan, wall ) ) return false;
				room.Walls.Add( wall );
				return true;

			// Onto a deck: the wall stops standing on a floor and starts standing on a roof, which is the only
			// difference between a partition and a parapet.
			case (ArchWall wall, ArchRoofPart roof):
				if ( !Unfile( plan, wall ) ) return false;
				roof.Walls.Add( wall );
				return true;

			case (ArchOpening opening, ArchWall wall):
				if ( plan.AllWalls().FirstOrDefault( candidate => candidate.Openings.Contains( opening ) ) is not { } fromOpening ) return false;
				fromOpening.Openings.Remove( opening );
				wall.Openings.Add( opening );
				return true;

			case (ArchWallModPart modifier, ArchWall host):
				if ( plan.AllWalls().FirstOrDefault( candidate => candidate.Modifiers.Contains( modifier ) ) is not { } fromModifier ) return false;
				fromModifier.Modifiers.Remove( modifier );
				host.Modifiers.Add( modifier );
				return true;

			case (ArchStairPart stair, ArchRoom room):
				if ( !Unfile( plan, stair ) ) return false;
				room.Stairs.Add( stair );
				return true;

			case (ArchTrimPart trim, ArchRoom room):
				if ( !Unfile( plan, trim ) ) return false;
				room.Trims.Add( trim );
				return true;

			case (ArchPillarPart pillar, ArchRoom room):
				if ( !Unfile( plan, pillar ) ) return false;
				room.Pillars.Add( pillar );
				return true;

			// Onto a porch: the flight stops standing on a floor and starts standing on a deck, which is all
			// that separates an inside stair from the steps off a veranda.
			case (ArchStairPart stair, ArchPorchPart porch):
				if ( !Unfile( plan, stair ) ) return false;
				porch.Stairs.Add( stair );
				return true;

			case (ArchPillarPart pillar, ArchPorchPart porch):
				if ( !Unfile( plan, pillar ) ) return false;
				porch.Pillars.Add( pillar );
				return true;

			case (ArchTrimPart trim, ArchPorchPart porch):
				if ( !Unfile( plan, trim ) ) return false;
				porch.Trims.Add( trim );
				return true;

			// A cut is world-space and stays filed where it was: nesting it under a porch says what it belongs
			// to, the way a walkway's deck record does, and moves nothing the generator reads.
			case (ArchCutPart, ArchPorchPart):
				return true;

			case (ArchBeamPart beam, ArchRoom room):
				if ( plan.AllRooms().FirstOrDefault( candidate => candidate.Beams.Contains( beam ) ) is not { } fromBeam ) return false;
				fromBeam.Beams.Remove( beam );
				room.Beams.Add( beam );
				return true;

			case (ArchPorchPart porch, ArchRoom room):
				if ( plan.AllRooms().FirstOrDefault( candidate => candidate.Porches.Contains( porch ) ) is not { } fromPorch ) return false;
				fromPorch.Porches.Remove( porch );
				room.Porches.Add( porch );
				return true;

			case (ArchApproachPart approach, ArchRoom room):
				if ( plan.AllRooms().FirstOrDefault( candidate => candidate.Approaches.Contains( approach ) ) is not { } fromApproach ) return false;
				fromApproach.Approaches.Remove( approach );
				room.Approaches.Add( approach );
				return true;

			case (ArchRoofPart roof, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Roofs.Contains( roof ) ) is not { } fromRoof ) return false;
				fromRoof.Roofs.Remove( roof );
				building.Roofs.Add( roof );
				return true;

			case (ArchRoofLightPart light, ArchRoofPart roof):
				if ( plan.Buildings.SelectMany( candidate => candidate.Roofs ).FirstOrDefault( candidate => candidate.Lights.Contains( light ) ) is not { } fromLight ) return false;
				fromLight.Lights.Remove( light );
				roof.Lights.Add( light );
				return true;

			case (ArchPlatformPart platform, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Platforms.Contains( platform ) ) is not { } fromPlatform ) return false;
				fromPlatform.Platforms.Remove( platform );
				building.Platforms.Add( platform );
				return true;

			case (ArchStairPart stair, ArchPlatformPart platform):
				if ( !Unfile( plan, stair ) ) return false;
				platform.Stairs.Add( stair );
				return true;

			case (ArchDownpipePart pipe, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Downpipes.Contains( pipe ) ) is not { } fromPipe ) return false;
				fromPipe.Downpipes.Remove( pipe );
				building.Downpipes.Add( pipe );
				return true;

			case (ArchPipePart run, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Pipes.Contains( run ) ) is not { } fromRun ) return false;
				fromRun.Pipes.Remove( run );
				building.Pipes.Add( run );
				return true;

			case (ArchPipeBracketPart bracket, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Brackets.Contains( bracket ) ) is not { } fromBracket ) return false;
				fromBracket.Brackets.Remove( bracket );
				building.Brackets.Add( bracket );
				return true;

			case (ArchFencePart fence, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Fences.Contains( fence ) ) is not { } fromFence ) return false;
				fromFence.Fences.Remove( fence );
				building.Fences.Add( fence );
				return true;

			case (ArchLadderPart ladder, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Ladders.Contains( ladder ) ) is not { } fromLadder ) return false;
				fromLadder.Ladders.Remove( ladder );
				building.Ladders.Add( ladder );
				return true;

			case (ArchBalconyPart balcony, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Balconies.Contains( balcony ) ) is not { } fromBalcony ) return false;
				fromBalcony.Balconies.Remove( balcony );
				building.Balconies.Add( balcony );
				return true;

			case (ArchExteriorStairPart flight, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.ExteriorStairs.Contains( flight ) ) is not { } fromFlight ) return false;
				fromFlight.ExteriorStairs.Remove( flight );
				building.ExteriorStairs.Add( flight );
				return true;

			case (ArchCutPart cut, ArchBuilding building):
				if ( plan.Buildings.FirstOrDefault( candidate => candidate.Cuts.Contains( cut ) ) is not { } fromCut ) return false;
				fromCut.Cuts.Remove( cut );
				building.Cuts.Add( cut );
				return true;

			case (ArchBridgePart bridge, ArchRoadPart road):
				if ( plan.Roads().FirstOrDefault( candidate => candidate.Bridges.Contains( bridge ) ) is not { } fromBridge ) return false;
				fromBridge.Bridges.Remove( bridge );
				road.Bridges.Add( bridge );
				return true;

			case (ArchTunnelPart tunnel, ArchRoadPart road):
				if ( plan.Roads().FirstOrDefault( candidate => candidate.Tunnels.Contains( tunnel ) ) is not { } fromTunnel ) return false;
				fromTunnel.Tunnels.Remove( tunnel );
				road.Tunnels.Add( tunnel );
				return true;

			default:
				return false;
		}
	}

	public bool IsEnabled( object payload )
	{
		return payload is null || byPayload.TryGetValue( payload, out var node ) && node.Enabled;
	}

	public bool IsEnabled( int id )
	{
		return byId.TryGetValue( id, out var node ) && node.Enabled;
	}

	public bool IsLocked( int id )
	{
		return byId.TryGetValue( id, out var node ) && node.Locked;
	}

	public bool IsLocked( object payload )
	{
		return payload is not null && byPayload.TryGetValue( payload, out var node ) && node.Locked;
	}

	// LOCKED HERE OR ANYWHERE ABOVE. A lock is how an author says "I have hand-edited these faces and the
	// generator is finished with them", so locking a house has to freeze every wall in it - a rebuild that walked
	// into one of them would throw the edit away, which is the one thing the lock exists to prevent.
	public bool Frozen( int itemId )
	{
		if ( itemId == 0 || !byId.TryGetValue( itemId, out var found ) )
		{
			return false;
		}

		for ( var node = found; node is not null; node = node.Parent )
		{
			if ( node.Locked )
			{
				return true;
			}
		}

		return false;
	}

	public bool AnyFrozen() => byId.Values.Any( node => node.Locked );

	// The messages a layer must surface: unresolved required links, missing children.
	public IReadOnlyList<string> Problems( ArchLayerNode node )
	{
		var problems = new List<string>();

		if ( node?.Ref is not { } nodeRef )
		{
			return problems;
		}

		foreach ( var link in Links.Where( link => link.Required && link.SourceId == nodeRef.ItemId ) )
		{
			if ( !byId.ContainsKey( link.TargetId ) )
			{
				problems.Add( $"Required link {link.SourcePort} → {link.TargetPort} (id {link.TargetId}) does not resolve." );
			}
		}

		return problems;
	}

	// The ids a rebuild must touch when this layer changes: itself, its descendants, the hosts that
	// receive its owned effects, and required linked consumers. Generation still rebuilds the whole
	// scene today, but the dirty set is what partial regeneration will later replace.
	public IReadOnlySet<int> DirtyClosure( int itemId )
	{
		var dirty = new HashSet<int> { itemId };

		if ( byId.TryGetValue( itemId, out var node ) )
		{
			CollectDescendants( node, dirty );
		}

		foreach ( var link in Links.Where( link => link.Required && link.TargetId == itemId ) )
		{
			dirty.Add( link.SourceId );
		}

		foreach ( var entry in byId.Values )
		{
			if ( OwnsEffectsOn( entry.Payload, itemId ) && entry.Ref is { } entryRef )
			{
				dirty.Add( entryRef.ItemId );
			}
		}

		return dirty;
	}

	static void CollectDescendants( ArchLayerNode node, HashSet<int> into )
	{
		foreach ( var child in node.Children )
		{
			if ( child.Ref is { } childRef )
			{
				into.Add( childRef.ItemId );
			}

			CollectDescendants( child, into );
		}
	}

	// A wall opening or slab cutout records who made it; disabling that owner has to dirty the host.
	static bool OwnsEffectsOn( object payload, int ownerId )
	{
		return payload switch
		{
			ArchBuilding building => building.Cutouts.Any( cutout => cutout.OwnerId == ownerId ),
			ArchRoom room => room.Walls.SelectMany( wall => wall.Openings ).Any( opening => opening.OwnerId == ownerId ),
			_ => false
		};
	}

	// Builds the tree from typed ownership; explicit records override a payload's parent, kind and
	// stage when present. Old plans carry no records and project unchanged.
	public static ArchLayerTree Project( ArchPlan plan )
	{
		var tree = new ArchLayerTree();

		if ( plan is null )
		{
			return tree;
		}

		var kinds = ArchKinds.Load();
		var entries = new List<Entry>();
		var byId = new Dictionary<int, Entry>();

		void Register( object payload, int id, int parent, ArchKind kind, ArchBuilding building, ArchRoom room,
			int floor = int.MinValue, ArchLayerStage? stage = null )
		{
			var entry = new Entry
			{
				Payload = payload,
				Id = id,
				ParentId = parent,
				Kind = kind,
				Stage = stage,
				Name = kinds.NameOf( kind, payload, id ),
				Building = building,
				Room = room,
				Floor = floor,
			};

			entries.Add( entry );
			byId[id] = entry;
		}

		foreach ( var building in plan.Buildings )
		{
			Register( building, building.Id, 0, ArchKind.Building, building, null );

			foreach ( var room in building.Rooms )
			{
				var kind = room.Spans ? ArchKind.Walkway : ArchKind.Room;
				Register( room, room.Id, building.Id, kind, building, room, room.Floor );

				foreach ( var wall in room.Walls )
				{
					Register( wall, wall.Id, room.Id, ArchKind.Wall, building, room );

					foreach ( var opening in wall.Openings )
					{
						Register( opening, opening.Id, wall.Id, ArchKind.Opening, building, room );
					}

					// The stage is the payload's, not the kind's: a pilaster builds, a recess cuts.
					foreach ( var modifier in wall.Modifiers )
					{
						Register( modifier, modifier.Id, wall.Id, ArchKind.WallMod, building, room,
							stage: modifier.Carves ? ArchLayerStage.Void : ArchLayerStage.Structure );
					}
				}

				foreach ( var stair in room.Stairs )
				{
					Register( stair, stair.Id, room.Id, ArchKind.Stair, building, room );
				}

				foreach ( var trim in room.Trims )
				{
					Register( trim, trim.Id, room.Id, ArchKind.Trim, building, room );
				}

				foreach ( var pillar in room.Pillars )
				{
					Register( pillar, pillar.Id, room.Id, ArchKind.Pillar, building, room );
				}

				foreach ( var span in room.PierSpans )
				{
					Register( span, span.Id, room.Id, ArchKind.Span, building, room );
				}

				foreach ( var beam in room.Beams )
				{
					Register( beam, beam.Id, room.Id, ArchKind.Beam, building, room );
				}

				foreach ( var porch in room.Porches )
				{
					Register( porch, porch.Id, room.Id, ArchKind.Porch, building, room );

					// A porch HOSTS, so its flights, columns and runs are rows under it rather than loose
					// siblings of the room's own - the same shape a walkway's contents take.
					foreach ( var stair in porch.Stairs )
					{
						Register( stair, stair.Id, porch.Id, ArchKind.Stair, building, room );
					}

					foreach ( var pillar in porch.Pillars )
					{
						Register( pillar, pillar.Id, porch.Id, ArchKind.Pillar, building, room );
					}

					foreach ( var trim in porch.Trims )
					{
						Register( trim, trim.Id, porch.Id, ArchKind.Trim, building, room );
					}
				}

				foreach ( var approach in room.Approaches )
				{
					Register( approach, approach.Id, room.Id, ArchKind.Approach, building, room );
				}
			}

			foreach ( var roof in building.Roofs )
			{
				Register( roof, roof.Id, building.Id, ArchKind.Roof, building, null );

				foreach ( var light in roof.Lights )
				{
					Register( light, light.Id, roof.Id, ArchKind.RoofLight, building, null );
				}

				// A wall standing on the deck is a Wall like any other, so it picks, edits and dresses through
				// every path a wall in a room already takes.
				foreach ( var wall in roof.Walls )
				{
					Register( wall, wall.Id, roof.Id, ArchKind.Wall, building, null );

					foreach ( var opening in wall.Openings )
					{
						Register( opening, opening.Id, wall.Id, ArchKind.Opening, building, null );
					}

					foreach ( var modifier in wall.Modifiers )
					{
						Register( modifier, modifier.Id, wall.Id, ArchKind.WallMod, building, null,
							stage: modifier.Carves ? ArchLayerStage.Void : ArchLayerStage.Structure );
					}
				}
			}

			foreach ( var pipe in building.Downpipes )
			{
				Register( pipe, pipe.Id, building.Id, ArchKind.Downpipe, building, null );
			}

			foreach ( var run in building.Pipes )
			{
				Register( run, run.Id, building.Id, ArchKind.Pipe, building, null );
			}

			foreach ( var bracket in building.Brackets )
			{
				Register( bracket, bracket.Id, building.Id, ArchKind.Bracket, building, null );
			}

			foreach ( var fence in building.Fences )
			{
				Register( fence, fence.Id, building.Id, ArchKind.Fence, building, null );
			}

			foreach ( var platform in building.Platforms )
			{
				Register( platform, platform.Id, building.Id, ArchKind.Platform, building, null );

				foreach ( var stair in platform.Stairs )
				{
					Register( stair, stair.Id, platform.Id, ArchKind.CarvedStair, building, null );
				}
			}

			foreach ( var ladder in building.Ladders )
			{
				Register( ladder, ladder.Id, building.Id, ArchKind.Ladder, building, null );
			}

			foreach ( var balcony in building.Balconies )
			{
				Register( balcony, balcony.Id, building.Id, ArchKind.Balcony, building, null );
			}

			foreach ( var flight in building.ExteriorStairs )
			{
				Register( flight, flight.Id, building.Id, ArchKind.ExteriorStair, building, null );
			}

			foreach ( var cut in building.Cuts )
			{
				Register( cut, cut.Id, building.Id, ArchKind.Cut, building, null,
					stage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );
			}
		}

		foreach ( var road in plan.Roads() )
		{
			Register( road, road.Id, 0, ArchKind.Road, null, null );

			foreach ( var crossing in road.Crossings )
			{
				Register( crossing, crossing.Id, road.Id, ArchKind.Crossing, null, null );
			}

			foreach ( var bridge in road.Bridges )
			{
				Register( bridge, bridge.Id, road.Id, ArchKind.Bridge, null, null );
			}

			foreach ( var tunnel in road.Tunnels )
			{
				Register( tunnel, tunnel.Id, road.Id, ArchKind.Tunnel, null, null );
			}


			foreach ( var cut in road.Cuts )
			{
				Register( cut, cut.Id, road.Id, ArchKind.Cut, null, null,
					stage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );
			}
		}

		// Every OTHER top-level unit, which is how a kind an addon brought gets a row in the stack without this walk
		// naming it. A house and a street are walked above only because their children are shapes this assembly knows.
		foreach ( var unit in plan.Units )
		{
			if ( unit is ArchBuilding or ArchRoadPart )
			{
				continue;
			}

			// Under whatever it says it hangs on, which for a unit stored flat is still the building it attached
			// to - where it is FILED and where it BELONGS are two questions, and the row answers the second.
			var owner = kinds.Parent( unit.Kind, unit );

			Register( unit, unit.Id, owner, unit.Kind, plan.FindBuilding( owner ), null,
				stage: kinds.Stage( unit.Kind, unit ) );

			foreach ( var cut in unit.Cuts )
			{
				Register( cut, cut.Id, unit.Id, ArchKind.Cut, null, null,
					stage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );
			}
		}

		// Explicit metadata overrides ownership defaults; a self-parent record is nonsense and falls back.
		foreach ( var record in plan.Layers )
		{
			if ( byId.TryGetValue( record.ItemId, out var entry ) && record.ParentId != record.ItemId )
			{
				entry.ParentId = record.ParentId;
				entry.Kind = record.Kind;
				entry.Stage = record.Stage;
				entry.Enabled = record.Enabled;
				entry.Locked = record.Locked;
				entry.Order = record.Order;
			}
		}

		var nodes = new Dictionary<int, ArchLayerNode>();

		foreach ( var entry in entries )
		{
			var node = new ArchLayerNode
			{
				Ref = new ArchLayerRef { ItemId = entry.Id, Kind = entry.Kind, ParentId = entry.ParentId },
				Kind = entry.Kind,
				Stage = entry.Stage ?? kinds.Stage( entry.Kind ),
				Domain = kinds.Domain( entry.Kind ),
				Payload = entry.Payload,
				Name = entry.Name,
				Enabled = entry.Enabled,
				Locked = entry.Locked,
				Order = entry.Order,
				Floor = entry.Floor,
				Building = entry.Building,
				Room = entry.Room,
			};

			nodes[entry.Id] = node;
			tree.byId[entry.Id] = node;
			tree.byPayload[entry.Payload] = node;
		}

		// A group is a folder, so its members MOVE into it rather than being listed twice - the whole
		// point of the scope is that a layer stands in exactly one of them. A group sits in the domain
		// its members came from, so grouping two houses does not empty the Buildings branch.
		var memberOf = new Dictionary<int, ArchLayerNode>();

		foreach ( var assembly in plan.Assemblies )
		{
			var node = new ArchLayerNode
			{
				Ref = new ArchLayerRef { ItemId = assembly.Id, Kind = ArchKind.Assembly, ParentId = 0 },
				Kind = ArchKind.Assembly,
				Stage = kinds.Stage( ArchKind.Assembly ),
				Domain = DomainOf( assembly, byId, kinds ),
				Payload = assembly,
				Name = assembly.Name,
			};

			nodes[assembly.Id] = node;
			tree.byId[assembly.Id] = node;
			tree.byPayload[assembly] = node;

			foreach ( var childId in assembly.Children.Where( childId => childId != assembly.Id ) )
			{
				memberOf[childId] = node;
			}
		}

		foreach ( var instance in plan.Instances )
		{
			var node = new ArchLayerNode
			{
				Ref = new ArchLayerRef { ItemId = instance.Id, Kind = ArchKind.AssetInstance, ParentId = 0 },
				Kind = ArchKind.AssetInstance,
				Stage = kinds.Stage( ArchKind.AssetInstance ),
				Domain = ArchLayerDomain.Connections,
				Payload = instance,
				Name = instance.Name,
			};

			nodes[instance.Id] = node;
			tree.byId[instance.Id] = node;
			tree.byPayload[instance] = node;
			tree.AddRoot( node );
		}

		// Story headers exist before any room attaches, so floors sort ascending under their building.
		foreach ( var building in plan.Buildings )
		{
			if ( !nodes.TryGetValue( building.Id, out var buildingNode ) )
			{
				continue;
			}

			foreach ( var floor in entries
				.Where( entry => entry.ParentId == building.Id && (entry.Kind == ArchKind.Room || entry.Kind == ArchKind.Walkway) )
				.Select( entry => entry.Floor )
				.Distinct()
				.OrderBy( floor => floor ) )
			{
				var story = new ArchLayerNode
				{
					Kind = ArchKind.Story,
					Stage = ArchLayerStage.Shape,
					Domain = ArchLayerDomain.Buildings,
					Name = $"Level {floor}",
					Floor = floor,
					Building = building,
				};

				story.Parent = buildingNode;
				buildingNode.Children.Add( story );
				tree.stories[(building.Id, floor)] = story;
			}
		}

		// Where each layer would have stood WITHOUT its group, worked out even for a member that moves into one -
		// a group hangs where its members came from, so it needs to know where that was.
		var typedHost = new Dictionary<int, ArchLayerNode>();

		foreach ( var entry in entries )
		{
			var node = nodes[entry.Id];
			var host = entry.ParentId != 0 && nodes.TryGetValue( entry.ParentId, out var parent ) ? parent : null;

			if ( host is not null
				&& host.Kind == ArchKind.Building
				&& (node.Kind == ArchKind.Room || node.Kind == ArchKind.Walkway)
				&& tree.stories.TryGetValue( (entry.ParentId, entry.Floor), out var story ) )
			{
				host = story;
			}

			if ( host is not null )
			{
				typedHost[entry.Id] = host;
			}

			// Group membership outranks typed ownership: the walkway leaves the house that filed it.
			if ( memberOf.TryGetValue( entry.Id, out var group ) )
			{
				group.Children.Add( node );
				node.Parent = group;
				continue;
			}

			if ( host is null )
			{
				tree.AddRoot( node );
				continue;
			}

			host.Children.Add( node );
			node.Parent = host;
		}

		// A group may hold another group: the houses a connector merges gather above it, and the
		// connector is filed beneath the group of what it affects. Nested first and whole, or a
		// group listed before its holder would root itself and then be adopted as well.
		foreach ( var assembly in plan.Assemblies )
		{
			foreach ( var nested in plan.Assemblies.Where( inner => inner.Id != assembly.Id && assembly.Children.Contains( inner.Id ) ) )
			{
				var child = nodes[nested.Id];

				nodes[assembly.Id].Children.Add( child );
				child.Parent = nodes[assembly.Id];
			}
		}

		// An anchor names a layer the group reaches without owning - the far end of a connection.
		// Members already have a row, so only the outside ones are worth stating.
		foreach ( var assembly in plan.Assemblies )
		{
			var node = nodes[assembly.Id];

			// Members sit in the order the group recorded them, which is the shape of the join.
			var member = node.Children.OrderBy( child => Membership( assembly, child ) ).ToList();

			node.Children.Clear();
			node.Children.AddRange( member );

			foreach ( var link in plan.Links.Where( link => link.SourceId == assembly.Id ) )
			{
				tree.byId.TryGetValue( link.TargetId, out var targetNode );

				// An anchor pointing at something already in the group says nothing the rows above it
				// do not - and spelling out its whole path is how a tree turns into a wall of text.
				if ( targetNode is null || Within( targetNode, node ) )
				{
					continue;
				}

				node.Children.Add( new ArchLayerNode
				{
					Kind = ArchKind.Assembly,
					Stage = ArchLayerStage.Reference,
					Domain = node.Domain,
					Payload = new ArchLayerReference { SourcePort = link.SourcePort, TargetId = link.TargetId, TargetPort = link.TargetPort },
					Name = $"{link.SourcePort} → {targetNode.Name}",
					Parent = node,
				} );
			}

			if ( node.Parent is null && Homed( assembly, typedHost ) is { } home )
			{
				home.Children.Add( node );
				node.Parent = home;
			}

			if ( node.Parent is null )
			{
				tree.AddRoot( node );
			}
		}

		foreach ( var group in tree.Domains )
		{
			Sort( group.Children );
		}

		tree.Links = plan.Links;

		return tree;
	}

	// Stage decides evaluation; Order only decides where siblings sit inside their stage, which is
	// what a drag in the stack rearranges.
	static void Sort( List<ArchLayerNode> children )
	{
		if ( children.Count > 1 )
		{
			var ordered = children.OrderBy( child => child.Order ).ToList();

			children.Clear();
			children.AddRange( ordered );
		}

		foreach ( var child in children )
		{
			Sort( child.Children );
		}
	}

	static bool Within( ArchLayerNode node, ArchLayerNode ancestor )
	{
		for ( var current = node; current is not null; current = current.Parent )
		{
			if ( ReferenceEquals( current, ancestor ) )
			{
				return true;
			}
		}

		return false;
	}

	// A GROUP STANDS WHERE ITS MEMBERS STOOD. Folding five runs inside one house into a folder is a scope over
	// those rows, not a move to the top of the plan - rooting it there took them out of the house with it.
	//
	// Members drawn from two different hosts have no one home and root at the domain as before, and so does a group
	// whose members' host is itself a member, which would otherwise hang the folder inside its own contents.
	static ArchLayerNode Homed( ArchSiteAssembly assembly, Dictionary<int, ArchLayerNode> typedHost )
	{
		ArchLayerNode home = null;

		foreach ( var childId in assembly.Children )
		{
			if ( !typedHost.TryGetValue( childId, out var host ) )
			{
				return null;
			}

			home ??= host;

			if ( !ReferenceEquals( home, host ) )
			{
				return null;
			}
		}

		for ( var walk = home; walk is not null; walk = walk.Parent )
		{
			if ( walk.Ref is { } layer && assembly.Children.Contains( layer.ItemId ) )
			{
				return null;
			}
		}

		return home;
	}

	static int Membership( ArchSiteAssembly assembly, ArchLayerNode child )
	{
		var at = child.Ref is { } layer ? assembly.Children.IndexOf( layer.ItemId ) : -1;

		return at < 0 ? int.MaxValue : at;
	}

	// Where the folder sits: with whatever it holds, so a group of houses stays under Buildings.
	static ArchLayerDomain DomainOf( ArchSiteAssembly assembly, Dictionary<int, Entry> byId, ArchKinds kinds )
	{
		foreach ( var childId in assembly.Children )
		{
			if ( byId.TryGetValue( childId, out var entry ) )
			{
				return kinds.Domain( entry.Kind );
			}
		}

		return ArchLayerDomain.Connections;
	}

	void AddRoot( ArchLayerNode node )
	{
		var group = Domains.FirstOrDefault( domain => domain.Domain == node.Domain );

		if ( group is null )
		{
			group = new ArchLayerDomainGroup
			{
				Domain = node.Domain,
				Name = node.Domain switch
				{
					ArchLayerDomain.Buildings => "Buildings",
					ArchLayerDomain.Connections => "Connections",
					_ => "Infrastructure"
				}
			};

			Domains.Add( group );
		}

		group.Children.Add( node );
	}

	sealed class Entry
	{
		public object Payload;
		public int Id;
		public int ParentId;
		public ArchKind Kind;
		public ArchLayerStage? Stage;
		public bool Enabled = true;
		public bool Locked;
		public int Order;
		public string Name;
		public int Floor = int.MinValue;
		public ArchBuilding Building;
		public ArchRoom Room;
	}
}
sunless.lib_architecture / Editor/Output/ArchCull.Interiors.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// One shallow room behind one opening on a facade nobody can walk into. DERIVED, so it carries no id and no
// layer record - exactly as slabs, ceilings, roof decks and platforms carry none.
public sealed class ArchFalseInterior
{
	public ArchBuilding Building { get; init; }
	public ArchRoom Room { get; init; }
	public ArchWall Wall { get; init; }
	public ArchOpening Opening { get; init; }
	// The room face of the wall, in the wall's own frame - the plane the box is measured off.
	public float Face { get; init; }
	public float Depth { get; init; }

	public float Back => Face + Depth;
}

public static partial class ArchCull
{
	// No way in and nothing above, asked of a whole unit rather than one of its roofs: a shell with no door
	// and no archway, capped everywhere, is a facade. Anything else is a room, or becoming one.
	public static bool Unenterable( ArchBuilding building, ArchPlan plan, ArchKit kit )
	{
		if ( building is null )
		{
			return false;
		}

		var walls = building.Rooms.SelectMany( room => room.Walls.Select( wall => (Room: room, Wall: wall) ) ).ToList();

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

		// A single-sided shell emits no room face at all, so there is nothing in there to be in.
		if ( walls.All( standing => ArchWallSection.SingleSided( standing.Wall, standing.Room ) ) )
		{
			return true;
		}

		if ( walls.SelectMany( standing => standing.Wall.Openings ).Any( Entered ) )
		{
			return false;
		}

		return building.Roofs.Count > 0 && building.Roofs.All( roof => Sealed( roof, building, plan, kit ) );
	}

	// A leaf hangs in a way in and an archway is a hole walked straight through. A window is neither.
	static bool Entered( ArchOpening opening )
	{
		return opening.Kind.Hangs() || opening.Kind == OpeningKind.Archway;
	}

	// Every box the plan asks for, resolved without a scene, so the pass, the report and the test all read one
	// answer. A unit ArchCull calls enterable contributes none: behind a real room the box would z-fight the
	// room it is standing inside.
	public static List<ArchFalseInterior> Behind( ArchPlan plan, ArchKit kit )
	{
		var found = new List<ArchFalseInterior>();

		if ( plan is null )
		{
			return found;
		}

		var depth = MathF.Max( 1f, kit.FalseInteriorDepth );

		foreach ( var building in plan.Buildings.Where( unit => Unenterable( unit, plan, kit ) ) )
		{
			foreach ( var room in building.Rooms )
			{
				foreach ( var wall in room.Walls.Where( wall => !ArchWallJoins.CoveredBy( building, room, wall ) ) )
				{
					var face = ArchWallSection.Thickness( wall, kit ) * 0.5f;

					found.AddRange( Showing( wall ).Select( opening => new ArchFalseInterior
					{
						Building = building,
						Room = room,
						Wall = wall,
						Opening = opening,
						Face = face,
						Depth = depth
					} ) );
				}
			}
		}

		return found;
	}

	// The units the wall generator actually cut a hole for - the same filter it runs, because a box behind a
	// hole nothing opened is a box hanging in a solid wall.
	static IEnumerable<ArchOpening> Showing( ArchWall wall )
	{
		return wall.Openings
			.Where( opening => opening.Width > 0.5f && opening.Height > 0.5f )
			.Where( opening => ArchLayerGate.On( opening ) && ArchLayerGate.Owned( opening.OwnerId ) )
			.Where( opening => opening.SwallowedBy == 0 || !ArchLayerGate.Owned( opening.SwallowedBy ) );
	}

	// The box, in the wall's own frame, wound to face the hole. Its front IS the hole: a face on the wall's
	// room plane would be the sticker this exists to remove.
	public static void Line( ArchMesh canvas, ArchFalseInterior interior, ArchBrush brush )
	{
		var opening = interior.Opening;
		var left = opening.Left;
		var right = opening.Right;
		var bottom = MathF.Max( 0f, opening.SillHeight );
		var top = opening.Top;
		var face = interior.Face;
		var back = interior.Back;

		if ( right - left < 0.5f || top - bottom < 0.5f || interior.Depth < 0.5f )
		{
			return;
		}

		canvas.Quad(
			new Vector3( left, back, bottom ),
			new Vector3( right, back, bottom ),
			new Vector3( right, back, top ),
			new Vector3( left, back, top ),
			brush );

		canvas.Quad(
			new Vector3( left, face, bottom ),
			new Vector3( left, back, bottom ),
			new Vector3( left, back, top ),
			new Vector3( left, face, top ),
			brush );

		canvas.Quad(
			new Vector3( right, back, bottom ),
			new Vector3( right, face, bottom ),
			new Vector3( right, face, top ),
			new Vector3( right, back, top ),
			brush );

		canvas.Quad(
			new Vector3( left, face, top ),
			new Vector3( left, back, top ),
			new Vector3( right, back, top ),
			new Vector3( right, face, top ),
			brush );

		canvas.Quad(
			new Vector3( left, back, bottom ),
			new Vector3( left, face, bottom ),
			new Vector3( right, face, bottom ),
			new Vector3( right, back, bottom ),
			brush );
	}

	// The pass on its own, for when the faces have already been cleaned. A rebuild prunes what it emits, the
	// same way it takes back the faces Clean removed.
	public static int Interiors( Scene scene, ArchPlan plan, ArchKit kit )
	{
		var root = ArchScene.FindRoot( scene );

		if ( !root.IsValid() )
		{
			Log.Warning( "Architecture: nothing to line - no generated root in this scene." );

			return 0;
		}

		using ( SceneEditorSession.Active.UndoScope( "Line False Interiors" ).WithGameObjectChanges( root, GameObjectUndoFlags.All ).Push() )
		{
			return Lined( root, plan, kit );
		}
	}

	// Re-derived every run - a door added to a facade makes it a room, and the box behind its window has to GO rather
	// than be left standing inside it - but only HANDED OVER where the box actually changed. Drawing one is five
	// quads; giving it to the engine cooks a collision hull, a physics mesh and a trace mesh, and this pass reaches
	// every window on every facade in the plan.
	//
	// A null cache lines them all, which is what the menu action and a scene nobody has built through mean.
	public static int Lined( GameObject root, ArchPlan plan, ArchKit kit, ArchBuildCache cache = null )
	{
		var nodes = Walls( root );
		var style = new ArchStyle( kit );
		var wanted = Behind( plan, kit ).GroupBy( interior => interior.Wall.Id ).ToDictionary( group => group.Key, group => group.ToList() );
		var lined = 0;

		foreach ( var bare in nodes.Where( entry => !wanted.ContainsKey( entry.Key ) ) )
		{
			Strip( bare.Value );
			cache?.Unlined( bare.Key );
		}

		foreach ( var group in wanted )
		{
			if ( !nodes.TryGetValue( group.Key, out var node ) )
			{
				continue;
			}

			// The wall's own frame, so the boxes are drawn in the coordinates the wall generator uses and the
			// projection puts them on the same grain as the room face they hang behind.
			var canvas = new ArchMesh( node.WorldTransform );

			foreach ( var interior in group.Value )
			{
				Line( canvas, interior, style.Brush(
					ArchSurface.WallInterior, interior.Wall.Palette, interior.Room.Palette, interior.Building.Palette ) );

				lined++;
			}

			// Asked of the box's own key, the way a part is - and of the scene as well, because a hand-delete leaves
			// the cache saying something is standing that is not.
			if ( cache?.Lined( group.Key, canvas.Content ) == true && Standing( node ) )
			{
				continue;
			}

			Strip( node );
			Fit( node, canvas );
		}

		return lined;
	}

	static void Strip( GameObject wall )
	{
		if ( wall.Children.FirstOrDefault( child => child.Name == ArchPieces.Interior ) is { } standing )
		{
			standing.DestroyImmediate();
		}
	}

	static bool Standing( GameObject wall )
	{
		return wall.Children.Any( child => child.Name == ArchPieces.Interior );
	}

	static Dictionary<int, GameObject> Walls( GameObject root )
	{
		var nodes = new Dictionary<int, GameObject>();

		foreach ( var node in ArchScene.Descendants( root ) )
		{
			if ( ArchNames.TryParseId( node.Name, "Wall", out var id ) )
			{
				nodes[id] = node;
			}
		}

		return nodes;
	}

	static void Fit( GameObject wall, ArchMesh canvas )
	{
		if ( canvas.IsEmpty )
		{
			return;
		}

		var node = wall.Scene.CreateObject();

		node.Name = ArchPieces.Interior;
		node.SetParent( wall, false );
		node.Tags.Add( ArchScene.GeneratedTag );

		var renderer = node.Components.GetOrCreate<MeshComponent>();

		renderer.Color = Color.White;
		renderer.SmoothingAngle = 0f;

		// It must not be traceable: the cleaning pass reads cover by ray, and a box behind a window it could
		// hit would have the elevation in front of it stripped as buried.
		ArchCollision.Write( node, renderer, canvas, ArchCollisionMode.None );

		renderer.Mesh = canvas.Finish();
	}
}
sunless.lib_architecture / Editor/Carve/ArchCarveVolume.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// The fall is what makes a raked deck a carveable solid - not a lofted quad.
public readonly struct ArchCarvePlane
{
	public float Datum { get; init; }
	public Vector2 Origin { get; init; }
	public Vector2 Fall { get; init; }

	public static ArchCarvePlane Level( float height ) => new() { Datum = height };

	public static ArchCarvePlane Through( Vector2 origin, float datum, Vector2 fall )
	{
		return new ArchCarvePlane { Origin = origin, Datum = datum, Fall = fall };
	}

	public bool Rakes => Fall.Length > 0.0001f;

	public float At( Vector2 point ) => Datum + Vector2.Dot( Fall, point - Origin );

	public ArchCarvePlane Raised( float by ) => new() { Datum = Datum + by, Origin = Origin, Fall = Fall };

	// Where a ray meets this plane. A raked plane and a ray are both linear, so the crossing solves in closed
	// form; every deck a cursor can come to rest on is measured through here so a roof and a ramp cannot answer
	// the same ray two different ways.
	public bool Crosses( Ray ray, out float reach, out Vector3 hit )
	{
		reach = 0f;
		hit = default;

		var closing = ray.Forward.z - Vector2.Dot( Fall, new Vector2( ray.Forward.x, ray.Forward.y ) );

		if ( MathF.Abs( closing ) < 0.0001f )
		{
			return false;
		}

		reach = (At( new Vector2( ray.Position.x, ray.Position.y ) ) - ray.Position.z) / closing;

		if ( reach < 0f )
		{
			return false;
		}

		hit = ray.Position + ray.Forward * reach;

		return true;
	}
}

// A ruined edge, carried by the volume that takes the bite. The seed is the cut's own id, so the same shaft
// hands back the same ruin after a hotload and the build cache is not defeated by a vertex that moved.
public readonly struct ArchCarveBreak
{
	public int Seed { get; init; }
	public float Jitter { get; init; }

	public bool Breaks => Seed != 0 && Jitter > ArchGridService.FinestSize;
}

// Both sides of a carve - the solid and the cut - are this same volume.
public readonly struct ArchCarveVolume
{
	public IReadOnlyList<Vector2> Footprint { get; init; }
	public ArchCarvePlane Floor { get; init; }
	public ArchCarvePlane Ceiling { get; init; }
	public ArchCarveBreak Break { get; init; }

	public static ArchCarveVolume Over( IReadOnlyList<Vector2> footprint, float from, float to )
	{
		return new ArchCarveVolume
		{
			Footprint = footprint,
			Floor = ArchCarvePlane.Level( MathF.Min( from, to ) ),
			Ceiling = ArchCarvePlane.Level( MathF.Max( from, to ) )
		};
	}

	public ArchCarveVolume Breaking( ArchCarveBreak breaking )
	{
		return new ArchCarveVolume { Footprint = Footprint, Floor = Floor, Ceiling = Ceiling, Break = breaking };
	}

	// A level base under a raked top - the wedge a ramp is. Not Raked(): that is two parallel planes a constant
	// thickness apart, which is a sloping slab and not something whose foot meets the ground it stands on.
	public static ArchCarveVolume Under( IReadOnlyList<Vector2> footprint, float from, ArchCarvePlane top )
	{
		return new ArchCarveVolume { Footprint = footprint, Floor = ArchCarvePlane.Level( from ), Ceiling = top };
	}

	// The same wedge upside down, and the shape a SUBTRACTED ramp is: nothing taken out at the head, the whole
	// band gone at the foot, so what is left under it keeps the raked floor as its own top.
	public static ArchCarveVolume Above( IReadOnlyList<Vector2> footprint, ArchCarvePlane floor, float to )
	{
		return new ArchCarveVolume { Footprint = footprint, Floor = floor, Ceiling = ArchCarvePlane.Level( to ) };
	}

	// The two faces are parallel, which is what keeps the interval algebra one-dimensional.
	public static ArchCarveVolume Raked( IReadOnlyList<Vector2> footprint, ArchCarvePlane plane, float thickness )
	{
		var depth = MathF.Max( 0.05f, thickness );

		return new ArchCarveVolume { Footprint = footprint, Floor = plane, Ceiling = plane.Raised( depth ) };
	}

	public bool Rakes => Floor.Rakes || Ceiling.Rakes;

	public bool Covers( Vector2 point ) => ArchFootprint.Encloses( new[] { Footprint }, point );
}
sunless.lib_architecture / Editor/Data/ArchArchetypeSteps.cs
Editor library
namespace Sunless.Architecture;

public readonly record struct ArchArchetypeStepContext(
	ArchPlan Plan,
	ArchBuilding Building,
	ArchKit Kit,
	ArchArchetypeStep Step,
	Vector2 Min,
	Vector2 Max,
	ArchRoom Room,
	List<string> Skipped );

public interface IArchArchetypeStepBuilder
{
	ArchStepKind Kind { get; }

	string Build( ArchArchetypeStepContext context );
}

public sealed class ArchArchetypeSteps : ArchTable<ArchArchetypeSteps, ArchStepKind, IArchArchetypeStepBuilder>
{
	protected override IEnumerable<IArchArchetypeStepBuilder> Standing()
	{
		yield return new ArchPorchArchetypeStep();
	}

	protected override ArchStepKind KeyOf( IArchArchetypeStepBuilder builder ) => builder.Kind;

	protected override string Collision( IArchArchetypeStepBuilder standing, IArchArchetypeStepBuilder builder )
	{
		return $"{builder.Kind} is built by both {standing.GetType().Name} and {builder.GetType().Name}";
	}

	public IArchArchetypeStepBuilder For( ArchStepKind kind ) => Held( kind );
}
sunless.lib_architecture / Editor/Data/ArchBuild.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Auto lays the ridge along the longer side, so the flat ends land on the short walls.
public enum RidgeRun
{
	Auto,
	AlongX,
	AlongY
}

public enum SectionRoof
{
	Continue,
	Hip,
	Gable,
	Capped,
	None
}

public sealed class ArchSection
{
	public ArchRoom Room { get; init; }
	public ArchRoofPart Roof { get; init; }
	public int SharedWalls { get; init; }
	public bool Merged { get; init; }
}

// Shared by the Building subtool and the arch_* tools - both grow a plan the same way.
public static class ArchBuild
{
	// THE STOREY GRID, and the only answer to it. A building's own override first; otherwise the storey it
	// actually STANDS - its ground rooms' real plate plus a floor - never the one the kit would have stood. An
	// archetype gives its shell taller walls than the kit's, and a grid that never learns seats the next floor
	// mid-wall inside the storey below, with every column under it standing straight through the slab.
	public static float StoreyHeight( ArchBuilding building, ArchKit kit )
	{
		if ( building is { StoreyHeight: > 1f } )
		{
			return building.StoreyHeight;
		}

		return Ground( building, kit ) + kit.FloorThickness;
	}

	public static float FloorOf( ArchBuilding building, ArchKit kit, int level )
	{
		return level * StoreyHeight( building, kit ) + kit.GroundClearance;
	}

	// The tallest room on the ground, because a storey is as tall as the walls the floor above lands on. A
	// building with nothing standing yet is the kit's, which is what the first drag needs.
	static float Ground( ArchBuilding building, ArchKit kit )
	{
		var standing = 0f;

		foreach ( var room in building?.Rooms ?? Enumerable.Empty<ArchRoom>() )
		{
			if ( room.Floor != 0 || room.Spans )
			{
				continue;
			}

			standing = MathF.Max( standing, ArchFloorGen.WallHeight( room, kit ) );
		}

		return standing > 1f ? standing : kit.WallHeight;
	}

	public static ArchSection Shell(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		RoofStyle? style,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto )
	{
		var placement = new ArchBoundaryPlacementService( plan, kit ).Outside( level, min, max );

		if ( !placement.IsUsable )
		{
			return null;
		}

		min = placement.Min;
		max = placement.Max;
		var shell = CreateRoom( plan, building, "Shell", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );

		if ( style is null )
		{
			return new ArchSection { Room = shell };
		}

		var roof = Roof( plan, kit, level, style.Value, min, max, baseHeight + kit.WallHeight, gutters, false, ridge );
		building.Roofs.Add( roof );

		return new ArchSection { Room = shell, Roof = roof };
	}

	public static ArchSection Partition(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor )
	{
		( min, max ) = new ArchGridService().Rectangle( min, max );
		var party = new List<(Vector2 From, Vector2 To)>();
		var room = CreateRoom( plan, building, $"Room{building.Rooms.Count + 1}", level, baseHeight, kit.WallHeight, min, max, floor, party, kit.WallThickness );

		return new ArchSection { Room = room, SharedWalls = party.Count };
	}

	// Two buildings on one plot each roof their own storey and the pair clip each other into nonsense.
	public static bool Stacks( ArchBuilding building, ArchKit kit, Vector2 min, Vector2 max )
	{
		if ( building is null || !building.HasContent )
		{
			return false;
		}

		var covered = ArchRegion.Shell( ArchRegion.Footprints( building.Rooms ), kit.WallThickness );

		return ArchRegion.Covers( covered, ArchFootprint.Rect( min, max ) );
	}

	public static ArchSection Storey(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		RoofStyle? style,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto )
	{
		( min, max ) = new ArchGridService().Rectangle( min, max );
		var snapped = new ArchWingPlacementService( building, kit, level ).Snap( min, max );
		min = snapped.Min;
		max = snapped.Max;

		var room = CreateRoom( plan, building, $"Storey{level}", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );
		var plate = baseHeight + kit.WallHeight;
		var outline = ArchFootprint.Rect( min, max );

		if ( DeckBuiltOverBy( building, outline ) is { } below )
		{
			below.Level = level;
			below.BaseHeight = plate;
			below.Reshape( outline );

			return new ArchSection { Room = room, Roof = below, Merged = true };
		}

		if ( style is null )
		{
			return new ArchSection { Room = room };
		}

		var roof = Roof( plan, kit, level, style.Value, min, max, plate, gutters, false, ridge );
		building.Roofs.Add( roof );

		return new ArchSection { Room = room, Roof = roof };
	}

	// Only a FULLY covered deck is rebuilt - a partial one is a real stepped building.
	static ArchRoofPart DeckBuiltOverBy( ArchBuilding building, List<Vector2> outline )
	{
		return building.Roofs.FirstOrDefault( roof => ArchRegion.Covers( new[] { outline }, roof.Outline() ) );
	}

	// Continuing folds the wing into the section's footprint (valleys inside); otherwise its own roof, stepped by the eave drop.
	public static ArchSection Extend(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		SectionRoof choice,
		float drop,
		bool floor,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto )
	{
		( min, max ) = new ArchGridService().Rectangle( min, max );

		return new ArchWingGenerationService( plan, building, kit )
			.On( level, baseHeight, min, max )
			.WithRoof( choice, drop, gutters, ridge )
			.WithFloor( floor )
			.Create();
	}

	public static ArchRoofPart Roof(
		ArchPlan plan,
		ArchKit kit,
		int level,
		RoofStyle style,
		Vector2 min,
		Vector2 max,
		float baseHeight,
		bool gutters,
		bool parapet,
		RidgeRun ridge = RidgeRun.Auto )
	{
		return new ArchRoofPart
		{
			Id = plan.AllocateId(),
			Name = "Roof",
			Level = level,
			Style = style,
			Min = min,
			Max = max,
			Footprint = ArchFootprint.Rect( min, max ),
			BaseHeight = baseHeight,
			Pitch = kit.RoofPitch,
			RidgeAlongX = Ridged( ridge, min, max ),
			// A parapet stands on the wall line - an overhang would leave the coping floating.
			Overhang = parapet ? 0f : kit.RoofOverhang,
			Thickness = kit.RoofThickness,
			Gutters = gutters && !parapet,
			Fascia = !parapet,
			Soffit = !parapet,
			Parapet = parapet,
			Ceiling = Ceiling( style )
		};
	}

	// Shared with the ghost, so what is drawn is what gets built.
	public static bool Ridged( RidgeRun ridge, Vector2 min, Vector2 max ) => ridge switch
	{
		RidgeRun.AlongX => true,
		RidgeRun.AlongY => false,
		_ => max.x - min.x >= max.y - min.y
	};

	// Hip/gable leave a void, so they get a ceiling; shed/sawtooth are meant to be seen from underneath.
	static bool Ceiling( RoofStyle style )
	{
		return style is RoofStyle.Hip or RoofStyle.Gable;
	}

	// The one mapping for a wing's roof choice - the ghost, the generator and the re-dress must agree.
	public static RoofStyle Winged( SectionRoof choice )
	{
		return choice switch
		{
			SectionRoof.Gable => RoofStyle.Gable,
			SectionRoof.Capped => RoofStyle.Flat,
			_ => RoofStyle.Hip
		};
	}

	internal static ArchRoom CreateRoom(
		ArchPlan plan,
		ArchBuilding building,
		string name,
		int level,
		float baseHeight,
		float wallHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		List<(Vector2 From, Vector2 To)> shared,
		float thickness )
	{
		var room = new ArchRoom
		{
			Id = plan.AllocateId(),
			Name = name,
			Floor = level,
			BaseHeight = baseHeight,
			WallHeight = wallHeight,
			HasFloor = floor,
			Footprint = ArchFootprint.Rect( min, max )
		};

		for ( var index = 0; index < room.Footprint.Count; index++ )
		{
			var start = room.Footprint[index];
			var end = room.Footprint[(index + 1) % room.Footprint.Count];

			room.Walls.Add( new ArchWall
			{
				Id = plan.AllocateId(),
				Start = start,
				End = end,
				Exterior = true,
				Cap = true
			} );

			if ( shared is not null && ArchWallJoins.PartyWallExists( building, room.BaseHeight, start, end, thickness ) )
			{
				shared.Add( (start, end) );
			}
		}

		building.Rooms.Add( room );

		return room;
	}

}
sunless.lib_architecture / Editor/Data/ArchCatalogs.cs
Editor library
namespace Sunless.Architecture;

// Which shelf a catalog stands on. A role rather than an interface per catalog, because the catalogs differ only in
// what they hold - a module brings its own with new ArchShelf( role ), and the reservation only stops two of them
// meaning the same thing.
public readonly record struct ArchShelf( string Role )
{
	public static readonly ArchShelf Openings = new( "openings" );
	public static readonly ArchShelf Walls = new( "walls" );
	public static readonly ArchShelf Pillars = new( "pillars" );
	public static readonly ArchShelf RoadLines = new( "roadlines" );
	public static readonly ArchShelf Profiles = new( "profiles" );
	public static readonly ArchShelf Cornices = new( "cornices" );
	public static readonly ArchShelf Types = new( "types" );

	public override string ToString() => Role;
}

// One catalog of authored types, owned by the module whose subtool authors them. The TYPE itself stays in core: a
// porch fits an opening preset it never authored, so a preset is shared geometry by the same rule a payload is,
// while the catalog around it belongs to one module alone.
public interface IArchCatalog
{
	ArchShelf Shelf { get; }

	string Named( object entry );

	// What the module brings with it; an authored entry of the same name wins, so a tuned preset survives upgrades.
	IEnumerable<object> Shipped();

	// What stands on disk beside it, which tops what ships.
	IEnumerable<object> Authored();

	bool Save( object entry );
}

public sealed class ArchCatalogs : ArchTable<ArchCatalogs, ArchShelf, IArchCatalog>
{
	protected override IEnumerable<IArchCatalog> Standing()
	{
		yield return new ArchArchetypeCatalog();
		yield return new ArchProfileCatalog();
		yield return new ArchCorniceCatalog();
		yield return new ArchOpeningCatalog();
		yield return new ArchWallCatalog();
		yield return new ArchPillarCatalog();
		yield return new ArchRoadLineCatalog();
	}

	protected override ArchShelf KeyOf( IArchCatalog catalog ) => catalog.Shelf;

	protected override bool Accepts( IArchCatalog catalog ) => !string.IsNullOrWhiteSpace( catalog.Shelf.Role );

	protected override string Collision( IArchCatalog standing, IArchCatalog catalog )
	{
		return $"{catalog.Shelf} is claimed by both {standing.GetType().Name} and {catalog.GetType().Name}";
	}

	// A shelf no catalog claims stands empty rather than erroring.
	public IArchCatalog On( ArchShelf shelf ) => Held( shelf );

	public List<T> Ships<T>( ArchShelf shelf ) where T : class
	{
		return On( shelf ) is { } catalog ? catalog.Shipped().OfType<T>().ToList() : new List<T>();
	}

	// For a catalog whose authored folder IS the offering - a picker that browses what an author has on disk.
	public List<T> Authors<T>( ArchShelf shelf ) where T : class
	{
		return On( shelf ) is { } catalog ? catalog.Authored().OfType<T>().ToList() : new List<T>();
	}

	// What ships never displaces what is already held, and what is authored on disk always does - so an upgrade
	// brings new presets in beside the tuned ones and a written file is the last word on its own name.
	public void Stock<T>( ArchShelf shelf, List<T> saved ) where T : class
	{
		if ( saved is null || On( shelf ) is not { } catalog )
		{
			return;
		}

		foreach ( var entry in catalog.Shipped().OfType<T>() )
		{
			if ( !saved.Any( existing => Same( catalog.Named( existing ), catalog.Named( entry ) ) ) )
			{
				saved.Add( entry );
			}
		}

		foreach ( var entry in catalog.Authored().OfType<T>() )
		{
			var index = saved.FindIndex( existing => Same( catalog.Named( existing ), catalog.Named( entry ) ) );

			if ( index >= 0 )
			{
				saved[index] = entry;
			}
			else
			{
				saved.Add( entry );
			}
		}
	}

	public bool Save( ArchShelf shelf, object entry ) => On( shelf ) is { } catalog && catalog.Save( entry );

	static bool Same( string first, string second ) => string.Equals( first, second, StringComparison.OrdinalIgnoreCase );
}

// For a one-off caller holding no table of its own. Reaching through it in a loop loads the shelf every time.
public static class ArchShelved
{
	public static List<T> Ships<T>( ArchShelf shelf ) where T : class => ArchCatalogs.Load().Ships<T>( shelf );

	public static List<T> Authors<T>( ArchShelf shelf ) where T : class => ArchCatalogs.Load().Authors<T>( shelf );

	public static bool Save( ArchShelf shelf, object entry ) => ArchCatalogs.Load().Save( shelf, entry );
}
sunless.lib_architecture / Editor/Designers/ArchOpeningKinds.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

// The one table: generators, designer and property sheet ask here, so no control lies.
public static class ArchOpeningKinds
{
	public static bool Glazes( this OpeningKind kind ) => kind == OpeningKind.Window;

	public static bool Sills( this OpeningKind kind ) => kind == OpeningKind.Window;

	public static bool Hangs( this OpeningKind kind ) => kind is OpeningKind.Door or OpeningKind.DoubleDoor or OpeningKind.Garage;

	// Furniture is fixed to the frame the hole wears, so an archway carries none of it: its jambs are the wall's
	// own finish carried through the thickness and there is nothing there to bolt a grille to.
	public static bool Carries( this OpeningKind kind, OpeningFurniture furniture ) => furniture switch
	{
		OpeningFurniture.Bars => kind is OpeningKind.Window or OpeningKind.Hatch,
		OpeningFurniture.Boarded => kind is OpeningKind.Window or OpeningKind.Door or OpeningKind.DoubleDoor or OpeningKind.Hatch,
		OpeningFurniture.Shutter => kind is OpeningKind.Garage or OpeningKind.Window,
		OpeningFurniture.Gate => kind is OpeningKind.Door or OpeningKind.DoubleDoor,
		OpeningFurniture.Leaves => kind == OpeningKind.Window,
		_ => false
	};

	// A hood stands on the wall over the head, so it wants a hole with a frame under it to look bolted to. An
	// archway has none, a hatch is too small to carry one and a garage's head is where the door stacks.
	public static bool Wears( this OpeningKind kind, OpeningHood hood )
	{
		return hood != OpeningHood.None && kind is OpeningKind.Window or OpeningKind.Door or OpeningKind.DoubleDoor;
	}

	public static IEnumerable<OpeningHood> Hoods( this OpeningKind kind )
	{
		return Enum.GetValues<OpeningHood>().Where( hood => kind.Wears( hood ) );
	}

	// What a picker may offer, read off the same rows the generator builds from - so a form and a wall cannot
	// disagree about what a kind has.
	public static IEnumerable<OpeningFurniture> Furnishings( this OpeningKind kind )
	{
		return Enum.GetValues<OpeningFurniture>().Where( furniture => kind.Carries( furniture ) );
	}

	public static bool Furnishes( this OpeningKind kind ) => kind.Furnishings().Any();
}
sunless.lib_architecture / Editor/Designers/ArchOpeningPlacement.cs
Editor library
using System;
using Sandbox;

namespace Sunless.Architecture;

public readonly struct ArchOpeningPoint
{
	public ArchWall Wall { get; init; }
	public ArchRoom Room { get; init; }
	public ArchBuilding Building { get; init; }
	public float Along { get; init; }
	public float Height { get; init; }
	public bool WallPlane { get; init; }
}

// The four answers a drag needs beyond the cursor, held here because both opening tools drag the
// same way and a toggle that lived on one of them would be a second set of rules.
public sealed class ArchOpeningDrag
{
	public bool RestrictStartHeight { get; set; }
	public bool RestrictEndHeight { get; set; }
	public bool PadSides { get; set; } = true;
	public bool ClearCornice { get; set; }
}

public sealed class ArchOpeningPlacement
{
	public ArchOpeningDrag Drag { get; } = new();

	ArchOpeningPoint anchor;
	bool dragging;

	public void Update(
		ArchTool tool,
		bool focused,
		Action<ArchOpeningPoint, ArchOpeningPoint, bool> preview,
		Action<ArchOpeningPoint, ArchOpeningPoint, bool> place )
	{
		if ( !focused || !Point( tool, out var current ) )
		{
			return;
		}

		if ( Gizmo.WasLeftMousePressed )
		{
			anchor = current;
			dragging = true;
			return;
		}

		using ( ArchGhost.Begin() )
		{
			preview( dragging ? anchor : current, current, dragging );
		}

		if ( !dragging || !Gizmo.WasLeftMouseReleased )
		{
			return;
		}

		dragging = false;
		tool.EnsureTarget();

		var unit = tool.Grid.SubgridSize( 4 );
		var resized = MathF.Abs( current.Along - anchor.Along ) >= unit
			|| MathF.Abs( current.Height - anchor.Height ) >= unit;

		place( anchor, current, resized );
	}

	public static ArchOpeningShape Shape(
		ArchOpeningPoint from,
		ArchOpeningPoint to,
		ArchOpeningPreset preset,
		ArchGridService grid,
		ArchKit kit,
		ArchOpeningDrag drag )
	{
		var unit = grid.SubgridSize( 4 );
		var wallHeight = ArchWallSection.Height( from.Wall, from.Room, kit );
		var along = MathF.Abs( to.Along - from.Along );
		var vertical = MathF.Abs( to.Height - from.Height );
		var resizedWidth = along >= unit;
		var resizedHeight = vertical >= unit;
		var resizedVerticalSpan = resizedHeight && (!drag.RestrictStartHeight || !drag.RestrictEndHeight);
		var minimumWidth = resizedWidth ? unit : ArchGridService.FinestSize;
		var minimumHeight = resizedVerticalSpan ? unit : ArchGridService.FinestSize;
		var headroom = drag.ClearCornice
			? ArchOpeningClearance.Head( from.Room, from.Building, from.Wall, kit, wallHeight )
			: wallHeight;

		// Both ends are clamped into the run BEFORE the width is taken from them, so a drag that
		// overshoots a corner lands exactly on the margin instead of leaving whatever the cursor
		// happened to be short by. Measuring the width first and centring it afterwards is what
		// left an uneven sliver at each end of a full-length window.
		ArchOpeningSeats.Usable( from.Wall, preset, kit, drag.PadSides, minimumWidth, out var runFrom, out var runTo );

		float left;
		float right;

		if ( resizedWidth )
		{
			left = Math.Clamp( MathF.Min( from.Along, to.Along ), runFrom, runTo );
			right = Math.Clamp( MathF.Max( from.Along, to.Along ), runFrom, runTo );
		}
		else
		{
			ArchOpeningSeats.Centre( from.Along, ArchGridService.Fine( preset.Width ), runFrom, runTo, out left, out right );
		}

		var width = MathF.Max( minimumWidth, right - left );
		var offset = (left + right) * 0.5f;

		var wallPlaneDrag = resizedHeight && from.WallPlane && to.WallPlane;
		var presetSill = ArchGridService.Fine( preset.SillHeight );
		var presetTop = ArchGridService.Fine( preset.SillHeight + preset.Height );
		var sill = wallPlaneDrag && !drag.RestrictStartHeight
			? grid.Subgrid( MathF.Min( from.Height, to.Height ), 4 )
			: presetSill;
		var top = wallPlaneDrag && !drag.RestrictEndHeight
			? grid.Subgrid( MathF.Max( from.Height, to.Height ), 4 )
			: presetTop;

		sill = Math.Clamp( sill, 0f, MathF.Max( 0f, headroom - minimumHeight ) );
		top = Math.Clamp( top, minimumHeight, headroom );

		if ( top - sill < minimumHeight )
		{
			if ( drag.RestrictEndHeight && !drag.RestrictStartHeight )
			{
				sill = MathF.Max( 0f, top - minimumHeight );
			}
			else
			{
				top = MathF.Min( headroom, sill + minimumHeight );
			}
		}

		return new ArchOpeningShape
		{
			Offset = offset,
			Width = width,
			Height = top - sill,
			SillHeight = sill
		};
	}

	bool Point( ArchTool tool, out ArchOpeningPoint point )
	{
		if ( dragging )
		{
			return FixedWallPoint( tool, anchor, out point );
		}

		return NearestWallPoint( tool, out point );
	}

	static bool NearestWallPoint( ArchTool tool, out ArchOpeningPoint point )
	{
		point = default;

		if ( WallPlanePoint( tool, Gizmo.CurrentRay, null, out point ) )
		{
			return true;
		}

		// The AIM, not the settled point: which wall is being pointed at is a measurement, and the station along it
		// is put on the ladder below anyway. Asked of a snapped point, the search reads whichever wall the snap had
		// already chosen rather than the one under the cursor.
		if ( !tool.AimPoint( out var ground ) )
		{
			return false;
		}

		var wall = tool.NearestWall( ground, out var room, out var along, out var distance );

		if ( wall is null || room is null || distance > 64f )
		{
			return false;
		}

		point = new ArchOpeningPoint
		{
			Wall = wall,
			Room = room,
			Building = tool.Plan.OwnerOf( room ),
			Along = tool.Grid.Subgrid( along, 4 ),
			Height = 0f,
			WallPlane = false
		};

		return true;
	}

	static bool FixedWallPoint( ArchTool tool, ArchOpeningPoint anchor, out ArchOpeningPoint point )
	{
		if ( WallPlanePoint( tool, Gizmo.CurrentRay, anchor.Wall, out point ) )
		{
			return true;
		}

		if ( !tool.AimPoint( out var ground ) )
		{
			point = anchor;
			return true;
		}

		var along = Math.Clamp(
			Vector2.Dot( ground - anchor.Wall.Start, anchor.Wall.Direction ),
			0f,
			anchor.Wall.Length );
		var wallPoint = anchor.Wall.PointAt( along );

		point = new ArchOpeningPoint
		{
			Wall = anchor.Wall,
			Room = anchor.Room,
			Building = anchor.Building,
			Along = tool.Grid.Subgrid( along, 4 ),
			Height = tool.Grid.Subgrid( MathF.Abs( Vector2.Dot( ground - wallPoint, anchor.Wall.Normal ) ), 4 ),
			WallPlane = false
		};

		return true;
	}

	static bool WallPlanePoint( ArchTool tool, Ray ray, ArchWall fixedWall, out ArchOpeningPoint point )
	{
		point = default;
		var nearest = float.MaxValue;

		foreach ( var building in tool.Plan.Buildings )
		{
			var lift = ArchAsks.Lift( tool.Plan, building, tool.Kit );

			foreach ( var room in building.Rooms )
			{
				if ( room.Floor != tool.Level )
				{
					continue;
				}

				var baseHeight = room.BaseHeight + lift;
				var wallHeight = room.WallHeight > 0f ? room.WallHeight : tool.Kit.WallHeight;

				foreach ( var wall in room.Walls )
				{
					if ( fixedWall is not null && wall != fixedWall )
					{
						continue;
					}

					var denominator = ray.Forward.x * wall.Normal.x + ray.Forward.y * wall.Normal.y;

					if ( MathF.Abs( denominator ) < 0.0001f )
					{
						continue;
					}

					var offset = new Vector2( ray.Position.x, ray.Position.y ) - wall.Start;
					var distance = -Vector2.Dot( offset, wall.Normal ) / denominator;

					if ( distance < 0f || distance >= nearest )
					{
						continue;
					}

					var hit = ray.Position + ray.Forward * distance;
					var flat = new Vector2( hit.x, hit.y );
					var along = Vector2.Dot( flat - wall.Start, wall.Direction );
					var height = hit.z - baseHeight;

					if ( fixedWall is null
						&& (along < -8f || along > wall.Length + 8f || height < -8f || height > wallHeight + 8f) )
					{
						continue;
					}

					nearest = distance;
					point = new ArchOpeningPoint
					{
						Wall = wall,
						Room = room,
						Building = building,
						Along = tool.Grid.Subgrid( Math.Clamp( along, 0f, wall.Length ), 4 ),
						Height = tool.Grid.Subgrid( Math.Clamp( height, 0f, wallHeight ), 4 ),
						WallPlane = true
					};
				}
			}
		}

		return nearest < float.MaxValue;
	}
}
sunless.lib_architecture / Editor/Geometry/ArchGuards.cs
Editor library
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// One run of edge protection, resolved for a generator and read back by the tests.
public sealed class ArchGuardRun
{
	public ArchBarrierShape Shape { get; init; }
	public ArchBarrierSpec Spec { get; init; }
	public bool Closed { get; init; }
	public float Length { get; init; }

	// A closed run's last station IS its first, and posted twice it stands two posts inside each other.
	public Func<float, bool> Doubled()
	{
		if ( !Closed )
		{
			return null;
		}

		var closing = Length - 0.5f;

		return distance => distance > closing;
	}
}

// The barrier engine already owns posts, bays and rails; a deck only says where its edge is and how high to stand
// on it. A roof's guard and a platform's are the same rail, so the spec is core geometry.
public static class ArchGuards
{
	// Bays are authored EDGE BY EDGE, so every station lands either on a corner or inside one straight stretch.
	// Divided by arc length alone a bay spans a corner and its rail cuts the corner off.
	public static ArchBarrierSpec Spec( float height, ArchKit kit, ArchRunPath run, BarrierStyle style = BarrierStyle.PostAndRail )
	{
		var spec = new ArchBarrierSpec
		{
			Style = style,
			Ground = BarrierGround.Level,
			Height = height,
			PanelLength = ArchGuardStyles.Bay( style, kit ),
			PostSize = MathF.Max( 1f, kit.RoofGuardPost ),
			PanelThickness = kit.SolidGuardThickness,
			Rails = 2
		};

		for ( var edge = 0; edge < run.Edges; edge++ )
		{
			var division = ArchDivide.AtMost( (run.At( edge + 1 ) - run.At( edge )).Length, spec.PanelLength );

			for ( var bay = 0; bay < division.Count; bay++ )
			{
				spec.Bays.Add( new ArchBarrierBay { Span = division.Step } );
			}
		}

		return spec;
	}
}
sunless.lib_architecture / Editor/Geometry/ArchWallFaces.cs
Editor library
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Which way a wall faces out of its building, decided by probing both sides for a room rather than by the winding
// the wall was authored in. Fixtures, approaches and anything else seated on an exterior face reads this.
public static class ArchWallFaces
{
	public static bool TryOutward( ArchWall wall, ArchRoom room, ArchBuilding building, out Vector2 outward )
	{
		var normal = wall.Normal;
		var midpoint = wall.PointAt( wall.Length * 0.5f );
		var probe = MathF.Max( 4f, wall.Thickness );
		var positiveOccupied = Occupied( building, room.Floor, midpoint + normal * probe );
		var negativeOccupied = Occupied( building, room.Floor, midpoint - normal * probe );

		if ( positiveOccupied == negativeOccupied )
		{
			outward = default;

			return false;
		}

		outward = positiveOccupied ? -normal : normal;

		return true;
	}

	public static Vector2 Outward( ArchWall wall, ArchRoom room, ArchBuilding building )
	{
		if ( TryOutward( wall, room, building, out var outward ) )
		{
			return outward;
		}

		var normal = wall.Normal;
		var midpoint = wall.PointAt( wall.Length * 0.5f );

		return ArchFloorGen.Contains( ArchFloorGen.Footprint( room ), midpoint + normal * 4f ) ? -normal : normal;
	}

	static bool Occupied( ArchBuilding building, int level, Vector2 point )
	{
		if ( building is null )
		{
			return false;
		}

		foreach ( var room in building.Rooms )
		{
			if ( room.Floor == level && ArchFloorGen.Contains( ArchFloorGen.Footprint( room ), point ) )
			{
				return true;
			}
		}

		return false;
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 598,
    "Files": [
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Carve/ArchCut.cs",
            "FileName": "ArchCut.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// One resolution for ghost, generator and report; a stepped cut's band travels its whole chain.\npublic static class ArchCut\n{\n\tpublic const float MinRun = 18f;\n\n\tpublic static Vector2 WorldBand( ArchCutSegment segment, float lift )\n\t{\n\t\treturn new Vector2( segment.BaseHeight + lift, segment.TopHeight + lift );\n\t}\n\n\t// Held for the build: every host a shaft reaches asked for the same volumes, and each ask walked the whole\n\t// chain and built a footprint per step.\n\tpublic static IReadOnlyList<ArchCarveVolume> Resolve( ArchCutPart cut, ArchKit kit )\n\t{\n\t\treturn ArchBuildMemo.Held( memo => memo.Volumes, cut?.Id ?? 0, () => Resolved( cut, kit ).ToList() );\n\t}\n\n\tstatic IEnumerable<ArchCarveVolume> Resolved( ArchCutPart cut, ArchKit kit )\n\t{\n\t\tif ( cut is not { HasContent: true } )\n\t\t{\n\t\t\tyield break;\n\t\t}\n\n\t\tif ( cut.IsDamage )\n\t\t{\n\t\t\tforeach ( var volume in ArchDamage.Resolve( cut, kit ) )\n\t\t\t{\n\t\t\t\tyield return volume;\n\t\t\t}\n\n\t\t\tyield break;\n\t\t}\n\n\t\tvar legs = cut.Segments.Where( segment => segment.HasLoop || segment.Length > 1f ).ToList();\n\t\tvar breaking = Breaking( cut, kit );\n\n\t\tif ( cut.Profile != CutProfile.Steps )\n\t\t{\n\t\t\tforeach ( var leg in legs )\n\t\t\t{\n\t\t\t\tyield return Bite( cut, leg ).Breaking( breaking );\n\t\t\t}\n\n\t\t\tyield break;\n\t\t}\n\n\t\tvar going = cut.StepGoing > 0.5f ? cut.StepGoing : kit.StepGoing;\n\t\tvar rise = cut.StepRise > 0.5f ? cut.StepRise : kit.StepRise;\n\n\t\tforeach ( var leg in legs.Where( leg => !leg.HasLoop ) )\n\t\t{\n\t\t\t// The hole starts on the leg's own band, so it agrees with the walls round it.\n\t\t\tvar climbed = leg.BaseHeight;\n\t\t\tvar steps = Math.Max( 1, (int)MathF.Round( leg.Length / MathF.Max( 1f, going ) ) );\n\t\t\tvar tread = leg.Length / steps;\n\t\t\tvar axes = leg.Axes;\n\t\t\tvar half = MathF.Max( 1f, leg.Width ) * 0.5f;\n\n\t\t\tfor ( var step = 0; step < steps; step++ )\n\t\t\t{\n\t\t\t\tvar footprint = axes.Rect( step * tread, (step + 1) * tread, -half, half );\n\n\t\t\t\tclimbed += rise;\n\n\t\t\t\tyield return ArchCarveVolume.Over( footprint, climbed, leg.TopHeight ).Breaking( breaking );\n\t\t\t}\n\t\t}\n\t}\n\n\t// A box, or the wedge a ramped cut takes: its floor is the surface it leaves behind, so the head of the band\n\t// loses nothing and the foot loses all of it. Through ArchRamp, so the void the ghost draws is the void the\n\t// carve takes and no second slope is derived anywhere.\n\tpublic static ArchCarveVolume Bite( ArchCutPart cut, ArchCutSegment leg )\n\t{\n\t\treturn ArchRamp.Rakes( cut )\n\t\t\t? ArchCarveVolume.Above( leg.Outline(), Floor( cut, leg ), leg.TopHeight )\n\t\t\t: ArchCarveVolume.Over( leg.Outline(), leg.BaseHeight, leg.TopHeight );\n\t}\n\n\t// The plane the bite leaves standing under it - read by the generator, the ghost, the handles and the section.\n\tpublic static ArchCarvePlane Floor( ArchCutPart cut, ArchCutSegment leg )\n\t{\n\t\treturn ArchRamp.Rakes( cut )\n\t\t\t? ArchRamp.Deck( cut, leg.Outline(), leg.TopHeight, leg.TopHeight - leg.BaseHeight )\n\t\t\t: ArchCarvePlane.Level( leg.BaseHeight );\n\t}\n\n\t// The low end of a ramped bite, the way ArchRamp.Foot answers for a platform: the height the foot handle\n\t// stands at, and the one the fall is dragged by.\n\tpublic static float Toe( ArchCutPart cut, ArchCutSegment leg )\n\t{\n\t\treturn leg.TopHeight - ArchRamp.Fall( cut, leg.TopHeight - leg.BaseHeight );\n\t}\n\n\t// Seeded from the cut's own id: a ruin that reshuffled itself on every hotload could not be reviewed, and\n\t// every emitted vertex is folded into the incremental build's key.\n\tstatic ArchCarveBreak Breaking( ArchCutPart cut, ArchKit kit )\n\t{\n\t\treturn cut.BreakEdges ? new ArchCarveBreak { Seed = cut.Id, Jitter = kit?.BreakJitter ?? 0f } : default;\n\t}\n\n\t// Only legs whose band reaches this slab open it - the storey between stays whole.\n\t// Derived, never stored, as ArchFloorCutout - its arbitrary Loop already carries angled holes.\n\t// World-space: one shaft opens every slab it overlaps in every building it is allowed to reach.\n\tpublic static bool Affects( ArchCutPart cut, ArchCutAffects target ) => (cut.Affects & target) == target;\n\n\t// The wells a storey loses because the layer that pierced them is still enabled. Slabs, ceilings,\n\t// foundations and roofs all gather this the same way, so disabling a flight fills its stairwell back\n\t// in without the plan losing the well it would restore.\n\tpublic static IEnumerable<ArchFloorCutout> Stored( ArchBuilding building, int level )\n\t{\n\t\treturn building.Cutouts.Where( cutout => cutout.Level == level && ArchLayerGate.Owned( cutout.OwnerId ) );\n\t}\n\n\tpublic static IEnumerable<ArchFloorCutout> Holes(\n\t\tArchPlan plan,\n\t\tint level,\n\t\tArchKit kit,\n\t\tfloat bottom,\n\t\tfloat top,\n\t\tint hostId,\n\t\tArchCutAffects target = ArchCutAffects.Floors,\n\t\tint standing = 0 )\n\t{\n\t\tforeach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, target, standing ) )\n\t\t{\n\t\t\tvar hole = new ArchFloorCutout { Level = level, Name = cut.Name, Break = volume.Break };\n\n\t\t\thole.Reshape( volume.Footprint );\n\n\t\t\tyield return hole;\n\t\t}\n\t}\n\n\t// The loops a cut opened RIGHT THROUGH a slab and says it wants guarded. An edge a boolean left in a floor is\n\t// a drop exactly as the edge of a stairwell is, so the ring round the well walks these beside its own and every\n\t// rule it already keeps - the mouth left open, an edge against a wall left bare, the flight's own rail owning\n\t// what it reaches - applies to them unchanged.\n\tpublic static IEnumerable<List<Vector2>> Guarded( ArchPlan plan, int level, ArchKit kit, float bottom, float top, int hostId )\n\t{\n\t\tvar low = MathF.Min( bottom, top );\n\t\tvar high = MathF.Max( bottom, top );\n\n\t\tforeach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, ArchCutAffects.Floors ) )\n\t\t{\n\t\t\tif ( cut.GuardsOpenedEdges && Pierces( volume, low, high ) )\n\t\t\t{\n\t\t\t\tyield return volume.Footprint.ToList();\n\t\t\t}\n\t\t}\n\t}\n\n\t// The same shafts a slab loses, handed to a solid that carves in three dimensions rather than in plan.\n\t// A roof asks through here so the deck and the ceiling under it read one list.\n\tpublic static IEnumerable<ArchCarveVolume> Volumes(\n\t\tArchPlan plan,\n\t\tint level,\n\t\tArchKit kit,\n\t\tfloat bottom,\n\t\tfloat top,\n\t\tint hostId,\n\t\tArchCutAffects target,\n\t\tint standing = 0 )\n\t{\n\t\treturn Reaching( plan, level, kit, bottom, top, hostId, target, standing ).Select( found => found.Volume );\n\t}\n\n\tpublic static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Damage(\n\t\tArchPlan plan,\n\t\tint level,\n\t\tArchKit kit,\n\t\tfloat bottom,\n\t\tfloat top,\n\t\tint hostId,\n\t\tArchCutAffects target,\n\t\tint standing = 0 )\n\t{\n\t\treturn Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Damage );\n\t}\n\n\t// The zones asking their hosts to stand work PROUD rather than to break or to be carved. Same walk, same\n\t// reach and same order-of-operations answer - only the mode differs.\n\tpublic static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Extrusions(\n\t\tArchPlan plan,\n\t\tint level,\n\t\tArchKit kit,\n\t\tfloat bottom,\n\t\tfloat top,\n\t\tint hostId,\n\t\tArchCutAffects target,\n\t\tint standing = 0 )\n\t{\n\t\treturn Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Extrude );\n\t}\n\n\tstatic IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Reaching(\n\t\tArchPlan plan,\n\t\tint level,\n\t\tArchKit kit,\n\t\tfloat bottom,\n\t\tfloat top,\n\t\tint hostId,\n\t\tArchCutAffects target,\n\t\tint standing = 0,\n\t\tArchZoneMode mode = ArchZoneMode.Carve )\n\t{\n\t\tif ( plan is null )\n\t\t{\n\t\t\tyield break;\n\t\t}\n\n\t\tvar low = MathF.Min( bottom, top );\n\t\tvar high = MathF.Max( bottom, top );\n\n\t\tforeach ( var cut in Cuts( plan, level, hostId, target, standing, mode ) )\n\t\t{\n\t\t\tforeach ( var volume in Resolve( cut, kit ) )\n\t\t\t{\n\t\t\t\tif ( Reaches( volume, low, high ) )\n\t\t\t\t{\n\t\t\t\t\tyield return (cut, volume);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// A cut opens the slabs of whatever its group holds, and of every building when it stands in none.\n\t// A disabled one is still authored and still selectable - it just stops being read.\n\t//\n\t// It also only reaches what was STANDING when it was made: order of operations is not a boolean feature,\n\t// it is how the whole stack evaluates, so the one answer to that lives in ArchLayerOrder and is asked\n\t// here - the single place a cut chooses what it may touch.\n\tstatic IEnumerable<ArchCutPart> Cuts( ArchPlan plan, int level, int hostId, ArchCutAffects target, int standing = 0, ArchZoneMode mode = ArchZoneMode.Carve )\n\t{\n\t\tforeach ( var building in plan.Buildings )\n\t\t{\n\t\t\tforeach ( var cut in Filed( plan, building.Cuts, building.Id, level, hostId, target, standing, mode ) )\n\t\t\t{\n\t\t\t\tyield return cut;\n\t\t\t}\n\t\t}\n\n\t\t// A road holds its own, so a bore and a carriageway are opened by the algebra a slab is.\n\t\tforeach ( var road in plan.Roads() )\n\t\t{\n\t\t\tforeach ( var cut in Filed( plan, road.Cuts, road.Id, level, hostId, target, standing, mode ) )\n\t\t\t{\n\t\t\t\tyield return cut;\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic IEnumerable<ArchCutPart> Filed(\n\t\tArchPlan plan,\n\t\tIReadOnlyList<ArchCutPart> cuts,\n\t\tint ownerId,\n\t\tint level,\n\t\tint hostId,\n\t\tArchCutAffects target,\n\t\tint standing,\n\t\tArchZoneMode mode )\n\t{\n\t\tforeach ( var cut in cuts )\n\t\t{\n\t\t\tif ( cut.Mode != mode || cut.Level > level || !cut.HasContent || !ArchLayerGate.On( cut ) || !Affects( cut, target ) )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( !ArchLayerOrder.Applies( plan, cut.Id, standing ) )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar reach = ArchLayerGroups.Reach( plan, cut.Id, ownerId );\n\n\t\t\tif ( hostId == 0 || reach is null || reach.Contains( hostId ) )\n\t\t\t{\n\t\t\t\tyield return cut;\n\t\t\t}\n\t\t}\n\t}\n\n\t// At the corners: a raked band reaches one end of a pitched deck long before the other.\n\tpublic static bool Reaches( ArchCarveVolume volume, float low, float high )\n\t{\n\t\tvar floor = float.MaxValue;\n\t\tvar ceiling = float.MinValue;\n\n\t\tforeach ( var corner in volume.Footprint )\n\t\t{\n\t\t\tfloor = MathF.Min( floor, volume.Floor.At( corner ) );\n\t\t\tceiling = MathF.Max( ceiling, volume.Ceiling.At( corner ) );\n\t\t}\n\n\t\treturn ceiling > low + ArchCarve.Grain && floor < high - ArchCarve.Grain;\n\t}\n\n\t// A volume spanning the whole band it reaches takes that body out entirely - a well, and a well is\n\t// what gets an edge dressed. One that stops inside leaves material over or under it, which is a\n\t// recess, and a recess is trimmed by nothing: its faces ARE the body it was taken out of.\n\tpublic static bool Pierces( ArchCarveVolume volume, float low, float high )\n\t{\n\t\tvar floor = float.MinValue;\n\t\tvar ceiling = float.MaxValue;\n\n\t\tforeach ( var corner in volume.Footprint )\n\t\t{\n\t\t\tfloor = MathF.Max( floor, volume.Floor.At( corner ) );\n\t\t\tceiling = MathF.Min( ceiling, volume.Ceiling.At( corner ) );\n\t\t}\n\n\t\treturn floor <= low + ArchCarve.Grain && ceiling >= high - ArchCarve.Grain;\n\t}\n\n\tpublic static IEnumerable<(float From, float To)> Outside(\n\t\tArchPlan plan,\n\t\tArchKit kit,\n\t\tint level,\n\t\tint hostId,\n\t\tVector2 from,\n\t\tVector2 to,\n\t\tfloat bottom,\n\t\tfloat top,\n\t\tArchCutAffects target = ArchCutAffects.WallFittings,\n\t\tint standing = 0 )\n\t{\n\t\tvar blocked = new List<(float From, float To)>();\n\n\t\tif ( plan is not null && (to - from).Length > ArchCarve.Grain )\n\t\t{\n\t\t\tforeach ( var cut in Cuts( plan, level, hostId, target, standing ) )\n\t\t\t{\n\t\t\t\tforeach ( var volume in Resolve( cut, kit ).Where( volume => Reaches( volume, bottom, top ) ) )\n\t\t\t\t{\n\t\t\t\t\tblocked.AddRange( ArchFootprint.Inside( volume.Footprint, from, to ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar marks = new List<float> { 0f, 1f };\n\n\t\tforeach ( var range in blocked )\n\t\t{\n\t\t\tmarks.Add( Math.Clamp( range.From, 0f, 1f ) );\n\t\t\tmarks.Add( Math.Clamp( range.To, 0f, 1f ) );\n\t\t}\n\n\t\tmarks = marks.Distinct().OrderBy( value => value ).ToList();\n\n\t\tfor ( var index = 0; index + 1 < marks.Count; index++ )\n\t\t{\n\t\t\tvar start = marks[index];\n\t\t\tvar finish = marks[index + 1];\n\t\t\tvar middle = (start + finish) * 0.5f;\n\n\t\t\tif ( finish - start > 0.001f && !blocked.Any( range => middle > range.From && middle < range.To ) )\n\t\t\t{\n\t\t\t\tyield return (start, finish);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic static IEnumerable<List<Vector3>> OutsidePath(\n\t\tArchPlan plan,\n\t\tArchKit kit,\n\t\tint level,\n\t\tint hostId,\n\t\tIReadOnlyList<Vector3> path,\n\t\tArchCutAffects target )\n\t{\n\t\tvar runs = new List<List<Vector3>>();\n\n\t\tfor ( var index = 0; index + 1 < (path?.Count ?? 0); index++ )\n\t\t{\n\t\t\tvar from = path[index];\n\t\t\tvar to = path[index + 1];\n\t\t\tvar flatFrom = new Vector2( from.x, from.y );\n\t\t\tvar flatTo = new Vector2( to.x, to.y );\n\n\t\t\tvar spans = Outside( plan, kit, level, hostId, flatFrom, flatTo, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), target ).ToList();\n\n\t\t\tforeach ( var span in spans )\n\t\t\t{\n\t\t\t\tvar start = Vector3.Lerp( from, to, span.From );\n\t\t\t\tvar finish = Vector3.Lerp( from, to, span.To );\n\t\t\t\tvar current = runs.LastOrDefault();\n\n\t\t\t\tif ( current is null || (current[^1] - start).Length > 0.05f )\n\t\t\t\t{\n\t\t\t\t\tcurrent = new List<Vector3> { start };\n\t\t\t\t\truns.Add( current );\n\t\t\t\t}\n\n\t\t\t\tif ( (current[^1] - finish).Length > 0.01f )\n\t\t\t\t{\n\t\t\t\t\tcurrent.Add( finish );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( spans.Count == 0 || spans.Any( span => span.From > 0.001f || span.To < 0.999f ) )\n\t\t\t{\n\t\t\t\truns.Add( null );\n\t\t\t}\n\t\t}\n\n\t\treturn runs.Where( run => run is { Count: >= 2 } );\n\t}\n\n\t// The one cut of a LEVEL run: a ring is closed before it is cut so its seam edge can break like any\n\t// other, then each surviving stretch is normalized (closure detected, duplicated closing point\n\t// dropped) and handed back as a run. Gutters, fascia, soffits, parapets and closures all read this\n\t// answer, so a hole reads the same to every band that crosses it.\n\tpublic static IEnumerable<ArchRunPath> Runs(\n\t\tArchPlan plan,\n\t\tArchKit kit,\n\t\tint level,\n\t\tint hostId,\n\t\tArchRunPath run,\n\t\tArchCutAffects target )\n\t{\n\t\tvar path = run.Raised();\n\n\t\tif ( run.Closed && path.Count >= 3 )\n\t\t{\n\t\t\tpath.Add( path[0] );\n\t\t}\n\n\t\tforeach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) )\n\t\t{\n\t\t\tyield return ArchRunPath.Normalized( surviving, run.Height );\n\t\t}\n\t}\n\n\t// The 3D form of the same cut: a trim climbs its host's jambs, so its surviving stretches keep\n\t// their own per-point heights and only the closure is answered here. The author closes the ring\n\t// when it is one - ArchTrimFollow.Walked does - so no pre-close is asked for.\n\tpublic static IEnumerable<(List<Vector3> Points, bool Closed)> Surviving(\n\t\tArchPlan plan,\n\t\tArchKit kit,\n\t\tint level,\n\t\tint hostId,\n\t\tIReadOnlyList<Vector3> path,\n\t\tArchCutAffects target )\n\t{\n\t\tforeach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) )\n\t\t{\n\t\t\tvar closed = ArchRunPath.IsClosed( surviving );\n\n\t\t\tyield return (closed ? surviving.Take( surviving.Count - 1 ).ToList() : surviving, closed);\n\t\t}\n\t}\n\n\t// Is one POINT inside a cut - the question a fitting too small to have a run of its own asks, like the\n\t// block that takes a ridge's corner. Its band is the fitting's own, so a cut passing under it leaves it.\n\tpublic static bool Covers( ArchPlan plan, ArchKit kit, int level, int hostId, Vector3 from, Vector3 to, ArchCutAffects target )\n\t{\n\t\tvar at = new Vector2( (from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f );\n\n\t\treturn Volumes( plan, level, kit, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), hostId, target )\n\t\t\t.Any( volume => volume.Covers( at ) );\n\t}\n\n\t// Asked by the solid's own generator, so nothing is told in advance it will be cut.\n\tpublic static IEnumerable<ArchCutPart> Over(\n\t\tArchPlan plan,\n\t\tint level,\n\t\tIReadOnlyList<Vector2> outline,\n\t\tint hostId,\n\t\tArchCutAffects target = ArchCutAffects.Platforms,\n\t\tint standing = 0 )\n\t{\n\t\tif ( plan is null || outline is not { Count: >= 3 } )\n\t\t{\n\t\t\tyield break;\n\t\t}\n\n\t\tforeach ( var cut in Cuts( plan, level, hostId, target, standing ) )\n\t\t{\n\t\t\tif ( cut.Outlines().Any( loop => ArchFootprint.Overlaps( loop, outline ) ) )\n\t\t\t{\n\t\t\t\tyield return cut;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Each leg continues the last, lifted by the chain's Rise - one part can wind round a tower.\n\t// The first leg has nothing to continue from - it takes the band it was given.\n\tpublic static ArchCutSegment Extend( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight )\n\t{\n\t\tvar segment = Next( cut, from, to, width, baseHeight, topHeight );\n\n\t\tif ( segment is null )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\tcut.Segments.Add( segment );\n\n\t\treturn segment;\n\t}\n\n\t// Ghost and commit both go through here - the leg you are shown is the leg you get.\n\tpublic static ArchCutSegment Next( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool? snapAngle = null )\n\t{\n\t\tvar last = cut?.Segments.Count > 0 ? cut.Segments[^1] : null;\n\t\tvar lift = last is null ? 0f : cut.Rise;\n\n\t\treturn Sketch(\n\t\t\tlast?.End ?? from,\n\t\t\tto,\n\t\t\twidth,\n\t\t\t(last?.BaseHeight ?? baseHeight) + lift,\n\t\t\t(last?.TopHeight ?? topHeight) + lift,\n\t\t\tsnapAngle ?? cut?.SnapAngle ?? true );\n\t}\n\n\t// A LOOP leg is one shape described twice - the loop the carve takes, and the run the handles, the ghost and\n\t// the carve frame read off Start/End. So the run is DERIVED from the loop: centred on it, along the yaw it\n\t// already stands at, measured to its own extremes. Author the two separately and every drag drifts them a\n\t// rounding further apart, until the widget, the preview and the geometry are three different shapes.\n\tpublic static void Fit( ArchCutSegment segment, float yaw )\n\t{\n\t\tif ( segment is null || !segment.HasLoop )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar axes = new ArchStairAxes { Yaw = yaw };\n\t\tvar along = axes.Along;\n\t\tvar centre = segment.Loop.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / segment.Loop.Count;\n\t\tvar reach = segment.Loop.Select( point => Vector2.Dot( point - centre, along ) ).ToList();\n\n\t\tsegment.Start = centre + along * reach.Min();\n\t\tsegment.End = centre + along * reach.Max();\n\t}\n\n\t// Rigid: the loop and the run travel together and neither is re-derived, so a move cannot turn or stretch it.\n\tpublic static void Shift( ArchCutSegment segment, Vector2 by )\n\t{\n\t\tif ( segment is null || by.Length < ArchGridService.FinestSize )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tsegment.Start += by;\n\t\tsegment.End += by;\n\n\t\tif ( segment.HasLoop )\n\t\t{\n\t\t\tsegment.Loop = segment.Loop.Select( point => point + by ).ToList();\n\t\t}\n\t}\n\n\t// The loop swings and the run is refitted to where it landed, so the yaw the frame reads is the yaw the\n\t// shape actually stands at. The SWING is what lands on the ladder, not the angle it arrives at: rounding the\n\t// total dragged a shape drawn at seven degrees onto the nearest fifteen the moment the ring was touched, and\n\t// an edit is supposed to leave where a shape already stands alone. The handle has already ratcheted, so this\n\t// is idempotent for a stepped drag and only guards a caller that has not.\n\tpublic static void Turn( ArchCutSegment segment, Vector2 about, float degrees, bool snapAngle )\n\t{\n\t\tif ( segment is null )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar swing = snapAngle ? ArchGridService.Snap( degrees, ArchGridService.AngleStep ) : degrees;\n\t\tvar wanted = segment.Yaw + swing;\n\n\t\tif ( MathF.Abs( swing ) < 0.001f )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tif ( segment.HasLoop )\n\t\t{\n\t\t\tsegment.Loop = ArchFootprint.Turned( segment.Loop, about, swing );\n\t\t\tFit( segment, wanted );\n\n\t\t\treturn;\n\t\t}\n\n\t\tvar ends = ArchFootprint.Turned( new[] { segment.Start, segment.End }, about, swing );\n\n\t\tsegment.Start = ends[0];\n\t\tsegment.End = ends[1];\n\t}\n\n\t// Redrawn through Sketch, so a handled leg is one the tool would have let you draw.\n\tpublic static bool Reshape( ArchCutSegment segment, Vector2 from, Vector2 to, bool snapAngle = true )\n\t{\n\t\t// A loop leg's run is the loop's, not the drag's - re-sketching it here is what let the two disagree.\n\t\tif ( segment is { HasLoop: true } )\n\t\t{\n\t\t\tShift( segment, from - segment.Start );\n\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( segment is null || Sketch( from, to, segment.Width, segment.BaseHeight, segment.TopHeight, snapAngle ) is not { } redrawn )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tsegment.Start = redrawn.Start;\n\t\tsegment.End = redrawn.End;\n\n\t\treturn true;\n\t}\n\n\t// The chain invariant - each leg keeps its own vector, so this slides the chain after edits.\n\tpublic static void Relink( ArchCutPart cut )\n\t{\n\t\tif ( cut is null )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tfor ( var index = 1; index < cut.Segments.Count; index++ )\n\t\t{\n\t\t\tvar previous = cut.Segments[index - 1];\n\t\t\tvar leg = cut.Segments[index];\n\n\t\t\tReshape( leg, previous.End, previous.End + (leg.End - leg.Start), cut.SnapAngle );\n\t\t}\n\t}\n\n\tpublic static ArchCutSegment Sketch( Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool snapAngle = true )\n\t{\n\t\tvar span = to - from;\n\n\t\tif ( span.Length < MinRun )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\t// Whole degrees stay clean; 15 degree steps are the ladder unless the part says otherwise.\n\t\tvar yaw = ArchGridService.Snap( MathF.Atan2( span.y, span.x ).RadianToDegree(), snapAngle ? ArchGridService.AngleStep : 1f ).DegreeToRadian();\n\t\tvar along = new Vector2( MathF.Cos( yaw ), MathF.Sin( yaw ) );\n\n\t\treturn new ArchCutSegment\n\t\t{\n\t\t\tStart = from,\n\t\t\tEnd = from + along * ArchGridService.Fine( Vector2.Dot( span, along ) ),\n\t\t\tWidth = MathF.Max( 1f, width ),\n\t\t\tBaseHeight = MathF.Min( baseHeight, topHeight ),\n\t\t\tTopHeight = MathF.Max( baseHeight, topHeight )\n\t\t};\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Bool/ArchBoolUi.cs",
            "FileName": "ArchBoolUi.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing Editor;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// Read by the Boolean Modifiers subtool and by Select, off the same records - a draft and a placed shape\n// carry identical controls because they ARE the same records. Neither a platform nor a cut had a property\n// sheet before this: selecting one showed an empty shelf.\npublic static class ArchBoolUi\n{\n\tstatic bool AffectsExpanded\n\t{\n\t\tget => EditorCookie.Get( \"arch.bool.affects.expanded\", false );\n\t\tset => EditorCookie.Set( \"arch.bool.affects.expanded\", value );\n\t}\n\n\t// A tick that GATES the fields under it has to rebuild the sheet as well as commit, or the fields it reveals\n\t// never arrive and the ones it hides stay put. Handing these only `changed` is why ticking the edge dressing\n\t// did nothing you could see, and why the coping numbers could not be reached at all.\n\tstatic Action Both( Action changed, Action refresh )\n\t{\n\t\treturn () =>\n\t\t{\n\t\t\tchanged?.Invoke();\n\t\t\trefresh?.Invoke();\n\t\t};\n\t}\n\n\tpublic static void Platform( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed )\n\t{\n\t\tvar slab = panel.AddGroup( \"Solid\" );\n\n\t\tslab.Add( ArchPartUi.Number( platform.Ramp ? \"Head\" : \"Rise\", platform.Rise, 24f,\n\t\t\tvalue => platform.TopHeight = platform.GradeHeight + MathF.Max( 4f, value ), changed ) );\n\n\t\tslab.Add( ArchPartUi.Check( \"Ramp the deck\", platform.Ramp, value => platform.Ramp = value, Both( changed, refresh ) ) );\n\n\t\tif ( platform.Ramp )\n\t\t{\n\t\t\tRamp( panel, platform, \"rise\", changed );\n\n\t\t\treturn;\n\t\t}\n\n\t\tGuard( panel, platform, refresh, changed );\n\t\tCoping( panel, platform, refresh, changed );\n\t}\n\n\t// The two groups the placement tool asks for as well, where the rise is the seed's own and the rake is the\n\t// kind already chosen - so it reads these rather than the whole sheet, and a change lands in one place.\n\tpublic static void Guard( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed )\n\t{\n\t\tvar guard = panel.AddGroup( \"Guardrail\" );\n\n\t\tguard.Add( ArchPartUi.Check( \"Guard rail round the edge\", platform.Guardrail, value => platform.Guardrail = value, Both( changed, refresh ) ) );\n\n\t\tif ( !platform.Guardrail )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tguard.Add( ArchPartUi.Number( \"Height\", platform.GuardHeight, 42f,\n\t\t\tvalue => platform.GuardHeight = MathF.Max( 12f, value ), changed ) );\n\n\t\tArchBarrierUi.GuardStyles( guard, platform.GuardStyle, style =>\n\t\t{\n\t\t\tplatform.GuardStyle = style;\n\t\t\tchanged?.Invoke();\n\t\t} );\n\t}\n\n\tpublic static void Coping( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed )\n\t{\n\t\tvar coping = panel.AddGroup( \"Coping\" );\n\n\t\tcoping.Add( ArchPartUi.Check( \"Coping band\", platform.Coping, value => platform.Coping = value, Both( changed, refresh ) ) );\n\n\t\tif ( !platform.Coping )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tcoping.Add( ArchPartUi.Number( \"Height\", platform.CopingHeight, 4f, value => platform.CopingHeight = value, changed ) );\n\t\tcoping.Add( ArchPartUi.Number( \"Width\", platform.CopingWidth, 8f, value => platform.CopingWidth = value, changed ) );\n\t\tcoping.Add( ArchPartUi.Number( \"Oversail\", platform.CopingOversail, 1.5f, value => platform.CopingOversail = value, changed ) );\n\t\tcoping.Add( ArchPartUi.Check( \"Round the inside edges too\", platform.CopingInside, value => platform.CopingInside = value, changed ) );\n\t}\n\n\t// Which way it climbs and how far it drops getting to its foot. The gradient is never authored - it falls\n\t// out of the run the footprint has - so a ramp dragged longer is a gentler one with no second edit. Shared\n\t// by the platform that rakes its top and the cut that rakes the floor it leaves: same three numbers, one form.\n\t// A coping is not offered: a run is level, and a raked deck has no one height to walk a band round.\n\tpublic static void Ramp( ToolSidebarWidget panel, IArchRamped part, string shelf, Action changed )\n\t{\n\t\tvar ramp = panel.AddGroup( \"Ramp\" );\n\n\t\tramp.Add( ArchPartUi.Number( \"Climb angle\", part.RampYaw, 0f,\n\t\t\tvalue => part.RampYaw = ArchShapeHandles.Facing( value ), changed ) );\n\n\t\tramp.Add( ArchPartUi.Number( \"Fall\", part.RampFall, 0f,\n\t\t\tvalue => part.RampFall = MathF.Max( 0f, value ), changed ) );\n\n\t\tramp.Add( ArchPartUi.Wrapped( $\"Zero fall is the whole {shelf}, so the foot meets the bottom of the band. Type one to leave a level shelf.\" ) );\n\t}\n\n\t// One member or a field of them, and the field's own numbers. The drop is measured DOWN from the plane\n\t// it hangs on, so a deeper beam grows into the room rather than up through the ceiling.\n\tpublic static void Beam( ToolSidebarWidget panel, ArchBeamPart beam, Action refresh, Action changed )\n\t{\n\t\tusing ( var grid = ArchIconGrid.In( panel.AddGroup( \"Members\" ) ) )\n\t\t{\n\t\t\tgrid.Pick( \"One member \u2014 exactly the shape that was dragged\", \"beam_one\", \"horizontal_rule\", beam.Members == BeamMembers.One,\n\t\t\t\t() => { beam.Members = BeamMembers.One; changed?.Invoke(); refresh?.Invoke(); } );\n\n\t\t\tgrid.Pick( \"Filled \u2014 that same shape repeated as slats\", \"beam_field\", \"view_stream\", beam.Members == BeamMembers.Field,\n\t\t\t\t() => { beam.Members = BeamMembers.Field; changed?.Invoke(); refresh?.Invoke(); } );\n\t\t}\n\n\t\tSection( panel, beam, beam.Members == BeamMembers.Field, changed );\n\t}\n\n\t// Read by the placement tool too, where the Members picker would only repeat the kind already chosen.\n\tpublic static void Section( ToolSidebarWidget panel, ArchBeamPart beam, bool field, Action changed )\n\t{\n\t\tvar section = panel.AddGroup( \"Section\" );\n\n\t\tsection.Add( ArchPartUi.Number( \"Drop\", beam.Drop, 12f, value => beam.Drop = MathF.Max( 1f, value ), changed ) );\n\n\t\tif ( !field )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tsection.Add( ArchPartUi.Number( \"Member width\", beam.MemberWidth, 6f, value => beam.MemberWidth = MathF.Max( 1f, value ), changed ) );\n\t\tsection.Add( ArchPartUi.Number( \"Gap\", beam.MemberGap, 10f, value => beam.MemberGap = MathF.Max( 0f, value ), changed ) );\n\t\tsection.Add( ArchPartUi.Number( \"Angle\", beam.Yaw, 0f, value => beam.Yaw = value, changed ) );\n\t\tsection.Add( ArchPartUi.Integer( \"Sides\", beam.Sides, 0, value => beam.Sides = Math.Max( 0, value ), changed ) );\n\t}\n\n\tpublic static void Damage( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed )\n\t{\n\t\tusing ( var grid = ArchIconGrid.In( panel.AddGroup( \"Damage type\" ) ) )\n\t\t{\n\t\t\tDamage( grid, cut, ArchDamageKind.Masonry, \"Masonry \u2014 staggered exposed bricks at wall and pillar corners\", \"damage_masonry\", \"view_module\", refresh, changed );\n\t\t\tDamage( grid, cut, ArchDamageKind.SurfaceSpall, \"Surface spall \u2014 a shallow chipped region\", \"damage_spall\", \"texture\", refresh, changed );\n\t\t\tDamage( grid, cut, ArchDamageKind.MissingPanels, \"Missing panels \u2014 selected ceiling cells are absent\", \"damage_missing_panels\", \"grid_off\", refresh, changed );\n\t\t\tDamage( grid, cut, ArchDamageKind.DisplacedPanels, \"Displaced panels \u2014 selected ceiling cells hang dropped and tilted\", \"damage_displaced_panels\", \"view_quilt\", refresh, changed );\n\t\t\tDamage( grid, cut, ArchDamageKind.MixedPanels, \"Mixed panels \u2014 selected ceiling cells are missing or displaced\", \"damage_mixed_panels\", \"dashboard\", refresh, changed );\n\t\t}\n\n\t\tvar pattern = panel.AddGroup( \"Pattern\" );\n\n\t\tif ( cut.ResolvedDamage == ArchDamageKind.Masonry )\n\t\t{\n\t\t\tMasonryPresets( pattern, cut, refresh, changed );\n\n\t\t\tArchSidebarLayout.Form( pattern, nested =>\n\t\t\t\tArchSidebarSection.Disclosure( nested, \"Damage.masonry.advanced\", \"Advanced\", true, advanced =>\n\t\t\t\t{\n\t\t\t\t\tadvanced.Add( ArchPartUi.Number( \"Course coverage\", cut.DamageAmount, 1f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );\n\t\t\t\t\tadvanced.Add( ArchPartUi.Number( \"Brick width\", cut.DamageCellWidth, 16f, value => cut.DamageCellWidth = MathF.Max( 2f, value ), changed ) );\n\t\t\t\t\tadvanced.Add( ArchPartUi.Number( \"Course height\", cut.DamageCellLength, 8f, value => cut.DamageCellLength = MathF.Max( 2f, value ), changed ) );\n\t\t\t\t\tadvanced.Add( ArchPartUi.Number( \"Reveal depth\", cut.DamageDepth, 6f, value => cut.DamageDepth = MathF.Max( 0.25f, value ), changed ) );\n\t\t\t\t\tadvanced.Add( ArchPartUi.Check( \"Carve wall finish\", cut.MasonryCarves, value => cut.MasonryCarves = value, changed ) );\n\t\t\t\t} ) );\n\t\t}\n\t\telse if ( ArchDamage.Panels( cut.ResolvedDamage ) )\n\t\t{\n\t\t\tpattern.Add( ArchPartUi.Number( \"Amount\", cut.DamageAmount, 0.55f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );\n\t\t\tpattern.Add( ArchPartUi.Number( \"Panel width\", cut.DamageCellWidth, 24f, value => cut.DamageCellWidth = MathF.Max( 4f, value ), changed ) );\n\t\t\tpattern.Add( ArchPartUi.Number( \"Panel length\", cut.DamageCellLength, 48f, value => cut.DamageCellLength = MathF.Max( 4f, value ), changed ) );\n\n\t\t\tif ( cut.ResolvedDamage is ArchDamageKind.DisplacedPanels or ArchDamageKind.MixedPanels )\n\t\t\t{\n\t\t\t\tpattern.Add( ArchPartUi.Number( \"Drop\", cut.DamageDrop, 4f, value => cut.DamageDrop = MathF.Max( 0f, value ), changed ) );\n\t\t\t\tpattern.Add( ArchPartUi.Number( \"Tilt\", cut.DamageTilt, 12f, value => cut.DamageTilt = MathF.Max( 0f, value ), changed ) );\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tpattern.Add( ArchPartUi.Number( \"Amount\", cut.DamageAmount, 0.55f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );\n\t\t\tpattern.Add( ArchPartUi.Number( \"Damage depth\", cut.DamageDepth, 4f, value => cut.DamageDepth = MathF.Max( 0.5f, value ), changed ) );\n\t\t}\n\n\t\tAffects( panel, cut, refresh, changed, ArchDamage.Targets( cut.ResolvedDamage ) );\n\t}\n\n\t// The proud sibling of the damage sheet. There is no Affects group: an extrude zone stands work on a wall face\n\t// and a wall face is the only host that can answer it, so offering the mask would be offering a way to break it.\n\t// The kind row is the caller's where the caller already asked for it: the placement tool answers it in its own\n\t// step, and two grids of one choice light independently.\n\tpublic static void Extrude( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed, bool kinds = true )\n\t{\n\t\tif ( kinds )\n\t\t{\n\t\t\tusing var grid = ArchIconGrid.In( panel.AddGroup( \"Extrude\" ) );\n\n\t\t\tExtrude( grid, cut, ArchExtrudeKind.Face, refresh, changed );\n\t\t\tExtrude( grid, cut, ArchExtrudeKind.Quoins, refresh, changed );\n\t\t\tExtrude( grid, cut, ArchExtrudeKind.Courses, refresh, changed );\n\t\t\tExtrude( grid, cut, ArchExtrudeKind.Indent, refresh, changed );\n\t\t\tExtrude( grid, cut, ArchExtrudeKind.Pier, refresh, changed );\n\t\t}\n\n\t\tvar recessing = cut.Extrude == ArchExtrudeKind.Indent;\n\t\tvar standing = ArchExtrude.Stands( cut.Extrude );\n\t\tvar block = panel.AddGroup( recessing ? \"Recess\" : standing ? \"Pier\" : \"Block\" );\n\n\t\tblock.Add( ArchPartUi.Number( recessing ? \"Depth\" : \"Proud\", cut.ExtrudeDepth, 4f,\n\t\t\tvalue => cut.ExtrudeDepth = MathF.Max( ArchExtrude.LeastDepth, value ), changed ) );\n\n\t\t// A recess has no far side to offer: on both faces it would be a hole, which is Subtract's job.\n\t\tif ( !recessing )\n\t\t{\n\t\t\tblock.Add( ArchPartUi.Check( \"Both faces\", cut.ExtrudeBothFaces, value => cut.ExtrudeBothFaces = value, changed ) );\n\t\t}\n\n\t\tif ( standing )\n\t\t{\n\t\t\tusing ( var founding = ArchIconGrid.In( panel.AddGroup( \"Footing\" ) ) )\n\t\t\t{\n\t\t\t\tFooting( founding, cut, null, \"Type's own \u2014 whatever the pillar type authored\", \"footing_type\", \"block\", refresh, changed );\n\t\t\t\tFooting( founding, cut, PillarFooting.None, \"None \u2014 the shaft runs straight into what it stands on\", \"footing_none\", \"remove\", refresh, changed );\n\t\t\t\tFooting( founding, cut, PillarFooting.Square, \"Square \u2014 a plain pad, buried\", \"footing_square\", \"crop_square\", refresh, changed );\n\t\t\t\tFooting( founding, cut, PillarFooting.Bevelled, \"Bevelled \u2014 a splayed pad, buried\", \"footing_bevelled\", \"change_history\", refresh, changed );\n\t\t\t\tFooting( founding, cut, PillarFooting.Stepped, \"Stepped \u2014 two courses stepping out, buried\", \"footing_stepped\", \"stairs\", refresh, changed );\n\t\t\t}\n\n\t\t\t// Everything else about the column - footing, plinth, banded shaft, capital - is the TYPE's, so this is\n\t\t\t// the only other choice the zone makes about it.\n\t\t\tArchAsks.PillarKinds( panel, ArchAsks.PillarTypes(), cut.ExtrudePillarType, type =>\n\t\t\t{\n\t\t\t\tcut.ExtrudePillarType = type.Name;\n\t\t\t\tchanged?.Invoke();\n\t\t\t\trefresh?.Invoke();\n\t\t\t} );\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ArchExtrude.Coursed( cut.Extrude ) )\n\t\t{\n\t\t\tblock.Add( ArchPartUi.Number( \"Course height\", cut.ExtrudeCourse, 16f, value => cut.ExtrudeCourse = MathF.Max( 0f, value ), changed ) );\n\t\t\tblock.Add( ArchPartUi.Number( \"Joint\", cut.ExtrudeJoint, 0f, value => cut.ExtrudeJoint = MathF.Max( 0f, value ), changed ) );\n\t\t}\n\t}\n\n\tstatic void Footing( ArchIconGrid grid, ArchCutPart cut, PillarFooting? founding, string tooltip, string slug, string glyph, Action refresh, Action changed )\n\t{\n\t\tgrid.Pick( tooltip, slug, glyph, cut.ExtrudeFooting == founding, () =>\n\t\t{\n\t\t\tcut.ExtrudeFooting = founding;\n\t\t\tchanged?.Invoke();\n\t\t\trefresh?.Invoke();\n\t\t} );\n\t}\n\n\tstatic void Extrude( ArchIconGrid grid, ArchCutPart cut, ArchExtrudeKind kind, Action refresh, Action changed )\n\t{\n\t\tgrid.Pick( ArchExtrude.Label( kind ), $\"extrude_{kind.ToString().ToLowerInvariant()}\", ArchExtrude.Glyph( kind ), cut.Extrude == kind, () =>\n\t\t{\n\t\t\tcut.Extrude = kind;\n\t\t\tcut.Affects = ArchExtrude.Targets( kind );\n\t\t\tchanged?.Invoke();\n\t\t\trefresh?.Invoke();\n\t\t} );\n\t}\n\n\tstatic void Damage( ArchIconGrid grid, ArchCutPart cut, ArchDamageKind kind, string tooltip, string slug, string glyph, Action refresh, Action changed )\n\t{\n\t\tgrid.Pick( tooltip, slug, glyph, cut.ResolvedDamage == kind, () =>\n\t\t{\n\t\t\tcut.Damage = kind;\n\t\t\tcut.BreakEdges = kind == ArchDamageKind.Masonry;\n\t\t\tcut.Affects = ArchDamage.Targets( kind );\n\t\t\tcut.DamageAmount = kind == ArchDamageKind.Masonry ? 1f : cut.DamageAmount;\n\t\t\tcut.DamageCellWidth = kind == ArchDamageKind.Masonry ? 16f : cut.DamageCellWidth;\n\t\t\tcut.DamageCellLength = kind == ArchDamageKind.Masonry ? 8f : cut.DamageCellLength;\n\t\t\tcut.DamageDepth = kind == ArchDamageKind.Masonry ? 6f : cut.DamageDepth;\n\t\t\tcut.MasonryPattern = kind == ArchDamageKind.Masonry ? ArchMasonryPattern.ExposedEdge : cut.MasonryPattern;\n\t\t\tcut.MasonryCarves = kind == ArchDamageKind.Masonry ? false : cut.MasonryCarves;\n\t\t\tchanged?.Invoke();\n\t\t\trefresh?.Invoke();\n\t\t} );\n\t}\n\n\tstatic void MasonryPresets( Layout pattern, ArchCutPart cut, Action refresh, Action changed )\n\t{\n\t\tusing var presets = ArchIconGrid.In( pattern );\n\n\t\tpresets.Pick( \"Bricks - simple one-brick boxes extruded from an intact corner\", \"masonry_bricks\", \"view_in_ar\",\n\t\t\tMasonryMatches( cut, ArchMasonryPattern.ExposedEdge, 16f, 8f, 6f, false ), () => ApplyMasonry( cut, ArchMasonryPattern.ExposedEdge, 16f, 8f, 6f, false, refresh, changed ) );\n\n\t\tpresets.Pick( \"Edge \u2014 two bricks, a header and intact courses at a broken wall edge\", \"masonry_edge\", \"view_module\",\n\t\t\tMasonryMatches( cut, ArchMasonryPattern.ExposedEdge, 12f, 4f, 1.25f, true ), () => ApplyMasonry( cut, ArchMasonryPattern.ExposedEdge, 12f, 4f, 1.25f, true, refresh, changed ) );\n\t\tpresets.Pick( \"Worn \u2014 a fuller broken bond with fewer intact courses\", \"masonry_worn\", \"texture\",\n\t\t\tMasonryMatches( cut, ArchMasonryPattern.WornCorner, 12f, 4f, 1.75f, true ), () => ApplyMasonry( cut, ArchMasonryPattern.WornCorner, 12f, 4f, 1.75f, true, refresh, changed ) );\n\t\tpresets.Pick( \"Pier \u2014 dense alternating long and header courses around square pillars\", \"masonry_pier\", \"view_column\",\n\t\t\tMasonryMatches( cut, ArchMasonryPattern.BrickPier, 12f, 4f, 0.75f, false ), () => ApplyMasonry( cut, ArchMasonryPattern.BrickPier, 12f, 4f, 0.75f, false, refresh, changed ) );\n\t}\n\n\tstatic void ApplyMasonry( ArchCutPart cut, ArchMasonryPattern pattern, float width, float course, float depth, bool carves, Action refresh, Action changed )\n\t{\n\t\tcut.MasonryPattern = pattern;\n\t\tcut.DamageAmount = 1f;\n\t\tcut.DamageCellWidth = width;\n\t\tcut.DamageCellLength = course;\n\t\tcut.DamageDepth = depth;\n\t\tcut.MasonryCarves = carves;\n\n\t\tchanged?.Invoke();\n\t\trefresh?.Invoke();\n\t}\n\n\tstatic bool MasonryMatches( ArchCutPart cut, ArchMasonryPattern pattern, float width, float course, float depth, bool carves )\n\t{\n\t\treturn cut.MasonryPattern == pattern\n\t\t\t&& cut.MasonryCarves == carves\n\t\t\t&& MathF.Abs( cut.DamageAmount - 1f ) < 0.001f\n\t\t\t&& MathF.Abs( cut.DamageCellWidth - width ) < 0.001f\n\t\t\t&& MathF.Abs( cut.DamageCellLength - course ) < 0.001f\n\t\t\t&& MathF.Abs( cut.DamageDepth - depth ) < 0.001f;\n\t}\n\n\t// A cut that only carves builds nothing, so what it WEARS is the whole form: the walls it carries past\n\t// the solid it started in, and whatever caps it where it comes out.\n\tpublic static void Cut( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed )\n\t{\n\t\tAffects( panel, cut, refresh, changed );\n\n\t\tvar floor = panel.AddGroup( \"Floor\" );\n\n\t\tfloor.Add( ArchPartUi.Check( \"Ramp the floor it leaves\", cut.Ramp, value => cut.Ramp = value, Both( changed, refresh ) ) );\n\n\t\tif ( cut.Ramp )\n\t\t{\n\t\t\tRamp( panel, cut, \"depth\", changed );\n\t\t}\n\n\t\tvar edge = panel.AddGroup( \"Opening edge\" );\n\t\tedge.Add( ArchPartUi.Check( \"Dress the cut edge\", cut.Edge, value => cut.Edge = value, Both( changed, refresh ) ) );\n\n\t\tif ( cut.Edge )\n\t\t{\n\t\t\tedge.Add( ArchPartUi.Number( \"Edge width\", cut.EdgeWidth, 0f,\n\t\t\t\tvalue => cut.EdgeWidth = MathF.Max( 0f, value ), changed ) );\n\t\t}\n\n\t\tedge.Add( ArchPartUi.Check( \"Break the cut edge\", cut.BreakEdges, value => cut.BreakEdges = value, changed ) );\n\t\tedge.Add( ArchPartUi.Check( \"Rail the stair edges it opens\", cut.GuardsOpenedEdges,\n\t\t\tvalue => cut.GuardsOpenedEdges = value, changed ) );\n\n\t\tusing ( var grid = ArchIconGrid.In( panel.AddGroup( \"Enclosure\" ) ) )\n\t\t{\n\t\t\tgrid.Pick( \"Open \u2014 the shaft is the absence in what it passed through\", \"cut_open\", \"crop_free\",\n\t\t\t\tcut.Enclosure == CutEnclosure.None, () => { cut.Enclosure = CutEnclosure.None; changed?.Invoke(); refresh?.Invoke(); } );\n\n\t\t\tgrid.Pick( \"Walled \u2014 it carries its own walls up past the solid it started in\", \"cut_walled\", \"crop_square\",\n\t\t\t\tcut.Enclosure == CutEnclosure.Walls, () => { cut.Enclosure = CutEnclosure.Walls; changed?.Invoke(); refresh?.Invoke(); } );\n\t\t}\n\n\t\tvar walled = cut.Enclosure == CutEnclosure.Walls;\n\n\t\tif ( walled )\n\t\t{\n\t\t\tvar shell = panel.AddGroup( \"Walls\" );\n\n\t\t\tshell.Add( ArchPartUi.Number( \"Thickness\", cut.WallThickness, 8f, value => cut.WallThickness = value, changed ) );\n\t\t}\n\n\t\tusing ( var grid = ArchIconGrid.In( panel.AddGroup( \"Head\" ) ) )\n\t\t{\n\t\t\tgrid.Pick( \"Open to the sky\", \"cut_head_none\", \"crop_free\",\n\t\t\t\tcut.Head == CutHead.None, () => { cut.Head = CutHead.None; changed?.Invoke(); refresh?.Invoke(); } );\n\n\t\t\tgrid.Pick( \"Capped \u2014 a lid over the top\", \"cut_head_cap\", \"horizontal_rule\",\n\t\t\t\tcut.Head == CutHead.Cap, () => { cut.Head = CutHead.Cap; changed?.Invoke(); refresh?.Invoke(); } );\n\n\t\t\tgrid.Pick( \"Coped \u2014 a band round the mouth, finished like an outside edge\", \"cut_head_coping\", \"border_top\",\n\t\t\t\tcut.Head == CutHead.Coping, () => { cut.Head = CutHead.Coping; changed?.Invoke(); refresh?.Invoke(); } );\n\n\t\t\tgrid.Pick( \"Housing \u2014 a little walled box with its own roof\", \"cut_head_housing\", \"home\",\n\t\t\t\tcut.Head == CutHead.Housing, () => { cut.Head = CutHead.Housing; changed?.Invoke(); refresh?.Invoke(); } );\n\t\t}\n\n\t\tif ( cut.Head == CutHead.Coping )\n\t\t{\n\t\t\tvar coping = panel.AddGroup( \"Coping\" );\n\n\t\t\tcoping.Add( ArchPartUi.Number( \"Height\", cut.CopingHeight, 4f, value => cut.CopingHeight = value, changed ) );\n\t\t\tcoping.Add( ArchPartUi.Number( \"Oversail\", cut.CopingOversail, 1.5f, value => cut.CopingOversail = value, changed ) );\n\n\t\t\t// The band's width IS the shell's thickness, so a walled shaft has already been asked for it - two\n\t\t\t// controls on one field is the panel telling you they are two numbers.\n\t\t\tif ( !walled )\n\t\t\t{\n\t\t\t\tcoping.Add( ArchPartUi.Number( \"Band width\", cut.WallThickness, 8f, value => cut.WallThickness = value, changed ) );\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( cut.Head != CutHead.Housing )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar housing = panel.AddGroup( \"Housing\" );\n\n\t\thousing.Add( ArchPartUi.Number( \"Height\", cut.HeadHeight, 96f, value => cut.HeadHeight = value, changed ) );\n\t\thousing.Add( ArchPartUi.Check( \"Roof over it\", cut.HeadRoof, value => cut.HeadRoof = value, changed ) );\n\t}\n\n\tstatic void Affects( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed, ArchCutAffects allowed = ArchCutAffects.All )\n\t{\n\t\tvar open = AffectsExpanded;\n\t\tvar enabled = Enum.GetValues<ArchCutAffects>()\n\t\t\t.Count( target => target is not (ArchCutAffects.None or ArchCutAffects.All) && (allowed & target) == target && ArchCut.Affects( cut, target ) );\n\t\tvar targets = Enum.GetValues<ArchCutAffects>().Count( target => target is not (ArchCutAffects.None or ArchCutAffects.All) && (allowed & target) == target );\n\t\tvar toggle = new Button( $\"Affects ({enabled}/{targets})\", open ? \"expand_less\" : \"expand_more\" )\n\t\t{\n\t\t\tClicked = () => { AffectsExpanded = !open; refresh?.Invoke(); }\n\t\t};\n\n\t\tpanel.Layout.Add( toggle );\n\n\t\tif ( !open )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar affects = panel.AddGroup( \"Affects\" );\n\n\t\tAffect( affects, cut, allowed, \"Floors\", ArchCutAffects.Floors, changed );\n\t\tAffect( affects, cut, allowed, \"Ceilings\", ArchCutAffects.Ceilings, changed );\n\t\tAffect( affects, cut, allowed, \"Foundations\", ArchCutAffects.Foundations, changed );\n\t\tAffect( affects, cut, allowed, \"Walls and corners\", ArchCutAffects.Walls, changed );\n\t\tAffect( affects, cut, allowed, \"Wall fittings\", ArchCutAffects.WallFittings, changed );\n\t\tAffect( affects, cut, allowed, \"Windows and doors\", ArchCutAffects.Windows, changed );\n\t\tAffect( affects, cut, allowed, \"Roofs\", ArchCutAffects.Roofs, changed );\n\t\tAffect( affects, cut, allowed, \"Platforms\", ArchCutAffects.Platforms, changed );\n\t\tAffect( affects, cut, allowed, \"Pillars\", ArchCutAffects.Pillars, changed );\n\t\tAffect( affects, cut, allowed, \"Beams\", ArchCutAffects.Beams, changed );\n\t\tAffect( affects, cut, allowed, \"Stairs\", ArchCutAffects.Stairs, changed );\n\t\tAffect( affects, cut, allowed, \"Trims\", ArchCutAffects.Trims, changed );\n\t\tAffect( affects, cut, allowed, \"Gutters\", ArchCutAffects.Gutters, changed );\n\t\tAffect( affects, cut, allowed, \"Road strips\", ArchCutAffects.Roadway, changed );\n\t\tAffect( affects, cut, allowed, \"Tunnel lining\", ArchCutAffects.Lining, changed );\n\t}\n\n\tstatic void Affect( Layout group, ArchCutPart cut, ArchCutAffects allowed, string label, ArchCutAffects target, Action changed )\n\t{\n\t\tif ( (allowed & target) == target )\n\t\t{\n\t\t\tAffect( group, cut, label, target, changed );\n\t\t}\n\t}\n\n\tstatic void Affect( Layout group, ArchCutPart cut, string label, ArchCutAffects target, Action changed )\n\t{\n\t\tgroup.Add( ArchPartUi.Check( label, ArchCut.Affects( cut, target ), enabled =>\n\t\t{\n\t\t\tcut.Affects = enabled ? cut.Affects | target : cut.Affects & ~target;\n\t\t}, changed ) );\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Pillar/ArchPillarGen.cs",
            "FileName": "ArchPillarGen.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\npublic readonly record struct ArchPillarRun( int FromX, int FromY, int ToX, int ToY, bool AlongX );\n\npublic readonly record struct ArchPillarColumn( Vector3 At, int X, int Y );\n\npublic static class ArchPillarGen\n{\n\t// How far a column may be adrift of the surface under it before it is a part that never re-seated when it was\n\t// moved rather than one deliberately stood off its deck.\n\tconst float Adrift = 1f;\n\n\t// WHERE THE COLUMN ACTUALLY STANDS, resolved when it is drawn rather than when it was placed. A part dragged\n\t// across the plan - or one with a slab poured under it afterwards - kept the foot it was authored with and\n\t// came out buried in the floor it is standing on. Founded says the author set that foot by hand and means it.\n\t// The terrain is not re-read here: the plan has nothing to say about it, and the seat it was placed with is\n\t// the only honest answer where nothing is poured.\n\tpublic static float Seat( ArchPillarPart part, ArchKit kit, ArchPlan plan )\n\t{\n\t\tif ( part.Founded || plan is null )\n\t\t{\n\t\t\treturn part.BaseHeight;\n\t\t}\n\n\t\t// Up to where the column is already buried, never past it: a slab overhead is what the column reaches for,\n\t\t// and taking it as a seat would stand the column on the very thing it was put there to hold up.\n\t\tvar poured = ArchPillarSeat.Poured( plan, kit, part.Origin, part.BaseHeight + ArchPillarSeat.Sunk( kit, part ) );\n\n\t\treturn poured is { } standing && MathF.Abs( standing - part.BaseHeight ) > Adrift ? standing : part.BaseHeight;\n\t}\n\n\t// One answer to how tall a pillar stands: its authored height where it has one, else up to the lowest thing\n\t// standing over it - and CLAMPED to what is solid over it either way, because a column that grew past a slab\n\t// stands with its capital inside the floor it is there to carry. The generator, the ghost, the handle and the\n\t// extents report all resolve the same way, so a part that climbs past its room reads the same to the shaft\n\t// that builds it and the box that selects it.\n\t//\n\t// Level with that soffit rather than lapped into it: the contact pass deletes a face its coplanar neighbour\n\t// covers whole, so a bite would only bury a surviving face inside the slab.\n\tpublic static float Height( ArchPillarPart part, ArchRoom room, ArchKit kit, ArchPlan plan = null )\n\t{\n\t\tvar floor = Seat( part, kit, plan );\n\t\tvar slab = ArchPillarSoffit.Slab( plan, kit, part, floor );\n\n\t\tif ( part.Height > 0f )\n\t\t{\n\t\t\treturn slab < float.MaxValue ? MathF.Max( 8f, MathF.Min( part.Height, slab - floor ) ) : part.Height;\n\t\t}\n\n\t\tif ( room is null )\n\t\t{\n\t\t\treturn slab < float.MaxValue ? MathF.Max( 8f, slab - floor ) : kit.WallHeight;\n\t\t}\n\n\t\treturn MathF.Max( 8f, ArchPillarSoffit.Over( plan, kit, room, part, floor ) - floor );\n\t}\n\n\t// Computed once, so the spandrel and the run landing on it cannot disagree.\n\tpublic static List<ArchPillarRun> Runs( ArchPillarPart part )\n\t{\n\t\tvar runs = new List<ArchPillarRun>();\n\n\t\tif ( part.Span == PillarSpan.None || part.Placement != PillarPlacement.Grid )\n\t\t{\n\t\t\treturn runs;\n\t\t}\n\n\t\tvar countX = Math.Max( 1, part.CountX );\n\t\tvar countY = Math.Max( 1, part.CountY );\n\t\tvar edge = part.PerimeterOnly;\n\n\t\tif ( part.SpanAlongX )\n\t\t{\n\t\t\tfor ( var iy = 0; iy < countY; iy++ )\n\t\t\t{\n\t\t\t\tif ( edge && iy > 0 && iy < countY - 1 )\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfor ( var ix = 0; ix < countX - 1; ix++ )\n\t\t\t\t{\n\t\t\t\t\truns.Add( new ArchPillarRun( ix, iy, ix + 1, iy, true ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( !part.SpanAlongY )\n\t\t{\n\t\t\treturn runs;\n\t\t}\n\n\t\tfor ( var ix = 0; ix < countX; ix++ )\n\t\t{\n\t\t\tif ( edge && ix > 0 && ix < countX - 1 )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor ( var iy = 0; iy < countY - 1; iy++ )\n\t\t\t{\n\t\t\t\truns.Add( new ArchPillarRun( ix, iy, ix, iy + 1, false ) );\n\t\t\t}\n\t\t}\n\n\t\treturn runs;\n\t}\n\n\tpublic static HashSet<(int X, int Y)> Carried( ArchPillarPart part )\n\t{\n\t\tvar carried = new HashSet<(int X, int Y)>();\n\n\t\tforeach ( var run in Runs( part ) )\n\t\t{\n\t\t\tcarried.Add( (run.FromX, run.FromY) );\n\t\t\tcarried.Add( (run.ToX, run.ToY) );\n\t\t}\n\n\t\treturn carried;\n\t}\n\n\tpublic static void Build(\n\t\tArchMesh canvas,\n\t\tArchPillarPart part,\n\t\tArchRoom room,\n\t\tArchBuilding building,\n\t\tArchPlan plan,\n\t\tArchKit kit,\n\t\tArchStyle style )\n\t{\n\t\tvar chain = new[] { part.Palette, room.Palette, building.Palette };\n\t\tvar shaft = style.Brush( ArchSurface.Pillar, chain );\n\t\tvar cap = style.Brush( ArchSurface.PillarCap, chain );\n\n\t\tvar seat = Seat( part, kit, plan );\n\t\tvar height = Height( part, room, kit, plan );\n\n\t\tif ( part.Placement == PillarPlacement.Pilaster )\n\t\t{\n\t\t\tPilaster( canvas, part, room, height, shaft, cap, kit );\n\t\t\treturn;\n\t\t}\n\n\t\t// A span bears ON the columns, so the shaft stops at the springing.\n\t\tvar spanning = part.Span != PillarSpan.None && part.Placement == PillarPlacement.Grid;\n\t\tvar shaftHeight = spanning ? MathF.Max( 8f, height - part.SpanBand ) : height;\n\t\tvar carried = spanning ? Carried( part ) : new HashSet<(int X, int Y)>();\n\n\t\t// Only a column a run springs off gives up its top to the span.\n\t\tforeach ( var column in Columns( part, seat ) )\n\t\t{\n\t\t\tvar columnHeight = carried.Contains( (column.X, column.Y) ) ? shaftHeight : height;\n\n\t\t\tColumn( canvas, part, column.At, columnHeight, shaft, cap,\n\t\t\t\tBites( plan, kit, part, room, building, column.At, columnHeight ) );\n\n\t\t\tArchDamage.PillarCorners( canvas, plan, kit, building, room, part, column.At, columnHeight, style );\n\t\t}\n\n\t\tif ( spanning )\n\t\t{\n\t\t\tusing ( canvas.Part( ArchPieces.Span ) )\n\t\t\t{\n\t\t\t\tSpans( canvas, part, kit, seat, height, cap );\n\t\t\t}\n\t\t}\n\t}\n\n\t// A section standing where something else already decided its seat - roof plant on a deck plane, not a column\n\t// in a room, so there is no wall height to fall back on and its own is the only answer.\n\tpublic static void Stand( ArchMesh canvas, ArchPillarPart part, ArchBrush shaft, ArchBrush cap )\n\t{\n\t\tforeach ( var column in Columns( part ) )\n\t\t{\n\t\t\tColumn( canvas, part, column.At, MathF.Max( 1f, part.Height ), shaft, cap );\n\t\t}\n\t}\n\n\tpublic static List<Vector3> Positions( ArchPillarPart part ) => Columns( part ).Select( column => column.At ).ToList();\n\n\tpublic static List<ArchPillarColumn> Columns( ArchPillarPart part ) => Columns( part, part.BaseHeight );\n\n\tpublic static List<ArchPillarColumn> Columns( ArchPillarPart part, float seat )\n\t{\n\t\tvar columns = new List<ArchPillarColumn>();\n\t\tvar rotation = Rotation.FromYaw( part.Yaw );\n\n\t\tif ( part.Placement != PillarPlacement.Grid )\n\t\t{\n\t\t\tcolumns.Add( new ArchPillarColumn( new Vector3( part.Origin.x, part.Origin.y, seat ), 0, 0 ) );\n\t\t\treturn columns;\n\t\t}\n\n\t\tvar countX = Math.Max( 1, part.CountX );\n\t\tvar countY = Math.Max( 1, part.CountY );\n\n\t\tfor ( var ix = 0; ix < countX; ix++ )\n\t\t{\n\t\t\tfor ( var iy = 0; iy < countY; iy++ )\n\t\t\t{\n\t\t\t\tvar local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );\n\t\t\t\tvar world = rotation * local;\n\n\t\t\t\tcolumns.Add( new ArchPillarColumn( new Vector3( part.Origin.x + world.x, part.Origin.y + world.y, seat ), ix, iy ) );\n\t\t\t}\n\t\t}\n\n\t\treturn columns;\n\t}\n\n\treadonly record struct Course( float Bottom, float Top, Vector2 Lower, Vector2 Upper, bool Dressed );\n\n\t// What a Subtract standing over this column takes out of it, in the band the column actually occupies. Answered\n\t// through ArchCut like every other host's, so ArchLayerOrder decides whether a cut authored before the column\n\t// reaches it at all.\n\tpublic static List<ArchCarveVolume> Bites( ArchPlan plan, ArchKit kit, ArchPillarPart part, ArchRoom room, ArchBuilding building, Vector3 basePoint, float height )\n\t{\n\t\tvar bites = new List<ArchCarveVolume>();\n\n\t\tif ( plan is null || room is null )\n\t\t{\n\t\t\treturn bites;\n\t\t}\n\n\t\tvar widest = part.Reach;\n\t\tvar flat = new Vector2( basePoint.x, basePoint.y );\n\t\tvar footprint = ArchFootprint.Rect( flat - new Vector2( widest, widest ), flat + new Vector2( widest, widest ) );\n\t\tvar foot = basePoint.z - MathF.Max( 0f, part.FootingBuried );\n\t\tvar head = basePoint.z + height;\n\n\t\tforeach ( var cut in ArchCut.Over( plan, room.Floor, footprint, building?.Id ?? 0, ArchCutAffects.Pillars, part.Id ) )\n\t\t{\n\t\t\tbites.AddRange( ArchCut.Resolve( cut, kit ).Where( volume => ArchCut.Reaches( volume, foot, head ) ) );\n\t\t}\n\n\t\treturn bites;\n\t}\n\n\tstatic void Column( ArchMesh canvas, ArchPillarPart part, Vector3 basePoint, float height, ArchBrush shaft, ArchBrush cap, List<ArchCarveVolume> bites = null )\n\t{\n\t\tvar courses = Courses( part, basePoint.z, height );\n\n\t\tif ( courses.Count == 0 )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar flat = new Vector2( basePoint.x, basePoint.y );\n\n\t\tif ( bites is { Count: > 0 } )\n\t\t{\n\t\t\tCarved( canvas, part, flat, courses, bites, shaft, cap );\n\n\t\t\treturn;\n\t\t}\n\n\t\tHulled( canvas, part, flat, courses, shaft, cap );\n\t}\n\n\t// A column nothing carves stays ONE welded hull, which is what keeps a colonnade walkable and what stops a\n\t// stacked box sealing a face pair at every step.\n\tstatic void Hulled( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, ArchBrush shaft, ArchBrush cap )\n\t{\n\t\tvar sides = part.Sides;\n\n\t\tusing var welding = canvas.Welding();\n\n\t\t// ONE shape for the whole column, not one per course: the hull over every ring closes the inset at a plinth\n\t\t// or a capital step, which nothing can stand in anyway, and a colonnade stays walkable because each column\n\t\t// is its own hull rather than all of them being one mesh.\n\t\tusing var solid = canvas.Solid( ArchSolid.Hull( Rings( courses, flat, sides, part.Yaw ) ) );\n\n\t\tcanvas.Polygon( Ring( flat, courses[0].Lower, courses[0].Bottom, sides, part.Yaw ), courses[0].Dressed ? cap : shaft, true );\n\n\t\tfor ( var index = 0; index < courses.Count; index++ )\n\t\t{\n\t\t\tvar course = courses[index];\n\t\t\tvar brush = course.Dressed ? cap : shaft;\n\n\t\t\tFaces( canvas, flat, course, sides, part.Yaw, brush );\n\n\t\t\tif ( index == courses.Count - 1 )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tStep( canvas, flat, course, courses[index + 1], sides, part.Yaw, brush );\n\t\t}\n\n\t\tvar last = courses[^1];\n\n\t\tcanvas.Polygon( Ring( flat, last.Upper, last.Top, sides, part.Yaw ), last.Dressed ? cap : shaft );\n\t}\n\n\t// A column a cut reaches has no hull to be, so it goes through the same carve algebra a platform does - one\n\t// prism per course, so a bite's cheeks share the shaft's own edges. A course that TAPERS comes out straight\n\t// sided here: the prism algebra has no taper, and the only tapering course is a footing, which is buried.\n\tstatic void Carved( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, List<ArchCarveVolume> bites, ArchBrush shaft, ArchBrush cap )\n\t{\n\t\tusing var welding = canvas.Welding();\n\n\t\tforeach ( var course in courses )\n\t\t{\n\t\t\tvar ring = Ring( flat, course.Lower, 0f, part.Sides, part.Yaw )\n\t\t\t\t.Select( point => new Vector2( point.x, point.y ) )\n\t\t\t\t.ToList();\n\n\t\t\tvar carve = ArchCarve.Prism( ring, course.Bottom, course.Top ).In( part.Yaw );\n\n\t\t\tforeach ( var bite in bites )\n\t\t\t{\n\t\t\t\tcarve.Less( bite );\n\t\t\t}\n\n\t\t\tforeach ( var face in carve.Resolve().Faces )\n\t\t\t{\n\t\t\t\tcanvas.Polygon( face.Points, course.Dressed ? cap : shaft );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Every corner the column has, bottom ring to top ring, which is what its hull is taken over.\n\tstatic List<Vector3> Rings( List<Course> courses, Vector2 flat, int sides, float yaw )\n\t{\n\t\tvar points = new List<Vector3>();\n\n\t\tforeach ( var course in courses )\n\t\t{\n\t\t\tpoints.AddRange( Ring( flat, course.Lower, course.Bottom, sides, yaw ) );\n\t\t\tpoints.AddRange( Ring( flat, course.Upper, course.Top, sides, yaw ) );\n\t\t}\n\n\t\treturn points;\n\t}\n\n\tstatic List<Course> Courses( ArchPillarPart part, float baseHeight, float height )\n\t{\n\t\tvar courses = new List<Course>();\n\t\tvar section = part.Half;\n\n\t\tvar plinth = part.Plinth ? MathF.Max( 0f, part.PlinthHeight ) : 0f;\n\t\tvar capital = part.Capital ? MathF.Max( 0f, part.CapitalHeight ) : 0f;\n\n\t\tvar top = baseHeight + height;\n\t\tvar shaftBottom = baseHeight + plinth;\n\t\tvar shaftTop = top - capital;\n\n\t\tFooting( part, courses, section, baseHeight );\n\n\t\tif ( plinth > 0.05f )\n\t\t{\n\t\t\tvar spread = section + new Vector2( part.PlinthOversize, part.PlinthOversize );\n\n\t\t\tcourses.Add( new Course( baseHeight, shaftBottom, spread, spread, true ) );\n\t\t}\n\n\t\tif ( shaftTop - shaftBottom > 0.05f )\n\t\t{\n\t\t\tcourses.Add( new Course( shaftBottom, shaftTop, section, section, false ) );\n\t\t}\n\n\t\tif ( capital > 0.05f )\n\t\t{\n\t\t\tHead( part, courses, section, shaftTop, top );\n\t\t}\n\n\t\treturn courses;\n\t}\n\n\tstatic void Footing( ArchPillarPart part, List<Course> courses, Vector2 section, float baseHeight )\n\t{\n\t\tif ( part.Footing == PillarFooting.None )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar foot = baseHeight - MathF.Max( 0f, part.FootingBuried );\n\n\t\tcourses.AddRange( Pad( part.Footing, section, part.FootingReach,\n\t\t\tfoot, foot + MathF.Max( 1f, part.FootingHeight ) ) );\n\t}\n\n\t// THE SAME PAD, TURNED OVER. A course carries a lower and an upper section already, so mirroring one is\n\t// swapping those two and reflecting its band - which is why the head reuses the footing's shapes rather than\n\t// growing a taper of its own to keep in step with. It is never buried: level with the soffit it meets, because\n\t// the contact pass deletes a face its coplanar neighbour covers whole and a bite would only hide a survivor.\n\tstatic void Head( ArchPillarPart part, List<Course> courses, Vector2 section, float from, float top )\n\t{\n\t\tvar mirrored = new List<Course>();\n\n\t\tforeach ( var course in Pad( part.CapitalShape, section, MathF.Max( 0f, part.CapitalOversize ), from, top ) )\n\t\t{\n\t\t\tmirrored.Add( new Course( from + top - course.Top, from + top - course.Bottom,\n\t\t\t\tcourse.Upper, course.Lower, course.Dressed ) );\n\t\t}\n\n\t\t// ASCENDING, because the list's order is load-bearing: courses[0] caps the foot, courses[^1] caps the head,\n\t\t// and Step bridges each pair as neighbours. Mirroring reverses z, so the list has to be turned back.\n\t\tmirrored.Reverse();\n\t\tcourses.AddRange( mirrored );\n\t}\n\n\t// One pad, read bottom up: a plain block, a bevel dying into the shaft, or two steps.\n\tstatic IEnumerable<Course> Pad( PillarFooting kind, Vector2 section, float spread, float foot, float head )\n\t{\n\t\tvar wide = section + new Vector2( spread, spread );\n\t\tvar depth = MathF.Max( 1f, head - foot );\n\n\t\tswitch ( kind )\n\t\t{\n\t\t\tcase PillarFooting.Bevelled:\n\t\t\t\t// A third stays square under the taper, or the pad reads as a cone.\n\t\t\t\tvar shelf = foot + depth * 0.34f;\n\n\t\t\t\tyield return new Course( foot, shelf, wide, wide, true );\n\t\t\t\tyield return new Course( shelf, foot + depth, wide, section, true );\n\t\t\t\tbreak;\n\n\t\t\tcase PillarFooting.Stepped:\n\t\t\t\tvar middle = foot + depth * 0.5f;\n\t\t\t\tvar upper = section + new Vector2( spread * 0.5f, spread * 0.5f );\n\n\t\t\t\tyield return new Course( foot, middle, wide, wide, true );\n\t\t\t\tyield return new Course( middle, foot + depth, upper, upper, true );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tyield return new Course( foot, foot + depth, wide, wide, true );\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tstatic void Faces( ArchMesh canvas, Vector2 centre, Course course, int sides, float yaw, ArchBrush brush )\n\t{\n\t\tvar lower = Ring( centre, course.Lower, course.Bottom, sides, yaw );\n\t\tvar upper = Ring( centre, course.Upper, course.Top, sides, yaw );\n\n\t\tfor ( var index = 0; index < lower.Length; index++ )\n\t\t{\n\t\t\tvar next = (index + 1) % lower.Length;\n\n\t\t\tcanvas.Quad( lower[index], lower[next], upper[next], upper[index], brush );\n\t\t}\n\t}\n\n\t// Equal sections continue instead - a plain pillar stays six faces.\n\tstatic void Step( ArchMesh canvas, Vector2 centre, Course below, Course above, int sides, float yaw, ArchBrush brush )\n\t{\n\t\tif ( MathF.Abs( below.Upper.x - above.Lower.x ) < 0.01f && MathF.Abs( below.Upper.y - above.Lower.y ) < 0.01f )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\t// The two wind opposite ways - the wrong one leaves a hole in the half-edge mesh.\n\t\tvar stepsIn = below.Upper.x >= above.Lower.x;\n\n\t\tvar wide = Ring( centre, stepsIn ? below.Upper : above.Lower, below.Top, sides, yaw );\n\t\tvar tight = Ring( centre, stepsIn ? above.Lower : below.Upper, below.Top, sides, yaw );\n\n\t\tfor ( var index = 0; index < wide.Length; index++ )\n\t\t{\n\t\t\tvar next = (index + 1) % wide.Length;\n\n\t\t\tif ( stepsIn )\n\t\t\t{\n\t\t\t\tcanvas.Quad( wide[index], wide[next], tight[next], tight[index], brush );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tcanvas.Quad( tight[index], tight[next], wide[next], wide[index], brush );\n\t\t}\n\t}\n\n\t// The section, whatever number of sides it has: the rectangle by hand so a plain pier keeps its exact\n\t// corners, and anything rounder through ArchFootprint's own ellipse so a bool's circle and a column's\n\t// circle are the same loop. Turned about its own centre - a yaw that only marched the grid and left every\n\t// section square was a turn handle that did nothing at all to a lone column.\n\tstatic Vector3[] Ring( Vector2 centre, Vector2 half, float height, int sides, float yaw )\n\t{\n\t\tvar corners = sides < 5\n\t\t\t? new[]\n\t\t\t{\n\t\t\t\tnew Vector2( centre.x - half.x, centre.y - half.y ),\n\t\t\t\tnew Vector2( centre.x + half.x, centre.y - half.y ),\n\t\t\t\tnew Vector2( centre.x + half.x, centre.y + half.y ),\n\t\t\t\tnew Vector2( centre.x - half.x, centre.y + half.y )\n\t\t\t}\n\t\t\t: ArchFootprint.Ellipse( centre - half, centre + half, sides ).ToArray();\n\n\t\tvar turned = MathF.Abs( yaw % 360f ) < 0.01f ? corners : ArchFootprint.Turned( corners, centre, yaw ).ToArray();\n\n\t\treturn turned.Select( point => new Vector3( point.x, point.y, height ) ).ToArray();\n\t}\n\n\t// Seated so its top finishes on the column top, not proud of the capital.\n\tstatic void Spans( ArchMesh canvas, ArchPillarPart part, ArchKit kit, float seat, float height, ArchBrush brush )\n\t{\n\t\tvar rotation = Rotation.FromYaw( part.Yaw );\n\t\tvar top = seat + height;\n\t\tvar springing = MathF.Max( seat + 8f, top - part.SpanBand );\n\t\tvar half = part.Half;\n\t\tvar lap = ArchContact.Bite( kit );\n\n\t\tforeach ( var run in Runs( part ) )\n\t\t{\n\t\t\tvar inset = (run.AlongX ? half.x : half.y) - lap;\n\n\t\t\tConnect( canvas, part,\n\t\t\t\tPoint( part, rotation, run.FromX, run.FromY ), Point( part, rotation, run.ToX, run.ToY ),\n\t\t\t\trun.AlongX, springing, top, inset, brush );\n\t\t}\n\n\t\t// Laps a bite down into the shaft; flush leaves cap and block in one plane.\n\t\tforeach ( var (ix, iy) in Carried( part ) )\n\t\t{\n\t\t\tvar centre = Point( part, rotation, ix, iy );\n\n\t\t\tcanvas.Box(\n\t\t\t\tnew Vector3( centre.x - half.x, centre.y - half.y, ArchContact.Bury( kit, springing, 1f ) ),\n\t\t\t\tnew Vector3( centre.x + half.x, centre.y + half.y, top ),\n\t\t\t\tbrush );\n\t\t}\n\t}\n\n\tstatic Vector2 Point( ArchPillarPart part, Rotation rotation, int ix, int iy )\n\t{\n\t\tvar local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );\n\t\tvar world = rotation * local;\n\n\t\treturn new Vector2( part.Origin.x + world.x, part.Origin.y + world.y );\n\t}\n\n\t// Inset is the pier's half-section less a bite, so the run runs INTO the spandrel. The solid itself comes off\n\t// ArchSpanGen, which is what makes an arcade and a hand-picked arch the same geometry.\n\tstatic void Connect( ArchMesh canvas, ArchPillarPart part, Vector2 from, Vector2 to, bool alongX, float springing, float top, float inset, ArchBrush brush )\n\t{\n\t\tArchSpanGen.Build( canvas, new ArchSpanRun\n\t\t{\n\t\t\tFrom = from,\n\t\t\tTo = to,\n\t\t\tSpringing = springing,\n\t\t\tTop = top,\n\t\t\tThickness = part.SpanThickness( alongX ),\n\t\t\tInsetFrom = inset,\n\t\t\tInsetTo = inset,\n\t\t\tSegments = part.ArchSegments,\n\t\t\tRing = part.ArchRing,\n\t\t\tForm = part.Span\n\t\t}, brush );\n\t}\n\n\tstatic void Pilaster( ArchMesh canvas, ArchPillarPart part, ArchRoom room, float height, ArchBrush shaft, ArchBrush cap, ArchKit kit )\n\t{\n\t\tvar wall = room.Walls.Find( candidate => candidate.Id == part.WallId ) ?? room.Walls.FirstOrDefault();\n\n\t\tif ( wall is null )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;\n\t\tvar half = thickness * 0.5f;\n\t\tvar halfWidth = part.PilasterWidth * 0.5f;\n\t\tvar offset = Math.Clamp( part.WallOffset, halfWidth, Math.Max( halfWidth, wall.Length - halfWidth ) );\n\n\t\tvar direction = wall.Direction;\n\t\tvar normal = wall.Normal;\n\t\tvar centre = wall.PointAt( offset );\n\n\t\tFace( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, false );\n\n\t\tif ( part.PilasterBothFaces )\n\t\t{\n\t\t\tFace( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, true );\n\t\t}\n\t}\n\n\tstatic void Face(\n\t\tArchMesh canvas,\n\t\tVector2 centre,\n\t\tVector2 direction,\n\t\tVector2 normal,\n\t\tfloat halfWidth,\n\t\tfloat wallHalf,\n\t\tArchPillarPart part,\n\t\tfloat height,\n\t\tArchBrush shaft,\n\t\tArchBrush cap,\n\t\tbool inner )\n\t{\n\t\t// The primary face is the EXTERIOR one, and that is the -normal side: it is where ArchWallGen lays the\n\t\t// street skin and where a wall modifier's Outside stands, so a pilaster on +normal came out inside the\n\t\t// room it was put there to dress the outside of. Flipping the normal alone would reverse the loop's\n\t\t// winding, so the run is flipped with it - the block does not move, its corners are walked the other way.\n\t\tvar sign = inner ? 1f : -1f;\n\t\tvar run = direction * sign;\n\t\tvar near = wallHalf * sign;\n\t\tvar far = (wallHalf + part.PilasterProtrusion) * sign;\n\n\t\tvar bottom = part.BaseHeight;\n\t\tvar plinth = part.Plinth ? Math.Max( 0f, part.PlinthHeight ) : 0f;\n\t\tvar capital = part.Capital ? Math.Max( 0f, part.CapitalHeight ) : 0f;\n\n\t\tSlab( canvas, centre, run, normal, halfWidth, near, far, bottom + plinth, bottom + height - capital, shaft );\n\n\t\tif ( plinth > 0.05f )\n\t\t{\n\t\t\tSlab( canvas, centre, run, normal, halfWidth + part.PlinthOversize, near, far + part.PlinthOversize * sign, bottom, bottom + plinth, cap );\n\t\t}\n\n\t\tif ( capital > 0.05f )\n\t\t{\n\t\t\tSlab( canvas, centre, run, normal, halfWidth + part.CapitalOversize, near, far + part.CapitalOversize * sign, bottom + height - capital, bottom + height, cap );\n\t\t}\n\t}\n\n\tstatic void Slab(\n\t\tArchMesh canvas,\n\t\tVector2 centre,\n\t\tVector2 direction,\n\t\tVector2 normal,\n\t\tfloat halfWidth,\n\t\tfloat near,\n\t\tfloat far,\n\t\tfloat bottom,\n\t\tfloat top,\n\t\tArchBrush brush )\n\t{\n\t\tif ( top - bottom < 0.05f )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar a = centre - direction * halfWidth + normal * near;\n\t\tvar b = centre + direction * halfWidth + normal * near;\n\t\tvar c = centre + direction * halfWidth + normal * far;\n\t\tvar d = centre - direction * halfWidth + normal * far;\n\n\t\tvar lower = new List<Vector3>\n\t\t{\n\t\t\tnew( a.x, a.y, bottom ),\n\t\t\tnew( b.x, b.y, bottom ),\n\t\t\tnew( c.x, c.y, bottom ),\n\t\t\tnew( d.x, d.y, bottom )\n\t\t};\n\n\t\tvar upper = new List<Vector3>();\n\n\t\tforeach ( var point in lower )\n\t\t{\n\t\t\tupper.Add( point.WithZ( top ) );\n\t\t}\n\n\t\tcanvas.Prism( lower, upper, brush );\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Services/ArchGridService.cs",
            "FileName": "ArchGridService.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// The grid is the EDITOR'S \u2014 the one drawn in the viewport, with the spacing and the snap toggle already sitting\n// on the scene view's own bar. The tool held a second one of its own, which meant two answers to one question and\n// a cursor that could land on a rung nothing had drawn.\n//\n// Sizes always answer, because a reach or a minimum run is a measurement and does not stop being one when snapping\n// is off. Only a COORDINATE asks whether to snap, and it asks here rather than at any call site.\npublic sealed class ArchGridService\n{\n\tpublic const float FinestSize = 0.25f;\n\n\t// The angle ladder every authored turn ratchets onto. It stands beside the size ladder because stepping and\n\t// snapping are one invariant, and a module choosing its own would author off the grid with nothing to say so.\n\t// Authored like the size ladder the scene view owns, and ZERO is the free swing - which is why every reader\n\t// asks for it rather than testing a flag of its own.\n\tpublic static float AngleStep\n\t{\n\t\tget => Math.Clamp( EditorCookie.Get( \"arch.grid.anglestep\", 15f ), 0f, 90f );\n\t\tset => EditorCookie.Set( \"arch.grid.anglestep\", Math.Clamp( value, 0f, 90f ) );\n\t}\n\n\tpublic static bool Snapping => EditorScene.GizmoSettings.SnapToGrid;\n\n\tpublic float BaseSize => Math.Clamp( EditorScene.GizmoSettings.GridSpacing, FinestSize, 128f );\n\n\tpublic float Base( float value )\n\t{\n\t\treturn Snapping ? Snap( value, BaseSize ) : value;\n\t}\n\n\tpublic Vector2 Base( Vector2 point )\n\t{\n\t\treturn Snapping ? Snap( point, BaseSize ) : point;\n\t}\n\n\tpublic Vector3 Base( Vector3 point )\n\t{\n\t\treturn Snapping ? Snap( point, BaseSize ) : point;\n\t}\n\n\tpublic float Subgrid( float value, int divisions = 8 )\n\t{\n\t\treturn Snapping ? Snap( value, SubgridSize( divisions ) ) : value;\n\t}\n\n\tpublic Vector2 Subgrid( Vector2 point, int divisions = 8 )\n\t{\n\t\treturn Snapping ? Snap( point, SubgridSize( divisions ) ) : point;\n\t}\n\n\tpublic Vector3 Subgrid( Vector3 point, int divisions = 8 )\n\t{\n\t\treturn Snapping ? Snap( point, SubgridSize( divisions ) ) : point;\n\t}\n\n\tpublic Vector3 CurveControl( Vector3 point )\n\t{\n\t\tvar flat = Base( new Vector2( point.x, point.y ) );\n\n\t\treturn new Vector3( flat.x, flat.y, Height( point.z ) );\n\t}\n\n\t// A control DRAPED on ground: the flat lands on the ladder and the height does not, because a height read off\n\t// the terrain is measured rather than authored - rung it and the run sinks into the ground it was laid on.\n\tpublic Vector3 Draped( Vector3 point )\n\t{\n\t\tvar flat = Base( new Vector2( point.x, point.y ) );\n\n\t\treturn new Vector3( flat.x, flat.y, point.z );\n\t}\n\n\t// No second grid for z - the same ladder a plan coordinate lands on.\n\tpublic float Height( float value )\n\t{\n\t\treturn Base( value );\n\t}\n\n\tpublic List<Vector2> Base( IEnumerable<Vector2> points )\n\t{\n\t\treturn points.Select( Base ).ToList();\n\t}\n\n\tpublic (Vector2 Min, Vector2 Max) Rectangle( Vector2 first, Vector2 second )\n\t{\n\t\tvar min = Vector2.Min( first, second );\n\t\tvar max = Vector2.Max( first, second );\n\n\t\treturn (Base( min ), Base( max ));\n\t}\n\n\tpublic float SubgridSize( int divisions = 8 )\n\t{\n\t\tvar count = PowerOfTwo( Math.Max( 1, divisions ) );\n\n\t\treturn MathF.Max( FinestSize, BaseSize / count );\n\t}\n\n\tpublic ArchDivision DivideAtMost( float span, float spacing, int divisions = 8 )\n\t{\n\t\tvar length = MathF.Abs( Subgrid( span, divisions ) );\n\n\t\tif ( length < FinestSize )\n\t\t{\n\t\t\treturn new ArchDivision { Span = 0f, Count = 0 };\n\t\t}\n\n\t\tvar unit = SubgridSize( divisions );\n\t\tvar units = Math.Max( 1, (int)MathF.Round( length / unit ) );\n\t\tvar minimum = Math.Max( 1, (int)MathF.Ceiling( length / MathF.Max( unit, spacing ) ) );\n\t\tvar count = minimum;\n\n\t\twhile ( count < units && units % count != 0 )\n\t\t{\n\t\t\tcount++;\n\t\t}\n\n\t\treturn new ArchDivision { Span = length, Count = Math.Min( count, units ) };\n\t}\n\n\tpublic static float Fine( float value )\n\t{\n\t\treturn Snap( value, FinestSize );\n\t}\n\n\tpublic static Vector2 Fine( Vector2 point )\n\t{\n\t\treturn Snap( point, FinestSize );\n\t}\n\n\tpublic static Vector3 Fine( Vector3 point )\n\t{\n\t\treturn Snap( point, FinestSize );\n\t}\n\n\tpublic static float Snap( float value, float size )\n\t{\n\t\treturn MathF.Round( value / size ) * size;\n\t}\n\n\t// Where a DRAGGED edit lands: the step is what goes on the ladder, never the coordinate. An existing shape is\n\t// wherever it was authored - a turned loop's corners are nowhere near a rung, and a cut's band is wherever it\n\t// was pulled to - so snapping the absolute coordinate yanks the whole thing onto the nearest one the moment a\n\t// handle touches it. On a base grid of 64 a nudge along x dropped a cut standing at -40 straight to 0, which\n\t// is the shape jumping a storey for a gesture that never touched its height. Placement still snaps outright:\n\t// a NEW shape is authored on the grid, and an edit keeps the offset it already had.\n\tpublic static float Stepped( float from, float to, float size )\n\t{\n\t\treturn from + Snap( to - from, size );\n\t}\n\n\tpublic static Vector2 Stepped( Vector2 from, Vector2 to, float size )\n\t{\n\t\treturn new Vector2( Stepped( from.x, to.x, size ), Stepped( from.y, to.y, size ) );\n\t}\n\n\tstatic Vector2 Snap( Vector2 point, float size )\n\t{\n\t\treturn new Vector2( Snap( point.x, size ), Snap( point.y, size ) );\n\t}\n\n\tstatic Vector3 Snap( Vector3 point, float size )\n\t{\n\t\treturn new Vector3( Snap( point.x, size ), Snap( point.y, size ), Snap( point.z, size ) );\n\t}\n\n\tstatic int PowerOfTwo( int value )\n\t{\n\t\tvar result = 1;\n\n\t\twhile ( result < value )\n\t\t{\n\t\t\tresult *= 2;\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Tool/ArchSubtool.cs",
            "FileName": "ArchSubtool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\npublic abstract class ArchSubtool : EditorTool\n{\n\tToolSidebarWidget sidebar;\n\n\tprotected ArchSubtool( ArchTool owner )\n\t{\n\t\tOwner = owner;\n\t}\n\n\tprotected ArchTool Owner { get; }\n\n\tstatic readonly ArchViewAxis[] PlanOnly = { ArchViewAxis.Top };\n\n\tArchViewAxis served = (ArchViewAxis)(-1);\n\n\tprotected bool Dragging { get; private set; }\n\tprotected Vector2 DragStart { get; private set; }\n\tprotected Vector2 DragCurrent { get; private set; }\n\tprotected float DragStartHeight { get; private set; }\n\tprotected float DragHeight { get; private set; }\n\n\t// The deck the gesture began on, so a placement files itself on the roof it was drawn against rather than\n\t// looking one up again from a point the camera has since moved past.\n\tprotected ArchRoofPart DragDeck { get; private set; }\n\n\tprotected ArchCursor Pointer { get; private set; }\n\n\tprotected virtual bool UsesDrag => true;\n\n\t// Off the grid, the cursor is read where the ray actually landed instead. The grid that answers is the\n\t// editor's own, so this follows the scene view's snap toggle rather than one of the tool's own.\n\tprotected virtual bool Snapped => ArchGridService.Snapping;\n\n\t// The snap band THIS gesture wants, off the reach the author set on the bar. Tight by default, because most\n\t// gestures are drags and a face snap sets the point flush across the line: both ends inside the band and the\n\t// drag has no across component left to place anything with. A gesture whose whole purpose is to set something\n\t// flush against what already stands answers ArchWallSnap.Flush instead.\n\tpublic virtual ArchSnapReach SnapReach( float authored ) => ArchWallSnap.Reach( authored );\n\n\t// A footprint has no meaning in an elevation, and a tool that quietly placed at the origin would be worse than one that refuses.\n\tpublic virtual ArchViewAxis[] Works => PlanOnly;\n\n\tpublic bool Serves( ArchViewAxis axis ) => Works.Contains( axis );\n\n\t// The work-plane grid is scaffolding for placing; an unfocused view is not placing either.\n\tpublic virtual bool Placing => Manager?.IsCurrentViewFocused == true;\n\n\tpublic virtual ArchSurface[] Surfaces => Array.Empty<ArchSurface>();\n\n\t// One answer for the palette tile and the sidebar header: the authored art, else the type's own [Icon].\n\tpublic string Icon => ArchIcons.Get( ArchIcons.SubtoolSlug( this ), EditorTypeLibrary.GetType( GetType() )?.Icon ?? \"category\" );\n\n\t// A gesture the author is actually in the middle of: the mouse held through a drag, or a chained run with an\n\t// end already taken. Merely having a placement tool up is not one, and that is the whole difference between a\n\t// stack that says what you are doing and one that says \"placing\" from the moment you pick the tool.\n\tbool Gesturing => Dragging || Run().Active;\n\n\tpublic override void OnUpdate()\n\t{\n\t\tactiveDraft?.Show( Gesturing );\n\n\t\tif ( Manager?.IsCurrentViewFocused != true )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar axis = Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis;\n\n\t\tif ( axis != served )\n\t\t{\n\t\t\tserved = axis;\n\t\t\tRefresh();\n\t\t}\n\n\t\t// The selection's own widgets do not depend on where the cursor lands - the gizmo hit-tests itself - so\n\t\t// they are drawn before the work plane is asked for anything, and before a free-click tool takes the\n\t\t// frame. Behind those gates, aiming at the sky took the widgets off the part that was selected, a drag\n\t\t// already under way died halfway through the gesture, and a subtool reading faces showed no widget at\n\t\t// all - which is a layer selected in the stack with nothing to drag it by.\n\t\tif ( Serves( axis ) && Adjusting() )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\t// A pick that is not on the work plane at all - a face overhead - has to be taken BEFORE the cursor\n\t\t// gate. Looking up at a ceiling never crosses that plane, so the frame stopped here and the click\n\t\t// simply went missing.\n\t\tif ( TakesFreeClick )\n\t\t{\n\t\t\tusing ( ArchGhost.Begin() )\n\t\t\t{\n\t\t\t\tDrawFreeHover();\n\t\t\t}\n\n\t\t\tif ( Gizmo.WasLeftMousePressed )\n\t\t\t{\n\t\t\t\tOnFreeClick();\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !Serves( axis ) || !Owner.Cursor( out var cursor ) )\n\t\t{\n\t\t\t// A gesture with nowhere to land still has to END, or the preview follows the cursor for ever and\n\t\t\t// the next release dispatches a drag from a start the author left behind minutes ago.\n\t\t\tDragging &= !Gizmo.WasLeftMouseReleased;\n\n\t\t\tif ( !Dragging )\n\t\t\t{\n\t\t\t\tOwner.StepOff();\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tvar point = Snapped ? cursor.Plan : cursor.Free;\n\n\t\tPointer = cursor;\n\t\tDragCurrent = point;\n\t\tDragHeight = cursor.Height;\n\n\t\tif ( !UsesDrag )\n\t\t{\n\t\t\tusing ( ArchGhost.Begin() )\n\t\t\t{\n\t\t\t\tDrawHover( point );\n\t\t\t}\n\n\t\t\tif ( Gizmo.WasLeftMousePressed )\n\t\t\t{\n\t\t\t\tOwner.EnsureTarget();\n\t\t\t\tOnClick( point );\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Gizmo.WasLeftMousePressed )\n\t\t{\n\t\t\tDragStart = point;\n\t\t\tDragStartHeight = cursor.Height;\n\t\t\tDragDeck = cursor.OnDeck;\n\t\t\tDragging = true;\n\n\t\t\t// The whole gesture works whatever it began on. Without this the ray is re-projected onto the storey's\n\t\t\t// plane every frame, so a drag begun on a deck 128 inches up ran off across the yard the moment the\n\t\t\t// camera was not looking straight down at it.\n\t\t\tOwner.StandOn( cursor.Height, cursor.OnDeck );\n\n\t\t\treturn;\n\t\t}\n\n\t\tusing ( ArchGhost.Begin() )\n\t\t{\n\t\t\tif ( Dragging )\n\t\t\t{\n\t\t\t\tDrawPreview();\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tDrawHover( point );\n\t\t\t}\n\t\t}\n\n\t\tif ( !Gizmo.WasLeftMouseReleased || !Dragging )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tDragging = false;\n\t\tdrawn = null;\n\n\t\tOwner.StepOff();\n\t\tOwner.EnsureTarget();\n\t\tOnDrag( DragStart, point );\n\t}\n\n\t// Off by default: most tools place and move on. A tool that owns the thing it just placed turns it on\n\t// so the shape can be pulled about without leaving the tool that drew it.\n\tprotected virtual bool Adjusts => false;\n\n\t// Whoever draws the selection's widgets, they get the frame HERE - true while they hold the mouse, so the\n\t// tool's own gesture stands down. One slot, or a subtool that draws its handles somewhere further down is a\n\t// subtool whose handles are gated behind whatever it does first.\n\tprotected virtual bool Adjusting() => Adjust();\n\n\tbool adjusting;\n\n\tobject drawn;\n\n\t// The shape just drawn keeps the sidebar - its own numbers are what you reach for next - but NOT its\n\t// widgets, until a gesture has been and gone. A box dragger standing over the thing you just placed covers\n\t// the very surface you place the next one ON, and the gizmo takes the press first, so every drag after the\n\t// first went into the widget and nothing was ever placed again.\n\tprotected void Drew( object placed ) => drawn = placed;\n\n\t// A deliberate SELECT is the author asking to edit that shape, which is the one thing the memo must not\n\t// outlast: placing a bool files it as drawn, and nothing else cleared that, so picking it again in the Plan\n\t// Layers stack gave a selected part with the properties open and no widget anywhere until another shape had\n\t// been dragged over it. Placement sets Picked directly and does NOT come through here, so the shape you just\n\t// drew still waits for its gesture.\n\tinternal void Forget() => drawn = null;\n\n\t// True while the selection's own widgets have the mouse, so the placement gesture stands down. The\n\t// commit waits for the release, or a drag would stack one undo entry per pixel.\n\t//\n\t// The drawn shape is forgotten where the placement is DISPATCHED, never here: clearing it on the release\n\t// frame put the widgets back a frame early, and the engine still reports its pressed path on that frame,\n\t// so they took the release the drag was about to place on. Every gesture after the first was read as an\n\t// edit of the last shape - nothing was ever placed again and the preview never stood down.\n\tbool Adjust()\n\t{\n\t\t// A HELD WIDGET IS A GESTURE, AND A GESTURE ENDS WITH THE BUTTON. This latched true and was cleared only on a\n\t\t// release the three gates below let through, so anything that moved between the grab and the release - the\n\t\t// selection changing, the picked part becoming the one just drawn, the kind going out of this tool's reach -\n\t\t// left it set for the life of the subtool. CapturesPlacement then answered true on every frame afterwards,\n\t\t// Adjusting took the whole frame, and the tool never saw another press: a driveway that would not drag, for\n\t\t// ever, with nothing on screen to say why. The release frame is spared so the edit below still commits.\n\t\tif ( !Gizmo.IsLeftMouseDown && !Gizmo.WasLeftMouseReleased )\n\t\t{\n\t\t\tadjusting = false;\n\t\t}\n\n\t\tif ( !Adjusts || Owner.Picked is not { } picked || !ArchShapeHandles.ShowsHandles( picked.Item, drawn ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( ArchHandles.Draw( Owner, picked ) )\n\t\t{\n\t\t\tadjusting = true;\n\t\t\tOwner.Preview();\n\t\t}\n\n\t\tif ( !ArchShapeHandles.CapturesPlacement( ArchShapeHandles.HandlePressed, adjusting ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( Gizmo.WasLeftMouseReleased )\n\t\t{\n\t\t\tadjusting = false;\n\t\t\tOwner.Commit( $\"Edit {picked.Describe()}\" );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprotected virtual void OnDrag( Vector2 from, Vector2 to ) { }\n\n\tprotected virtual void OnClick( Vector2 point ) { }\n\n\t// For a subtool whose click is a ray into the scene rather than a point on the plan.\n\tprotected virtual bool TakesFreeClick => false;\n\n\tprotected virtual void OnFreeClick() { }\n\n\tprotected virtual void DrawFreeHover() { }\n\n\t// Overrides run inside an already-configured ArchGhost scope - no gizmo setup of their own.\n\tprotected virtual void DrawHover( Vector2 point )\n\t{\n\t\tArchGhost.Cursor( point, DragHeight, Owner.Kit.GridSize );\n\t}\n\n\tprotected virtual void DrawPreview()\n\t{\n\t\tArchGhost.Cursor( DragCurrent, DragHeight, Owner.Kit.GridSize );\n\n\t\tGizmo.Draw.Line(\n\t\t\tnew Vector3( DragStart.x, DragStart.y, DragStartHeight ),\n\t\t\tnew Vector3( DragCurrent.x, DragCurrent.y, DragHeight ) );\n\t}\n\n\tprotected void DrawRectPreview()\n\t{\n\t\tArchGhost.Plate( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), Owner.LevelHeight );\n\t}\n\n\tprotected static Vector2 Min( Vector2 a, Vector2 b ) => new( MathF.Min( a.x, b.x ), MathF.Min( a.y, b.y ) );\n\n\tprotected static Vector2 Max( Vector2 a, Vector2 b ) => new( MathF.Max( a.x, b.x ), MathF.Max( a.y, b.y ) );\n\n\t// A refresh rebuilds the whole sidebar, so a section can appear or disappear with the choice above it.\n\tpublic override Widget CreateToolSidebar()\n\t{\n\t\t// This tool is up in its own right now, so a refresh belongs to this panel again - the loan ended\n\t\t// whenever the shelf that borrowed it went away.\n\t\tlent = null;\n\n\t\tsidebar = new ToolSidebarWidget();\n\t\tPopulate();\n\n\t\treturn sidebar;\n\t}\n\n\t// Finish and Cancel exist only while a chained operation is live, and they ride the footer so a long\n\t// form cannot push the way out of a run off the bottom of the sidebar.\n\tpublic override Widget CreateToolFooter() => new ArchRunBar( Run, FinishRun, CancelRun );\n\n\tprotected virtual ArchRunState Run() => default;\n\n\tprotected virtual void FinishRun() { }\n\n\tprotected virtual void CancelRun() { }\n\n\t// A noun, not a gesture: the gesture belongs in the advice line.\n\tprotected virtual string Title() => \"Options\";\n\n\tprotected virtual string Shortcut() => null;\n\n\t// Disclosures, searches and browser heights survive a refresh by being keyed to the tool, not the widget.\n\tprotected string Scope( string key ) => ArchSidebarState.Scope( this, key );\n\n\t// A placement tool names the kind its draft previews; the draft lives in the tree for the\n\t// tool's whole life and FinishDraft binds the part the placement just committed.\n\tprotected virtual ArchKind? DraftKind => null;\n\n\tArchDraft activeDraft;\n\n\t// The insertion target wins; without one the draft names the active building.\n\tprotected ArchLayerRef? PlacementParent()\n\t{\n\t\tif ( Owner.InsertionTarget is { } target )\n\t\t{\n\t\t\treturn target;\n\t\t}\n\n\t\treturn Owner.LayerTree.Find( Owner.ActiveBuildingId )?.Ref;\n\t}\n\n\tpublic override void OnEnabled()\n\t{\n\t\tbase.OnEnabled();\n\n\t\tBeginDraft();\n\t}\n\n\t// Also reached by a tool whose gesture changes the kind it is about to place: left alone, the row the\n\t// draft opened still names the old kind, and the next placement arrives under the wrong heading.\n\tprotected void BeginDraft()\n\t{\n\t\tif ( DraftKind is { } kind )\n\t\t{\n\t\t\tactiveDraft ??= Owner.BeginPlacement( kind, PlacementParent(), null, null, this );\n\t\t}\n\t}\n\n\tpublic override void OnDisabled()\n\t{\n\t\tOwner.StepOff();\n\t\tCancelDraft();\n\t\tbase.OnDisabled();\n\t}\n\n\tprotected void CancelDraft()\n\t{\n\t\tif ( activeDraft is not null )\n\t\t{\n\t\t\tOwner.CancelPlacement( activeDraft );\n\t\t\tactiveDraft = null;\n\t\t}\n\t}\n\n\tprotected void FinishDraft() => FinishDraft( 0 );\n\n\tprotected void FinishDraft( int itemId )\n\t{\n\t\tif ( activeDraft is not null )\n\t\t{\n\t\t\tOwner.FinishPlacement( activeDraft, itemId );\n\t\t\tactiveDraft = null;\n\t\t}\n\t}\n\n\t// ONE LINE, and the line is the gesture. Anything an author would otherwise be told in a paragraph standing\n\t// over the controls belongs in this tool's guide, behind the header's HOW TO chip, where it can be as long\n\t// as it needs to be and nobody has to read past it to reach step one.\n\tprotected virtual string Advice() => null;\n\n\tprotected virtual void BuildOptions( ToolSidebarWidget panel ) { }\n\n\t// Whether this tool's own controls can be pointed at a SELECTION rather than at what it last placed.\n\t// Off unless Adopt is overridden: borrowed without one, every control on the panel writes the seed for the\n\t// next drag while the author watches the picked part not move.\n\t// Whether this subtool takes the turn key for itself. A placement that follows the cursor does; anything\n\t// else leaves it for the building already standing.\n\tpublic virtual bool Turns() => false;\n\n\tpublic virtual bool Adopts => false;\n\n\t// Whether this tool's panel is already the right one for what was picked, which decides whether selecting a\n\t// layer in the stack KEEPS the tool the author was drawing with. The manifest's own answer by default: a tool\n\t// that authors the kind and adopts a selection has nothing to gain from being swapped for Select.\n\tpublic virtual bool Hosts( object payload )\n\t{\n\t\tif ( !Adopts || payload is null )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\treturn ArchKinds.Load().Claiming( payload )?.Kind is { } claimed && ReferenceEquals( Authoring( claimed ), this );\n\t}\n\n\t// Selection borrows this tool's own options: picking a walkway in the stack should put the\n\t// walkway's roof, finish and pier controls in the shelf, not make you re-place one to reach them.\n\t// Adopt binds them to what is selected instead of to whatever this tool last created.\n\t//\n\t// The refresh comes from the BORROWER, and it is kept: the controls laid out here call back long after this\n\t// has returned, and this tool's own sidebar is not the one on screen while its shelf is being lent out.\n\tpublic void BuildAdopted( ToolSidebarWidget panel, ArchSelection picked, Action refresh )\n\t{\n\t\tlent = refresh;\n\n\t\tAdopt( picked );\n\t\tBuildOptions( panel );\n\t}\n\n\tAction lent;\n\n\t// Whether this panel is standing in another tool's shelf right now. A tool whose own form authors the NEXT\n\t// gesture needs to know, or lent out it tunes the seed while the author watches the picked part not move.\n\tprotected bool Lent => lent is not null;\n\n\t// A placement tool edits the thing it just made; adopting points that at the selection instead.\n\tprotected virtual void Adopt( ArchSelection picked ) { }\n\n\t// The shelf tool that authors what is selected, so its controls can be borrowed.\n\tpublic ArchSubtool Authoring( ArchKind kind )\n\t{\n\t\tvar wanted = ArchKindsAsked.Subtool( kind );\n\n\t\tif ( wanted.Length == 0 )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\treturn Owner.Tools.OfType<ArchSubtool>().FirstOrDefault( tool => tool.GetType().Name == wanted );\n\t}\n\n\tprotected void Refresh()\n\t{\n\t\tif ( lent is not null )\n\t\t{\n\t\t\tlent();\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !sidebar.IsValid() )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tsidebar.Layout.Clear( true );\n\t\tPopulate();\n\t}\n\n\t// The Plan Layers dock selects outside the subtool, so it needs the one refresh entry point.\n\tpublic void RefreshSidebar() => Refresh();\n\n\tvoid Populate()\n\t{\n\t\tvar serves = Serves( Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis );\n\n\t\tArchSidebarLayout.Header( sidebar, Title(), Icon, Shortcut(), serves ? Advice() : Refusal(), Guide() );\n\n\t\t// In a view it cannot honour the whole form goes, not just its enabled state.\n\t\tif ( !serves )\n\t\t{\n\t\t\tsidebar.Layout.AddStretchCell();\n\t\t\treturn;\n\t\t}\n\n\t\tBuildOptions( sidebar );\n\n\t\tsidebar.Layout.AddStretchCell();\n\t}\n\n\tAction Guide()\n\t{\n\t\tif ( ArchGuides.For( this ) is not { } written )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\treturn () => ArchGuideWindow.Open( written );\n\t}\n\n\tstring Refusal()\n\t{\n\t\tvar views = string.Join( \", \", Works.Select( axis => axis == ArchViewAxis.Top ? \"the plan\" : $\"the {axis} elevation\" ) );\n\n\t\treturn $\"Nothing to place from here \u2014 this tool works in {views}.\";\n\t}\n\n\tprotected static Widget Wrapped( string text ) => ArchPartUi.Wrapped( text );\n\n}\n\npublic static class ArchPaletteUi\n{\n\tpublic static Widget Row( Widget parent, ArchPalette palette, ArchSurface surface, Action changed )\n\t{\n\t\tvar holder = new Widget( parent );\n\t\tholder.Layout = Layout.Row();\n\t\tholder.Layout.Spacing = 4;\n\n\t\tvar label = new Label( surface.ToString() );\n\t\tlabel.MinimumWidth = 92;\n\t\tholder.Layout.Add( label );\n\n\t\tpalette.TryGet( surface, out var path );\n\n\t\tvar value = new Label( string.IsNullOrWhiteSpace( path ) ? \"(inherited)\" : System.IO.Path.GetFileNameWithoutExtension( path ) );\n\t\tvalue.MinimumWidth = 110;\n\t\tholder.Layout.Add( value );\n\n\t\tholder.Layout.Add( new Button( \"\", \"image\" )\n\t\t{\n\t\t\tClicked = () =>\n\t\t\t{\n\t\t\t\tvar picker = AssetPicker.Create( parent, AssetType.Material );\n\t\t\t\tpicker.Window.Title = $\"Material for {surface}\";\n\t\t\t\tpicker.OnAssetPicked = assets =>\n\t\t\t\t{\n\t\t\t\t\tvar asset = assets.FirstOrDefault();\n\t\t\t\t\tif ( asset is null ) return;\n\n\t\t\t\t\tpalette.Set( surface, asset.Path );\n\t\t\t\t\tArchStyle.InvalidateCache();\n\t\t\t\t\tchanged?.Invoke();\n\t\t\t\t};\n\t\t\t\tpicker.Show();\n\t\t\t}\n\t\t} );\n\n\t\t// Blank = no override; the generator falls back to ArchMesh.TexelScale.\n\t\tvar scale = palette.ScaleFor( surface );\n\t\tvar density = new LineEdit( scale > 0f ? scale.ToString( \"0.####\" ) : \"\" )\n\t\t{\n\t\t\tPlaceholderText = ArchMesh.TexelScale.ToString( \"0.####\" ),\n\t\t\tMaximumWidth = 64,\n\t\t\tToolTip = \"Units per texel for this role. Blank inherits.\"\n\t\t};\n\n\t\tdensity.TextEdited += text =>\n\t\t{\n\t\t\tpalette.SetScale( surface, float.TryParse( text, out var parsed ) ? parsed : 0f );\n\t\t\tArchStyle.InvalidateCache();\n\t\t\tchanged?.Invoke();\n\t\t};\n\n\t\tholder.Layout.Add( density );\n\n\t\tholder.Layout.Add( new Button( \"\", \"close\" )\n\t\t{\n\t\t\tClicked = () =>\n\t\t\t{\n\t\t\t\tpalette.Set( surface, null );\n\t\t\t\tdensity.Text = \"\";\n\t\t\t\tArchStyle.InvalidateCache();\n\t\t\t\tchanged?.Invoke();\n\t\t\t}\n\t\t} );\n\n\t\tholder.Layout.AddStretchCell();\n\n\t\treturn holder;\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Tool/Subtools/ArchBuildingSubtool.cs",
            "FileName": "ArchBuildingSubtool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\npublic enum BuildingMode\n{\n\tNew,\n\tExtend,\n\tPrefab,\n\tRow,\n\tSaved\n}\n\n[Title( \"Building\" ), Icon( \"domain_add\" ), Group( \"02\" )]\npublic sealed class ArchBuildingSubtool( ArchTool owner ) : ArchSubtool( owner )\n{\n\tprotected override ArchKind? DraftKind => ArchKind.Building;\n\n\tpublic override ArchSurface[] Surfaces => new[] { ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.Floor, ArchSurface.Roof, ArchSurface.Soffit, ArchSurface.WallCap };\n\n\tBuildingMode mode;\n\tstring archetype = \"house\";\n\tbool flipX;\n\tbool flipY;\n\tbool withRoof = true;\n\tbool withFloor = true;\n\tbool withGutters = true;\n\tbool withFoundation = true;\n\tRoofStyle roofStyle = RoofStyle.Hip;\n\tRidgeRun ridge = RidgeRun.Auto;\n\tSectionRoof wingRoof = SectionRoof.Continue;\n\t// Off: a dropped eave opens a void under the storey grid above it.\n\tfloat eaveDrop;\n\n\t// Seeded from the type then editable - an invisible number can't be matched by hand.\n\tfloat wallHeight;\n\tfloat roofPitch;\n\tbool withFrame;\n\tbool withFascia = true;\n\tbool withSoffit = true;\n\tbool withCeiling;\n\n\tbool seeded;\n\n\t// The saved building being stamped, its outlines worked out once, and how far round it has been turned.\n\tArchBuildingAsset saved;\n\tArchAssetGhost savedGhost;\n\tList<ArchPresetItem<ArchBuildingAsset>> savedOffered;\n\tint savedStamp = -1;\n\tint quarters;\n\n\t// What the next placement is turned by. R adds quarters on top of it, so a stamp aimed at 20 degrees still\n\t// turns square corners from there.\n\tfloat placementAngle;\n\n\t// Re-dresses while still the newest in the plan; anything else authored moves the counter.\n\tint placedRoom;\n\tint placedRoof;\n\tint placedStamp;\n\tbool placedMerged;\n\n\tbool Extending => mode == BuildingMode.Extend;\n\n\tbool Rowing => mode == BuildingMode.Row;\n\n\tbool Stamping => mode == BuildingMode.Saved;\n\n\t// A stamp has nothing to size, so it takes a point rather than a rectangle.\n\tprotected override bool UsesDrag => !Stamping;\n\n\tArchArchetype Chosen => Owner.FindArchetype( archetype );\n\n\t// One bearing for the drag and the stamp alike, so the ghost, the note and what is stood all read the same.\n\tfloat Facing => placementAngle + quarters * 90f;\n\n\t// A wing takes the bearing of the building it abuts and a row takes the street's, so neither has one to name.\n\tbool Aimable => !Extending && !Rowing;\n\n\tprotected override string Title() => Stamping ? \"Saved Building\" : \"Building\";\n\n\t// Whether the section this drag stands will carry a roof at all - a wing answers through its own rules.\n\tbool Roofed => Extending ? wingRoof != SectionRoof.None : withRoof;\n\n\tprotected override string Advice() => mode switch\n\t{\n\t\tBuildingMode.Extend => \"Drag a wing onto the active building. It shares the wall it abuts, and inherits its type.\",\n\t\tBuildingMode.Prefab => \"Drag the plot. The type's canned layout is laid out across it in one go.\",\n\t\tBuildingMode.Row => \"Drag the frontage and how far back it reaches. Along a street it takes that road's verge and the units step with the curve.\",\n\t\tBuildingMode.Saved => \"Pick a saved building and it follows the cursor. R turns it a quarter, the Bearing box aims it at anything else; click stands it there.\",\n\t\t_ => \"Drag the outer shell. The chosen type decides its heights, roof and trims.\"\n\t};\n\n\t// Picking the tool starts the sequence over, so the first step is open and asking what this drag will be.\n\tpublic override void OnEnabled()\n\t{\n\t\tbase.OnEnabled();\n\n\t\tArchWorkflow.Restart( Scope( \"flow\" ) );\n\t}\n\n\tpublic override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\n\t\t// The save that fills this shelf happens in the Plan Layers stack, which has no way to reach back\n\t\t// into the sidebar - so the shelf watches the count instead of waiting to be told.\n\t\tif ( Stamping && savedOffered is not null && savedStamp != ArchLayerAssets.Revision )\n\t\t{\n\t\t\tRefresh();\n\t\t}\n\t}\n\n\t// Only while a stamp is actually following the cursor: with the shelf up and nothing chosen the key belongs\n\t// to whatever is picked in the plan.\n\tpublic override bool Turns()\n\t{\n\t\tif ( !Stamping || !Placing || saved is null )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tquarters = (quarters + 1) % 4;\n\n\t\treturn true;\n\t}\n\n\tprotected override void DrawHover( Vector2 point )\n\t{\n\t\tbase.DrawHover( point );\n\n\t\tif ( !Stamping || savedGhost is not { Shapes.Count: > 0 } ghost )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar map = ArchBuildingStamp.Landing( ghost, point, Facing );\n\n\t\tforeach ( var shape in ghost.Shapes )\n\t\t{\n\t\t\tArchGhost.Prism( shape.Loop.Select( corner => map.Of( corner ) ).ToList(), shape.Bottom, shape.Top );\n\t\t}\n\n\t\tvar span = map.Swapped( ghost.Max - ghost.Min );\n\n\t\tArchGhost.Note( new Vector3( point.x + span.x, point.y + span.y, Owner.LevelHeight ),\n\t\t\t$\"{saved.Title} \u2014 {span.x:0} x {span.y:0}, turned {Facing:0.##}\u00b0\" );\n\t}\n\n\tprotected override void OnClick( Vector2 point )\n\t{\n\t\tif ( !Stamping )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tif ( saved is null )\n\t\t{\n\t\t\tLog.Info( \"Architecture: pick a saved building before clicking - there is nothing to stand yet.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ArchBuildingStamp.Place( Owner.Plan, saved, point, Facing ) is not { } placed )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: {saved.Title} could not be stamped there.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tOwner.ActiveBuildingId = placed.Id;\n\t\tOwner.ActiveRoomId = placed.Rooms[0].Id;\n\n\t\tFinishDraft( placed.Id );\n\t\tBeginDraft();\n\t\tOwner.Commit( $\"Place {saved.Title}\" );\n\t}\n\n\tprotected override void DrawPreview()\n\t{\n\t\tif ( mode == BuildingMode.Prefab )\n\t\t{\n\t\t\tDrawPrefabPreview();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Rowing )\n\t\t{\n\t\t\tDrawRowPreview();\n\t\t\treturn;\n\t\t}\n\n\t\tvar standard = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );\n\t\tvar height = Extending ? MathF.Max( 32f, standard - MathF.Max( 0f, eaveDrop ) ) : standard;\n\t\tvar placement = Resolve( DragStart, DragCurrent );\n\n\t\tif ( !placement.IsUsable || Extending && !placement.TouchesHost )\n\t\t{\n\t\t\tDrawRectPreview();\n\t\t\treturn;\n\t\t}\n\n\t\tvar aimed = Aimable && MathF.Abs( Facing ) > 0.001f;\n\n\t\tif ( aimed )\n\t\t{\n\t\t\tArchGhost.Prism( Turned( placement.Min, placement.Max ), Owner.LevelHeight, Owner.LevelHeight + height );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tArchGhost.Volume( placement.Min, placement.Max, Owner.LevelHeight, Owner.LevelHeight + height );\n\t\t}\n\n\t\tArchGhost.Note( new Vector3( placement.Max.x, placement.Max.y, Owner.LevelHeight + height ),\n\t\t\taimed\n\t\t\t\t? $\"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}, turned {Facing:0.##}\u00b0\"\n\t\t\t\t: $\"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}\" );\n\n\t\t// The pitch ghost is drawn off a box, so a turned shell shows its footprint and stands its roof on release.\n\t\tif ( !withRoof && !Extending || aimed )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar min = placement.Min;\n\t\tvar max = placement.Max;\n\t\tvar style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;\n\t\tvar pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;\n\n\t\tArchGhost.Pitch( min, max, Owner.LevelHeight + height, pitch, style, ArchBuild.Ridged( ridge, min, max ) );\n\t}\n\n\t// From the layout's resolved rectangles, so growth and placement show before release.\n\tvoid DrawPrefabPreview()\n\t{\n\t\tvar recipe = Owner.FindArchetype( archetype );\n\n\t\tif ( recipe is null || !recipe.HasLayout )\n\t\t{\n\t\t\tDrawRectPreview();\n\t\t\treturn;\n\t\t}\n\n\t\tvar plot = ResolvedPrefabPlot( recipe, DragStart, DragCurrent, out var usable );\n\n\t\tif ( !usable )\n\t\t{\n\t\t\tDrawRectPreview();\n\t\t\treturn;\n\t\t}\n\n\t\tArchGhost.Plate( plot.Min, plot.Max, Owner.LevelHeight, 8 );\n\n\t\tforeach ( var step in recipe.Steps )\n\t\t{\n\t\t\tif ( step.Kind is not (ArchStepKind.Shell or ArchStepKind.Wing or ArchStepKind.Canopy) )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tplot.Resolve( step.Rect, out var min, out var max );\n\t\t\tArchGhost.Volume( min, max, Owner.LevelHeight, Owner.LevelHeight + Standing( recipe, step ) );\n\t\t}\n\n\t\tArchGhost.Note( new Vector3( plot.Max.x, plot.Max.y, Owner.LevelHeight + 264f ),\n\t\t\t$\"{recipe.Title} \u2014 {plot.Size.x:0} x {plot.Size.y:0}\" );\n\t}\n\n\t// From the one resolve the commit uses, so the units drawn are the units stood.\n\tvoid DrawRowPreview()\n\t{\n\t\tvar row = ResolvedRow( DragStart, DragCurrent );\n\n\t\tif ( !row.IsUsable )\n\t\t{\n\t\t\tDrawRectPreview();\n\t\t\treturn;\n\t\t}\n\n\t\tvar standing = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );\n\t\tvar storey = standing + Owner.Kit.FloorThickness;\n\t\tvar pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;\n\n\t\tforeach ( var bay in row.Bays )\n\t\t{\n\t\t\tvar plate = Owner.LevelHeight + standing + storey * (bay.Storeys - 1);\n\n\t\t\tArchGhost.Volume( bay.Min, bay.Max, Owner.LevelHeight, plate );\n\n\t\t\tif ( withRoof )\n\t\t\t{\n\t\t\t\tArchGhost.Pitch( bay.Min, bay.Max, plate, pitch, roofStyle, ArchBuild.Ridged( ridge, bay.Min, bay.Max ) );\n\t\t\t}\n\t\t}\n\n\t\tvar last = row.Bays[^1];\n\n\t\tArchGhost.Note( new Vector3( last.Max.x, last.Max.y, Owner.LevelHeight + standing ),\n\t\t\trow.RoadId > 0\n\t\t\t\t? $\"{row.Bays.Count} units on the verge \u2014 {row.Frontage:0} x {row.Depth:0}\"\n\t\t\t\t: $\"{row.Bays.Count} units \u2014 {row.Frontage:0} x {row.Depth:0}\" );\n\t}\n\n\tprotected override void OnDrag( Vector2 from, Vector2 to )\n\t{\n\t\tif ( mode == BuildingMode.Prefab )\n\t\t{\n\t\t\tPlacePrefab( from, to );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Rowing )\n\t\t{\n\t\t\tPlaceRow( from, to );\n\t\t\treturn;\n\t\t}\n\n\t\tvar placement = Resolve( from, to );\n\t\tvar min = placement.Min;\n\t\tvar max = placement.Max;\n\n\t\tif ( !placement.IsUsable || Extending && !placement.TouchesHost )\n\t\t{\n\t\t\tLog.Info( Extending\n\t\t\t\t? \"Architecture: an extension must finish against an empty edge of the active building.\"\n\t\t\t\t: \"Architecture: that drag is fully occupied. Start or finish the drag in empty grid space.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tvar rules = Authored();\n\n\t\t// The drag names the building it joins, not the target picker - whose placeholder extends nothing.\n\t\tif ( Extending && placement.Host is { Rooms.Count: > 0 } active )\n\t\t{\n\t\t\tactive.Archetype = Chosen?.Name ?? active.Archetype;\n\n\t\t\tOwner.ActiveBuildingId = active.Id;\n\n\t\t\tvar wing = ArchBuild.Extend(\n\t\t\t\tOwner.Plan, active, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max,\n\t\t\t\twingRoof, eaveDrop, rules.Floor ?? true, rules.Gutters ?? true, ridge );\n\n\t\t\tif ( wing is null )\n\t\t\t{\n\t\t\t\tLog.Info( \"Architecture: that wing has no empty grid space beside the building.\" );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tArchArchetypeRules.Apply( wing, rules, Owner.Kit );\n\n\t\t\tOwner.ActiveRoomId = wing.Room.Id;\n\t\t\tCancelDraft();\n\t\t\tOwner.Commit();\n\t\t\tRemember( wing );\n\n\t\t\treturn;\n\t\t}\n\n\t\tvar building = new ArchBuilding\n\t\t{\n\t\t\tId = Owner.Plan.AllocateId(),\n\t\t\tName = $\"{Chosen?.Title ?? \"Building\"}{Owner.Plan.Buildings.Count + 1}\",\n\t\t\tArchetype = Chosen?.Name ?? \"\",\n\t\t\tGuttersEnabled = rules.Gutters ?? true\n\t\t};\n\n\t\tvar shell = ArchBuild.Shell(\n\t\t\tOwner.Plan, building, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max, rules.Floor ?? true,\n\t\t\twithRoof ? roofStyle : null, rules.Gutters ?? true, ridge );\n\n\t\tif ( shell is null )\n\t\t{\n\t\t\tLog.Info( \"Architecture: that building footprint has no empty grid space.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tArchArchetypeRules.Apply( shell, rules, Owner.Kit );\n\n\t\tOwner.Plan.Units.Add( building );\n\n\t\tif ( Aimable && ArchHandles.Pivot( building, out var about ) )\n\t\t{\n\t\t\tArchCarry.Turn( building, about, Facing );\n\t\t}\n\n\t\tOwner.ActiveBuildingId = building.Id;\n\t\tOwner.ActiveRoomId = shell.Room.Id;\n\t\tFinishDraft( building.Id );\n\t\tOwner.Commit();\n\t\tRemember( shell );\n\t}\n\n\t// The drag rectangle swung about its own centre, which is the pivot the placement turns on too - two\n\t// centres would show the shell in one place and stand it in another.\n\tstatic List<Vector2> Turned( Vector2 min, Vector2 max, float degrees ) => ArchFootprint.Turned( ArchFootprint.Rect( min, max ), (min + max) * 0.5f, degrees );\n\n\tList<Vector2> Turned( Vector2 min, Vector2 max ) => Turned( min, max, Facing );\n\n\tArchRectanglePlacement Resolve( Vector2 from, Vector2 to )\n\t{\n\t\treturn new ArchBoundaryPlacementService( Owner.Plan, Owner.Kit )\n\t\t\t.Outside( Owner.Level, from, to, Extending ? Owner.ActiveBuilding() : null );\n\t}\n\n\tvoid Remember( ArchSection section )\n\t{\n\t\tplacedRoom = section.Room?.Id ?? 0;\n\t\tplacedRoof = section.Roof?.Id ?? 0;\n\t\tplacedStamp = Owner.Plan.NextId;\n\t\tplacedMerged = section.Merged;\n\n\t\tRefresh();\n\t}\n\n\t// Shown in the panel - an untold live edit reads as the tool acting alone.\n\tArchRoom Editing => placedRoom != 0 && Owner.Plan.NextId == placedStamp ? Owner.Plan.FindRoom( placedRoom ) : null;\n\n\t// Edits the last drag's room and roof in place - nothing is re-created.\n\tvoid Restyle()\n\t{\n\t\tif ( placedRoom == 0 || Owner.Plan.NextId != placedStamp )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Owner.Plan.FindRoom( placedRoom ) is not { } room )\n\t\t{\n\t\t\tplacedRoom = 0;\n\t\t\treturn;\n\t\t}\n\n\t\tvar building = Owner.Plan.OwnerOf( room );\n\t\tvar roof = building?.Roofs.FirstOrDefault( part => part.Id == placedRoof );\n\n\t\tif ( building is not null && Chosen is { } chosen )\n\t\t{\n\t\t\tbuilding.Archetype = chosen.Name;\n\t\t}\n\n\t\t// A merged wing shares the section it folded into; style and ridge follow the host.\n\t\tif ( roof is not null && !placedMerged )\n\t\t{\n\t\t\troof.Style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;\n\t\t\troof.RidgeAlongX = ArchBuild.Ridged( ridge, roof.Min, roof.Max );\n\t\t}\n\n\t\tArchArchetypeRules.Apply( new ArchSection { Room = room, Roof = roof, Merged = placedMerged }, Authored(), Owner.Kit );\n\n\t\tOwner.Touch( \"Restyle Section\" );\n\t}\n\n\tvoid Set( Action change )\n\t{\n\t\tchange();\n\t\tRestyle();\n\t}\n\n\t// A choice that decides which rows exist re-lays out only after the standing section has been re-dressed;\n\t// refreshing from inside the change tears down the widget still handling the click.\n\tvoid Relayout( Action change )\n\t{\n\t\tSet( change );\n\t\tRefresh();\n\t}\n\n\t// The type only seeds these - every deciding number is visible and editable.\n\tArchSectionRules Authored()\n\t{\n\t\tvar type = Chosen is { } chosen ? (Extending ? chosen.Wing : chosen.Shell) : new ArchSectionRules();\n\n\t\treturn new ArchSectionRules\n\t\t{\n\t\t\tWallHeight = wallHeight,\n\t\t\tRidge = ridge,\n\t\t\tRoofPitch = roofPitch,\n\t\t\tOverhang = type.Overhang,\n\t\t\tFrameSpacing = type.FrameSpacing,\n\t\t\tFloor = withFloor,\n\t\t\tFoundation = withFoundation,\n\t\t\tCeiling = withCeiling,\n\t\t\tGutters = withGutters,\n\t\t\tFascia = withFascia,\n\t\t\tSoffit = withSoffit,\n\t\t\tFrame = withFrame,\n\t\t\tFloorBoards = type.FloorBoards,\n\t\t\tFloorBoardYaw = type.FloorBoardYaw,\n\t\t\tPalette = type.Palette\n\t\t};\n\t}\n\n\tfloat Standing( ArchArchetype recipe, ArchArchetypeStep step )\n\t{\n\t\tif ( step.Kind == ArchStepKind.Canopy )\n\t\t{\n\t\t\treturn step.HeadHeight;\n\t\t}\n\n\t\tvar type = step.Kind == ArchStepKind.Wing ? recipe.Wing : recipe.Shell;\n\t\tvar height = step.Rules.WallHeight > 1f ? step.Rules.WallHeight : type.WallHeight;\n\n\t\treturn ArchArchetypeRules.Standing( height, Owner.Kit );\n\t}\n\n\tvoid PlacePrefab( Vector2 from, Vector2 to )\n\t{\n\t\tvar recipe = Owner.FindArchetype( archetype );\n\n\t\tif ( recipe is null || !recipe.HasLayout )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: '{archetype}' has no canned layout - use New with the type chosen instead. Types live in Assets/{ArchStorage.ArchetypeDirectory}.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tvar plot = ResolvedPrefabPlot( recipe, from, to, out var usable );\n\n\t\tif ( !usable )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: {recipe.Title} has no empty grid space on that plot.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tvar placed = ArchArchetypeBuild.Place( Owner.Plan, Owner.Kit, recipe, plot, Owner.Level, Owner.LevelHeight, Owner.PillarTypes );\n\n\t\tif ( placed is null )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: {recipe.Title} laid out nothing on that plot.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tforeach ( var note in placed.Skipped )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: {recipe.Title} skipped {note}.\" );\n\t\t}\n\n\t\tOwner.ActiveBuildingId = placed.Building.Id;\n\t\tOwner.ActiveRoomId = placed.Building.Rooms[0].Id;\n\t\tOwner.Commit( $\"Place {recipe.Title}\" );\n\t}\n\n\tArchPlot ResolvedPrefabPlot( ArchArchetype recipe, Vector2 from, Vector2 to, out bool usable )\n\t{\n\t\treturn new ArchBoundaryPlacementService( Owner.Plan, Owner.Kit )\n\t\t\t.PlotOutside( Owner.Level, from, to, recipe.MinimumPlot, flipX, flipY, out usable );\n\t}\n\n\tvoid PlaceRow( Vector2 from, Vector2 to )\n\t{\n\t\tif ( Chosen is not { } type )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: a row takes its unit width from a building type, and none are in Assets/{ArchStorage.ArchetypeDirectory}.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tvar row = ResolvedRow( from, to );\n\n\t\tif ( !row.IsUsable )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: that drag names no frontage a {type.Title} unit fits along. Drag further along the street, or deeper back from it.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tvar placed = ArchRowPlacement.Stand(\n\t\t\tOwner.Plan, Owner.Kit, row, type, Authored(), Owner.Level, Owner.LevelHeight,\n\t\t\twithRoof ? roofStyle : null, ridge );\n\n\t\tif ( placed is null )\n\t\t{\n\t\t\tLog.Info( \"Architecture: every bay of that row landed on occupied ground.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( row.Blocked > 0 )\n\t\t{\n\t\t\tLog.Info( $\"Architecture: {row.Blocked} of the row's bays stood on occupied ground and were left out.\" );\n\t\t}\n\n\t\tOwner.ActiveBuildingId = placed.Buildings[0].Id;\n\t\tOwner.ActiveRoomId = placed.Buildings[0].Rooms[0].Id;\n\t\tCancelDraft();\n\t\tOwner.Commit( $\"Place Street Row of {placed.Buildings.Count}\" );\n\t}\n\n\t// The road comes from the tool's own memoised lookup - Nearest walks a whole dense curve, and the ghost asks per frame.\n\tArchRowShape ResolvedRow( Vector2 from, Vector2 to )\n\t{\n\t\tvar road = Owner.RoadAt( from, ArchRowPlacement.Verge, out _ ) ?? Owner.RoadAt( to, ArchRowPlacement.Verge, out _ );\n\n\t\treturn ArchRowPlacement.Resolve(\n\t\t\tOwner.Plan, Owner.Kit, Chosen, Owner.Level, from, to,\n\t\t\troad, road is null ? null : Owner.Resolved( road.Id, road.Curve ),\n\t\t\twithRoof && roofStyle == RoofStyle.Flat );\n\t}\n\n\t// The layout drawing leads the panel, because the shape of a shell already standing is the one thing this tool's\n\t// drag cannot go back and fix. Nothing to work on until a building stands, so it says so rather than opening empty.\n\tvoid Layout( ToolSidebarWidget panel )\n\t{\n\t\tvar building = Owner.ActiveBuilding();\n\t\tvar button = new Button.Primary( \"Toggle Building Layout\", \"architecture\" )\n\t\t{\n\t\t\tClicked = () => ArchBuildingWindow.Toggle( Owner, building )\n\t\t};\n\n\t\tbutton.Enabled = building is not null;\n\t\tbutton.ToolTip = building is null\n\t\t\t? \"Drag a shell first \u2014 the layout drawing works on a building that stands\"\n\t\t\t: $\"The plan of {building.Name}, storey by storey: move a corner, push a run, cut a corner off at an angle\";\n\n\t\tpanel.AddGroup( \"Layout\" ).AddRow().Add( button );\n\t}\n\n\t// The gesture as a sequence: which placement, what is being placed, what the shell carries, what covers it,\n\t// its numbers, and the bearing it stands at. All of those grids standing open at once is what made the first\n\t// tool on the shelf a wall of icons.\n\t//\n\t// The layout drawing leads it from OUTSIDE the sequence, because it works on a shell already standing -\n\t// the one shape the next drag cannot go back and fix.\n\tprotected override void BuildOptions( ToolSidebarWidget panel )\n\t{\n\t\tLayout( panel );\n\n\t\tInherit();\n\n\t\t// Asked whether or not the shelf is the step being shown: the stamp following the cursor is read off\n\t\t// this list, and a closed step never builds.\n\t\tif ( Stamping && (savedOffered is null || savedStamp != ArchLayerAssets.Revision) )\n\t\t{\n\t\t\tRereadSaved();\n\t\t}\n\n\t\tif ( Editing is { } room )\n\t\t{\n\t\t\tpanel.Layout.Add( ArchSidebarLayout.Advice( $\"Editing {room.Name} live. Place anything else and these go back to seeding the next drag.\" ) );\n\t\t}\n\n\t\tusing var flow = ArchWorkflow.In( panel, Scope( \"flow\" ), Refresh );\n\n\t\tflow.Step( \"Mode\", ModeName, ModeGlyph, PickMode );\n\n\t\tif ( Stamping )\n\t\t{\n\t\t\tflow.Step( \"Saved building\", saved?.Title ?? \"None\", \"inventory_2\", step => Stamps( panel, step ) );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tflow.Step( \"Type\", Chosen?.Title ?? \"None\", \"domain\", step => Types( panel, step ) );\n\t\t}\n\n\t\t// A canned layout decides its own steps and would be contradicted by the shell's, and a stamp stands\n\t\t// exactly what it was saved as - so neither is asked anything past what it is and which way it faces.\n\t\tif ( mode == BuildingMode.Prefab )\n\t\t{\n\t\t\tflow.Step( \"Mirror\", MirrorName, \"flip\", PickMirror );\n\t\t}\n\t\telse if ( !Stamping )\n\t\t{\n\t\t\tflow.Step( \"Include\", IncludeName, \"checklist\", PickInclude );\n\n\t\t\tif ( Covered )\n\t\t\t{\n\t\t\t\tflow.Step( Extending ? \"Wing roof\" : \"Roof\", RoofName, RoofGlyph, PickRoof );\n\t\t\t}\n\n\t\t\tif ( Wants( ArchOptionGroup.Section ) )\n\t\t\t{\n\t\t\t\tflow.Step( \"Structure\", StructureName, \"straighten\", BuildStructure );\n\t\t\t}\n\t\t}\n\n\t\t// Last wherever it is asked. A bearing is typed rather than picked, so the step can never say it has\n\t\t// been answered - anywhere but the end it stops the sequence dead on a box nobody has to fill in.\n\t\tif ( Aimable )\n\t\t{\n\t\t\tflow.Step( \"Bearing\", BearingName, \"explore\", BuildBearing );\n\t\t}\n\t}\n\n\t// Whether a roof is asked about at all - a wing answers through its type's own rules, and a shell whose\n\t// roof was switched off in Include has nothing left to style.\n\tbool Covered => Extending ? Wants( ArchOptionGroup.WingRoof ) : withRoof;\n\n\tbool Wants( ArchOptionGroup group ) => Chosen?.Wants( group ) ?? true;\n\n\tstring ModeName => mode switch\n\t{\n\t\tBuildingMode.Extend => \"Extend\",\n\t\tBuildingMode.Prefab => \"Canned layout\",\n\t\tBuildingMode.Row => \"Street row\",\n\t\tBuildingMode.Saved => \"Saved building\",\n\t\t_ => \"New building\"\n\t};\n\n\tstring ModeGlyph => mode switch\n\t{\n\t\tBuildingMode.Extend => \"add_home_work\",\n\t\tBuildingMode.Prefab => \"auto_awesome_motion\",\n\t\tBuildingMode.Row => \"view_column\",\n\t\tBuildingMode.Saved => \"inventory_2\",\n\t\t_ => \"domain_add\"\n\t};\n\n\tvoid PickMode( ArchWorkflowStep step )\n\t{\n\t\tusing var grid = ArchIconGrid.In( step.Layout );\n\n\t\tMode( grid, step, BuildingMode.New, \"mode_new_building\", \"New building \u2014 drag the outer shell\", \"domain_add\" );\n\t\tMode( grid, step, BuildingMode.Extend, \"mode_extend_building\", \"Extend the active building \u2014 drag a wing onto it\", \"add_home_work\" );\n\t\tMode( grid, step, BuildingMode.Prefab, \"mode_archetype\", \"Place the type's canned layout \u2014 one drag lays the whole unit out\", \"auto_awesome_motion\" );\n\t\tMode( grid, step, BuildingMode.Row, \"mode_street_row\", \"Street row \u2014 drag the frontage and its depth; one party-walled unit per bay, at the type's own unit width\", \"view_column\" );\n\t\tMode( grid, step, BuildingMode.Saved, \"mode_saved_building\", \"Saved building \u2014 stamp one you authored earlier back down, exactly as it stands\", \"inventory_2\" );\n\t}\n\n\tvoid Mode( ArchIconGrid grid, ArchWorkflowStep step, BuildingMode choice, string slug, string tooltip, string fallback )\n\t{\n\t\tgrid.Pick( tooltip, slug, fallback, mode == choice, () =>\n\t\t{\n\t\t\tmode = choice;\n\t\t\t// Cleared, not reseeded - Extend picks up the active building's own type first.\n\t\t\tseeded = false;\n\n\t\t\tstep.Chose();\n\t\t} );\n\t}\n\n\tstring MirrorName => flipX && flipY ? \"X and Y\" : flipX ? \"Across X\" : flipY ? \"Across Y\" : \"None\";\n\n\tvoid PickMirror( ArchWorkflowStep step )\n\t{\n\t\tusing var grid = ArchIconGrid.In( step.Layout );\n\n\t\tgrid.Toggle( \"Mirror the layout across X\", \"flip_x\", \"swap_horiz\", flipX, value => flipX = value );\n\t\tgrid.Toggle( \"Mirror the layout across Y\", \"flip_y\", \"swap_vert\", flipY, value => flipY = value );\n\t}\n\n\tstring IncludeName\n\t{\n\t\tget\n\t\t{\n\t\t\tvar carried = new List<string>();\n\n\t\t\tif ( Wants( ArchOptionGroup.Foundation ) )\n\t\t\t{\n\t\t\t\tcarried.Add( withFoundation ? \"Raised\" : \"Nested\" );\n\t\t\t}\n\n\t\t\tif ( withFloor )\n\t\t\t{\n\t\t\t\tcarried.Add( \"floor\" );\n\t\t\t}\n\n\t\t\tif ( Roofed && withGutters )\n\t\t\t{\n\t\t\t\tcarried.Add( \"gutters\" );\n\t\t\t}\n\n\t\t\tif ( !Extending && !withRoof )\n\t\t\t{\n\t\t\t\tcarried.Add( \"no roof\" );\n\t\t\t}\n\n\t\t\treturn carried.Count > 0 ? string.Join( \", \", carried ) : \"Shell only\";\n\t\t}\n\t}\n\n\t// Foundation and the include flags are one question - what this shell is made of - and were two boxes of\n\t// icons asking it. Two grids inside one step, because a Pick is exclusive within its own grid.\n\tvoid PickInclude( ArchWorkflowStep step )\n\t{\n\t\tif ( Wants( ArchOptionGroup.Foundation ) )\n\t\t{\n\t\t\tusing var footing = ArchIconGrid.In( step.Layout );\n\n\t\t\tfooting.Pick( \"Raised foundation \u2014 the plinth stands proud of grade and the building sits up on it\",\n\t\t\t\t\"foundation_raised\", \"vertical_align_top\", withFoundation, () => Answer( () => withFoundation = true, step ) );\n\n\t\t\tfooting.Pick( \"Nested foundation \u2014 the footing is buried and the floor sits on grade\",\n\t\t\t\t\"foundation_nested\", \"vertical_align_bottom\", !withFoundation, () => Answer( () => withFoundation = false, step ) );\n\t\t}\n\n\t\tusing var grid = ArchIconGrid.In( step.Layout );\n\n\t\tgrid.Toggle( \"Floor slab\", \"opt_floor_slab\", \"layers\", withFloor, value => Set( () => withFloor = value ) );\n\n\t\tif ( Roofed )\n\t\t{\n\t\t\tgrid.Toggle( \"Gutters and downpipes\", \"opt_gutters\", \"water_damage\", withGutters, value => Set( () => withGutters = value ) );\n\t\t}\n\n\t\tif ( !Extending )\n\t\t{\n\t\t\tgrid.Toggle( \"Roof\", \"opt_roof\", \"roofing\", withRoof, value => Relayout( () => withRoof = value ) );\n\t\t}\n\t}\n\n\tstring RoofName => Extending ? wingRoof.ToString() : roofStyle.ToString();\n\n\tstring RoofGlyph => Extending ? Fallback( wingRoof ) : ArchIcons.RoofStyleGlyph( roofStyle );\n\n\tvoid PickRoof( ArchWorkflowStep step )\n\t{\n\t\tif ( Extending )\n\t\t{\n\t\t\tPickWingRoof( step );\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Wants( ArchOptionGroup.RoofStyle ) )\n\t\t{\n\t\t\tusing var grid = ArchIconGrid.In( step.Layout );\n\n\t\t\tforeach ( var value in Enum.GetValues<RoofStyle>() )\n\t\t\t{\n\t\t\t\tvar captured = value;\n\n\t\t\t\tgrid.Pick( captured.ToString(), ArchIcons.RoofStyleSlug( captured ), ArchIcons.RoofStyleGlyph( captured ), roofStyle == captured,\n\t\t\t\t\t() => Answer( () => roofStyle = captured, step ) );\n\t\t\t}\n\t\t}\n\n\t\tRidge( step.Layout );\n\t}\n\n\tvoid PickWingRoof( ArchWorkflowStep step )\n\t{\n\t\tusing ( var grid = ArchIconGrid.In( step.Layout ) )\n\t\t{\n\t\t\tforeach ( var value in Enum.GetValues<SectionRoof>() )\n\t\t\t{\n\t\t\t\tvar captured = value;\n\n\t\t\t\tgrid.Pick( Describe( captured ), $\"wing_{captured}\".ToLowerInvariant(), Fallback( captured ), wingRoof == captured,\n\t\t\t\t\t() => Answer( () => wingRoof = captured, step ) );\n\t\t\t}\n\t\t}\n\n\t\t// Continue takes the host's eave, and no roof has no eave to drop.\n\t\tif ( wingRoof is not (SectionRoof.Continue or SectionRoof.None) )\n\t\t{\n\t\t\tstep.Layout.Add( ArchPartUi.Number( \"Eave drop\", eaveDrop, 0f, value => Set( () => eaveDrop = value ) ) );\n\t\t}\n\n\t\tRidge( step.Layout );\n\t}\n\n\t// Advancing IS the re-lay-out: the answer just given decides which rows exist below it, and refreshing from\n\t// inside the change would tear down the widget still handling the click.\n\tvoid Answer( Action change, ArchWorkflowStep step )\n\t{\n\t\tSet( change );\n\n\t\tstep.Chose();\n\t}\n\n\tstring StructureName => $\"{ArchArchetypeRules.Standing( wallHeight, Owner.Kit ):0} high\";\n\n\t// The type's own numbers, editable - a wing must match the shell it joins.\n\tvoid BuildStructure( ArchWorkflowStep step )\n\t{\n\t\tstep.Layout.Add( ArchPartUi.Number( \"Wall height\", wallHeight, 0f, value => Set( () => wallHeight = value ) ) );\n\n\t\t// Pitch describes a deck; with no roof over the section there is none.\n\t\tif ( Roofed )\n\t\t{\n\t\t\tstep.Layout.Add( ArchPartUi.Number( \"Roof pitch\", roofPitch, 0f, value => Set( () => roofPitch = value ) ) );\n\t\t}\n\n\t\tusing var grid = ArchIconGrid.In( step.Layout );\n\n\t\tgrid.Toggle( \"Ceiling under the roof\", \"opt_ceiling\", \"square\", withCeiling, value => Set( () => withCeiling = value ) );\n\n\t\tif ( !Roofed )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tgrid.Toggle( \"Exposed rafters and purlins under the deck\", \"opt_roof_frame\", \"reorder\", withFrame, value => Set( () => withFrame = value ) );\n\t\tgrid.Toggle( \"Fascia along the eaves\", \"opt_fascia\", \"border_bottom\", withFascia, value => Set( () => withFascia = value ) );\n\t\tgrid.Toggle( \"Lined soffit under the overhang\", \"opt_soffit\", \"flip_to_back\", withSoffit, value => Set( () => withSoffit = value ) );\n\t}\n\n\tstring BearingName => MathF.Abs( Facing ) < 0.001f ? \"Square on\" : $\"{Facing:0.##}\u00b0\";\n\n\t// What the next drag stands at. R still turns quarters on top of it while a stamp follows the cursor, so\n\t// the box is the bearing and the key is the corner.\n\tvoid BuildBearing( ArchWorkflowStep step )\n\t{\n\t\tstep.Layout.Add( ArchPartUi.Angle( \"Angle\", placementAngle, value => placementAngle = value ) );\n\t\tstep.Layout.Add( ArchPartUi.Increment( Refresh ) );\n\t}\n\n\t// Picking re-types it - being unable to change your mind is worse than a mismatch. The designer rides the\n\t// browser's own header, where every other authored kit lives; a full-width button of its own put the way\n\t// into the designer above the thing it designs.\n\t//\n\t// The shelf and the designer hang off the SIDEBAR rather than off the step, because a step's widget is torn\n\t// down on the next refresh and a modal parented to one would go with it.\n\tvoid Types( ToolSidebarWidget panel, ArchWorkflowStep step )\n\t{\n\t\tvar offered = Offered();\n\t\tvar removed = ArchStorage.HiddenArchetypes();\n\n\t\tif ( offered.Count == 0 && removed.Count == 0 )\n\t\t{\n\t\t\tstep.Layout.Add( ArchSidebarLayout.Advice( mode == BuildingMode.Prefab\n\t\t\t\t? \"No building type carries a canned layout. Use New with a type chosen instead.\"\n\t\t\t\t: $\"No building types. Authored ones live in Assets/{ArchStorage.ArchetypeDirectory}.\" ) );\n\n\t\t\treturn;\n\t\t}\n\n\t\tvar shelf = new ArchPresetBrowser<ArchArchetype>( panel, Owner.Kit, Scope( \"types\" ), \"Building types\" )\n\t\t{\n\t\t\tChosen = type => string.Equals( archetype, type.Name, StringComparison.OrdinalIgnoreCase ),\n\t\t\tChoose = type => Adopt( type, step ),\n\t\t\tDesign = () => new ArchBuildingDesigner( panel, Owner, Chosen, designed => ApplyDesigned( designed, step ) ).Show(),\n\t\t\tReload = Reread,\n\t\t\tRemove = item => Remove( item.Value ),\n\t\t\tRestore = () =>\n\t\t\t{\n\t\t\t\tArchStorage.RestoreArchetypes();\n\t\t\t\tReread();\n\t\t\t}\n\t\t};\n\n\t\tshelf.DesignButton.Visible = true;\n\t\tshelf.DesignButton.ToolTip = \"Design a building type\";\n\t\tshelf.ReloadButton.Visible = true;\n\t\tshelf.RestoreButton.Visible = removed.Count > 0;\n\t\tshelf.RestoreButton.ToolTip = $\"Bring back {string.Join( \", \", removed )}\";\n\t\tshelf.Set( offered );\n\n\t\tstep.Add( shelf );\n\t}\n\n\tvoid Stamps( ToolSidebarWidget panel, ArchWorkflowStep step )\n\t{\n\t\tif ( savedOffered.Count == 0 )\n\t\t{\n\t\t\tstep.Layout.Add( ArchSidebarLayout.Advice( \"No saved buildings yet. Right-click a building in the Plan Layers stack and Save, and it turns up here.\" ) );\n\n\t\t\treturn;\n\t\t}\n\n\t\tvar shelf = new ArchPresetBrowser<ArchBuildingAsset>( panel, Owner.Kit, Scope( \"stamps\" ), \"Saved buildings\" )\n\t\t{\n\t\t\tChosen = asset => saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ),\n\t\t\tChoose = asset =>\n\t\t\t{\n\t\t\t\tHold( asset );\n\n\t\t\t\tstep.Chose();\n\t\t\t},\n\t\t\tReload = RereadSaved,\n\t\t\tRemove = item => Discard( item.Value )\n\t\t};\n\n\t\tshelf.ReloadButton.Visible = true;\n\t\tshelf.Set( savedOffered );\n\n\t\tstep.Add( shelf );\n\t}\n\n\tList<ArchPresetItem<ArchBuildingAsset>> Kept()\n\t{\n\t\treturn ArchLayerAssets.Buildings()\n\t\t\t.Select( asset => new ArchPresetItem<ArchBuildingAsset>\n\t\t\t{\n\t\t\t\tValue = asset,\n\t\t\t\tName = asset.Title,\n\t\t\t\tDetail = ArchBuildingStamp.Describe( asset ),\n\t\t\t\tIdentity = ArchBuildingStamp.Identity( asset ),\n\t\t\t\tCategory = \"Saved\",\n\t\t\t\tGlyph = \"home_work\",\n\t\t\t\tTags = asset.Name,\n\t\t\t\tView = ArchPresetPreview.Quarter,\n\t\t\t\tFocus = ArchPreviewFocus.Whole,\n\t\t\t\tRecipe = () => ArchBuildingStamp.Stage( asset )\n\t\t\t} )\n\t\t\t.ToList();\n\t}\n\n\tvoid Discard( ArchBuildingAsset asset )\n\t{\n\t\tif ( !ArchLayerAssets.RemoveBuilding( asset.Name ) )\n\t\t{\n\t\t\tLog.Warning( $\"Architecture: could not remove the saved building '{asset.Title}'.\" );\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ) )\n\t\t{\n\t\t\tHold( null );\n\t\t}\n\n\t\tRereadSaved();\n\t\tRefresh();\n\t}\n\n\t// The outlines are worked out here and nowhere else, because the hover asks for them every frame.\n\tvoid Hold( ArchBuildingAsset asset )\n\t{\n\t\tsaved = asset;\n\t\tsavedGhost = asset is null ? null : ArchBuildingStamp.Ghost( asset );\n\t}\n\n\tvoid RereadSaved()\n\t{\n\t\tvar chosen = saved?.Name;\n\n\t\tsavedStamp = ArchLayerAssets.Revision;\n\t\tsavedOffered = Kept();\n\n\t\tHold( savedOffered.Select( item => item.Value ).FirstOrDefault( asset => string.Equals( asset.Name, chosen, StringComparison.OrdinalIgnoreCase ) ) );\n\t}\n\n\tvoid Reread()\n\t{\n\t\tOwner.ReloadArchetypes();\n\t\tSeed();\n\t\tRefresh();\n\t}\n\n\tvoid Remove( ArchArchetype type )\n\t{\n\t\tif ( !ArchStorage.RemoveArchetype( type.Name ) )\n\t\t{\n\t\t\tLog.Warning( $\"Architecture: could not remove the building type '{type.Name}'.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tOwner.ReloadArchetypes();\n\n\t\t// A drag reads its heights off the chosen type, so removing that one has to leave another standing.\n\t\tif ( Owner.FindArchetype( archetype ) is null )\n\t\t{\n\t\t\tarchetype = Owner.Archetypes.FirstOrDefault()?.Name ?? \"\";\n\t\t}\n\n\t\tSeed();\n\t\tRefresh();\n\t}\n\n\t// A canned layout is the whole gesture in Prefab mode, so a type without one is not a choice there.\n\tList<ArchPresetItem<ArchArchetype>> Offered()\n\t{\n\t\treturn Owner.Archetypes\n\t\t\t.Where( type => mode != BuildingMode.Prefab || type.HasLayout )\n\t\t\t.Select( type => new ArchPresetItem<ArchArchetype>\n\t\t\t{\n\t\t\t\tValue = type,\n\t\t\t\tName = type.Title,\n\t\t\t\tDetail = ArchArchetypeStage.Describe( type ),\n\t\t\t\tIdentity = ArchArchetypeStage.Identity( type ),\n\t\t\t\tCategory = type.HasLayout ? \"Has layout\" : \"Shell\",\n\t\t\t\tBadge = type.HasLayout ? \"layout\" : null,\n\t\t\t\tGlyph = type.Icon,\n\t\t\t\tTags = $\"{type.Name} {type.Description}\",\n\t\t\t\tView = ArchPresetPreview.Quarter,\n\t\t\t\tFocus = ArchPreviewFocus.Whole,\n\t\t\t\tRecipe = () => ArchArchetypeStage.Of( type, Owner.Kit )\n\t\t\t} )\n\t\t\t.ToList();\n\t}\n\n\t// Shown once - re-reading it every rebuild would undo the re-type pick.\n\tvoid Inherit()\n\t{\n\t\tif ( !Extending || seeded )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Owner.ActiveBuilding()?.Archetype is { Length: > 0 } existing && Owner.FindArchetype( existing ) is not null )\n\t\t{\n\t\t\tarchetype = existing;\n\t\t}\n\n\t\tSeed();\n\t}\n\n\t// Seeds every switch the type has an opinion about, so the sidebar matches the drag.\n\tvoid Adopt( ArchArchetype type, ArchWorkflowStep step )\n\t{\n\t\tarchetype = type.Name;\n\n\t\tSeed();\n\t\tRestyle();\n\n\t\tstep.Chose();\n\t}\n\n\t// Adopts like a grid pick, then re-dresses the standing section.\n\tvoid ApplyDesigned( ArchArchetype designed, ArchWorkflowStep step )\n\t{\n\t\tarchetype = designed.Name;\n\n\t\tOwner.RegisterArchetype( designed );\n\t\tSeed();\n\t\tRestyle();\n\n\t\tstep.Chose();\n\t}\n\n\tvoid Seed()\n\t{\n\t\tif ( Chosen is not { } type )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar rules = Extending ? type.Wing : type.Shell;\n\n\t\tif ( rules.Roof is { } style )\n\t\t{\n\t\t\troofStyle = style;\n\t\t}\n\n\t\twingRoof = type.WingRoof;\n\t\teaveDrop = type.WingEaveDrop;\n\t\twallHeight = rules.WallHeight;\n\t\troofPitch = rules.RoofPitch;\n\t\twithGutters = rules.Gutters ?? true;\n\t\twithFloor = rules.Floor ?? true;\n\t\twithFoundation = rules.Foundation ?? true;\n\t\twithFascia = rules.Fascia ?? true;\n\t\twithSoffit = rules.Soffit ?? true;\n\t\twithCeiling = rules.Ceiling ?? false;\n\t\twithFrame = rules.Frame ?? false;\n\n\t\tseeded = true;\n\t}\n\n\t// Its own grid inside whichever roof section called it - a ridge direction and a roof style are two\n\t// exclusive choices, and sharing one grid would unlight the style when a direction was picked.\n\tvoid Ridge( Layout roof )\n\t{\n\t\tif ( !Wants( ArchOptionGroup.Ridge ) || !Roofed\n\t\t\t|| !ArchRoofPlane.Ridged( Extending ? ArchBuild.Winged( wingRoof ) : roofStyle ) )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tusing var grid = ArchIconGrid.In( roof );\n\n\t\tforeach ( var value in Enum.GetValues<RidgeRun>() )\n\t\t{\n\t\t\tvar captured = value;\n\n\t\t\tgrid.Pick( ArchIcons.RidgeAdvice( captured ), ArchIcons.RidgeSlug( captured ), ArchIcons.RidgeGlyph( captured ), ridge == captured,\n\t\t\t\t() => Set( () => ridge = captured ) );\n\t\t}\n\t}\n\n\tstatic string Fallback( SectionRoof choice ) => choice switch\n\t{\n\t\tSectionRoof.Continue => \"merge_type\",\n\t\tSectionRoof.Hip => \"roofing\",\n\t\tSectionRoof.Gable => \"change_history\",\n\t\tSectionRoof.Capped => \"crop_din\",\n\t\t_ => \"block\"\n\t};\n\n\tstatic string Describe( SectionRoof choice ) => choice switch\n\t{\n\t\tSectionRoof.Continue => \"Continue the roof (one hip, valleys)\",\n\t\tSectionRoof.Hip => \"Own hip, stepped down\",\n\t\tSectionRoof.Gable => \"Own gable, stepped down\",\n\t\tSectionRoof.Capped => \"Flat with a parapet cap\",\n\t\t_ => \"No roof\"\n\t};\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Tool/Subtools/ArchOpeningUi.cs",
            "FileName": "ArchOpeningUi.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Linq;\nusing Editor;\n\nnamespace Sunless.Architecture;\n\n// The drag options both opening tools share, laid out once so the two sidebars cannot drift apart.\npublic static class ArchOpeningUi\n{\n\t// The whole furniture control: one exclusive pick in the grid the kind's other options already stand in.\n\t// What a kind may carry comes from ArchOpeningKinds and the pitch, proud and frame are kit stock, so there\n\t// is nothing else here to offer.\n\tpublic static void Furniture( ArchIconGrid grid, OpeningKind kind, OpeningFurniture fitted, Action<OpeningFurniture> chosen )\n\t{\n\t\tvar offered = kind.Furnishings().ToList();\n\n\t\tif ( offered.Count == 0 )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tgrid.Pick( Advice( OpeningFurniture.None ), Slug( OpeningFurniture.None ), Glyph( OpeningFurniture.None ),\n\t\t\tfitted == OpeningFurniture.None, () => chosen( OpeningFurniture.None ) );\n\n\t\tforeach ( var furniture in offered )\n\t\t{\n\t\t\tvar picked = furniture;\n\n\t\t\tgrid.Pick( Advice( picked ), Slug( picked ), Glyph( picked ), fitted == picked, () => chosen( picked ) );\n\t\t}\n\t}\n\n\t// What stands OVER the head, offered in the same grid the furniture is: both are things a hole wears, and a\n\t// second panel for the one above it would only be the same control twice.\n\tpublic static void Hood( ArchIconGrid grid, OpeningKind kind, OpeningHood worn, Action<OpeningHood> chosen )\n\t{\n\t\tvar offered = kind.Hoods().ToList();\n\n\t\tif ( offered.Count == 0 )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tgrid.Pick( Advice( OpeningHood.None ), Slug( OpeningHood.None ), Glyph( OpeningHood.None ),\n\t\t\tworn == OpeningHood.None, () => chosen( OpeningHood.None ) );\n\n\t\tforeach ( var hood in offered )\n\t\t{\n\t\t\tvar picked = hood;\n\n\t\t\tgrid.Pick( Advice( picked ), Slug( picked ), Glyph( picked ), worn == picked, () => chosen( picked ) );\n\t\t}\n\t}\n\n\tstatic string Slug( OpeningHood hood ) => $\"opt_hood_{hood}\".ToLowerInvariant();\n\n\tstatic string Advice( OpeningHood hood ) => hood switch\n\t{\n\t\tOpeningHood.Cornice => \"Moulded hood on corbels over the head\",\n\t\tOpeningHood.Pediment => \"Hood under a gable over the head\",\n\t\t_ => \"Bare head\"\n\t};\n\n\tstatic string Glyph( OpeningHood hood ) => hood switch\n\t{\n\t\tOpeningHood.Cornice => \"horizontal_split\",\n\t\tOpeningHood.Pediment => \"change_history\",\n\t\t_ => \"block\"\n\t};\n\n\t// Named for the answer line a workflow row reads back, so the tile and the closed step agree.\n\tpublic static string Name( OpeningFurniture furniture ) => furniture switch\n\t{\n\t\tOpeningFurniture.Bars => \"Burglar bars\",\n\t\tOpeningFurniture.Boarded => \"Boarded over\",\n\t\tOpeningFurniture.Shutter => \"Roller shutter\",\n\t\tOpeningFurniture.Gate => \"Security gate\",\n\t\tOpeningFurniture.Leaves => \"Louvred shutters\",\n\t\t_ => \"Nothing\"\n\t};\n\n\tstatic string Slug( OpeningFurniture furniture ) => $\"opt_furniture_{furniture}\".ToLowerInvariant();\n\n\tstatic string Advice( OpeningFurniture furniture ) => furniture switch\n\t{\n\t\tOpeningFurniture.Bars => \"Burglar bars across it\",\n\t\tOpeningFurniture.Boarded => \"Boarded over with planks\",\n\t\tOpeningFurniture.Shutter => \"Roller shutter down over it\",\n\t\tOpeningFurniture.Gate => \"Security gate across it\",\n\t\tOpeningFurniture.Leaves => \"Louvred shutters hinged either side\",\n\t\t_ => \"Nothing over it\"\n\t};\n\n\t// Classic Material Icons only - a glyph the editor's older set does not know draws an empty box.\n\tpublic static string Glyph( OpeningFurniture furniture ) => furniture switch\n\t{\n\t\tOpeningFurniture.Bars => \"fence\",\n\t\tOpeningFurniture.Boarded => \"table_rows\",\n\t\tOpeningFurniture.Shutter => \"density_small\",\n\t\tOpeningFurniture.Gate => \"grid_on\",\n\t\tOpeningFurniture.Leaves => \"menu_open\",\n\t\t_ => \"block\"\n\t};\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Tool/Subtools/ArchPorchSubtool.cs",
            "FileName": "ArchPorchSubtool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n[Title( \"Porch\" ), Icon( \"deck\" ), Group( \"13\" )]\npublic sealed class ArchPorchSubtool( ArchTool owner ) : ArchSubtool( owner )\n{\n\tprotected override ArchKind? DraftKind => ArchKind.Porch;\n\n\tpublic override ArchSurface[] Surfaces => new[] { ArchSurface.Deck, ArchSurface.Railing, ArchSurface.Baseboard, ArchSurface.Roof, ArchSurface.Soffit };\n\n\treadonly ArchPorchPart draft = new() { GradeHeight = -24f };\n\n\tprotected override string Title() => \"Porch\";\n\n\tprotected override string Advice() => \"Drag the deck over the wall it hangs off.\";\n\n\tpublic override void OnEnabled()\n\t{\n\t\tbase.OnEnabled();\n\n\t\tArchWorkflow.Restart( Scope( \"flow\" ) );\n\t}\n\n\t// Resolved from the shape, not the drag, so the ghost matches the generator.\n\tprotected override void DrawPreview()\n\t{\n\t\tvar min = Min( DragStart, DragCurrent );\n\t\tvar max = Max( DragStart, DragCurrent );\n\t\tvar room = Owner.RoomAt( (min + max) * 0.5f, out var building );\n\t\tvar deck = Owner.LevelHeight;\n\t\tvar legs = ArchPorch.Preview( building, room, Owner.Kit, min, max, draft.Standing, out var standing, out var joining );\n\n\t\tif ( legs.Count == 0 )\n\t\t{\n\t\t\tArchGhost.Plate( min, max, deck, 8 );\n\t\t\tArchGhost.Note( new Vector3( max.x, max.y, deck + 60f ), \"porch \u2014 nowhere to stand a deck there\" );\n\t\t\treturn;\n\t\t}\n\n\t\tvar sketch = Sketch( legs, standing );\n\t\tvar shape = ArchPorchShape.Resolve( sketch, room, building, Owner.Kit );\n\t\tvar head = shape.Head;\n\t\tvar rail = deck + Owner.Kit.PorchRailHeight;\n\n\t\tforeach ( var leg in legs )\n\t\t{\n\t\t\tif ( draft.Deck )\n\t\t\t{\n\t\t\t\tArchGhost.Volume( leg.Min, leg.Max, deck - draft.DeckDrop, deck );\n\t\t\t}\n\n\t\t\tArchGhost.Plate( leg.Min, leg.Max, deck, 8 );\n\t\t}\n\n\t\tforeach ( var bay in ArchPorchGen.Bays( shape, sketch, Owner.Kit ) )\n\t\t{\n\t\t\tif ( draft.Posts )\n\t\t\t{\n\t\t\t\tArchGhost.Post( bay.From.Point, Owner.Kit.PorchPostSize, deck, head );\n\n\t\t\t\tif ( bay.Last )\n\t\t\t\t{\n\t\t\t\t\tArchGhost.Post( bay.To.Point, Owner.Kit.PorchPostSize, deck, head );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( draft.Railings && !bay.Gated )\n\t\t\t{\n\t\t\t\tGizmo.Draw.Line( bay.From.Raised.WithZ( rail ), bay.To.Raised.WithZ( rail ) );\n\t\t\t}\n\t\t}\n\n\t\tif ( draft.Roofed )\n\t\t{\n\t\t\tArchGhost.Ring( shape.Footprint, head + Owner.Kit.PorchBeamDepth );\n\t\t}\n\n\t\tArchGhost.Note( new Vector3( max.x, max.y, head ), Note( legs, standing, joining ) );\n\t}\n\n\tstatic string Note( IReadOnlyList<ArchPorchLeg> legs, PorchStanding standing, ArchPorchPart joining )\n\t{\n\t\tif ( joining is not null )\n\t\t{\n\t\t\treturn $\"porch \u2014 joining {joining.Name}\";\n\t\t}\n\n\t\tif ( standing == PorchStanding.Free )\n\t\t{\n\t\t\treturn \"porch \u2014 free-standing, no wall to lean on\";\n\t\t}\n\n\t\treturn legs.Count > 1 ? $\"porch \u2014 {legs.Count} legs, wrapping the corner\" : \"porch\";\n\t}\n\n\t// Unattached to the plan, so the ghost resolves exactly what Attach will - the gates in its railing\n\t// included, which is why it carries the flight the placement is about to seed.\n\tArchPorchPart Sketch( List<ArchPorchLeg> legs, PorchStanding standing )\n\t{\n\t\tvar deck = Owner.LevelHeight;\n\n\t\tvar sketch = new ArchPorchPart\n\t\t{\n\t\t\tLegs = legs,\n\t\t\tStanding = standing,\n\t\t\tBaseHeight = deck,\n\t\t\tGradeHeight = deck - draft.DeckDrop,\n\t\t\tHeadHeight = MathF.Max( 60f, draft.HeadHeight ),\n\t\t\tPostSpacing = MathF.Max( 32f, Owner.Kit.PorchPostSize * 8f ),\n\t\t\tDeck = draft.Deck,\n\t\t\tPosts = draft.Posts,\n\t\t\tRafters = draft.Rafters,\n\t\t\tBraces = draft.Braces,\n\t\t\tPitch = draft.Pitch,\n\t\t\tRailings = draft.Railings,\n\t\t\tSteps = draft.Steps,\n\t\t\tRoofed = draft.Roofed\n\t\t};\n\n\t\treturn sketch;\n\t}\n\n\tprotected override void OnDrag( Vector2 from, Vector2 to )\n\t{\n\t\tvar min = Min( from, to );\n\t\tvar max = Max( from, to );\n\n\t\tif ( (max - min).Length < 24f )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar room = Owner.RoomAt( (min + max) * 0.5f, out var building );\n\t\tvar placement = ArchPorch.Attach( Owner.Plan, building, room, Owner.Kit, min, max, draft );\n\n\t\tif ( placement is null )\n\t\t{\n\t\t\tLog.Info( \"Architecture: no room under that drag for a porch to stand in.\" );\n\t\t\treturn;\n\t\t}\n\n\t\t// A porch is a cluster of legs, not one part - the draft's work is done either way.\n\t\tFinishDraft();\n\t\tOwner.Commit( \"Create Porch\" );\n\t}\n\n\t// One list: what the deck leans on and whether anything covers it - both picks - and then the three typed rows\n\t// that can never answer. A porch is a cluster of legs rather than one part, so nothing here is ever pointed at\n\t// a selection: a standing porch is edited from its own sheet in Select.\n\tprotected override void BuildOptions( ToolSidebarWidget panel )\n\t{\n\t\tusing var flow = ArchWorkflow.In( panel, Scope( \"flow\" ), Refresh );\n\n\t\tflow.Step( \"Standing\", ArchPorchUi.StandingName( draft ), ArchPorchUi.StandingGlyph( draft ), step =>\n\t\t\tArchPorchUi.Standing( step.Layout, draft, step.Chose, null ) );\n\n\t\tflow.Step( \"Roof\", draft.Roofed ? \"Roofed\" : \"Open\", draft.Roofed ? \"roofing\" : \"deck\", step =>\n\t\t\tArchPorchUi.Roof( step.Layout, draft, step.Chose, null ) );\n\n\t\tflow.Step( \"Include\", IncludeName, \"checklist\", step => ArchPorchUi.Include( step.Layout, draft, null ) );\n\n\t\tflow.Step( \"Frame\", FrameName, \"view_column\", step => ArchPorchUi.Frame( step.Layout, draft, null ) );\n\n\t\tflow.Step( \"Placement\", $\"{draft.DeckDrop:0} drop\", \"height\", step => ArchPorchUi.Placement( step.Layout, draft, null ) );\n\t}\n\n\tstring IncludeName => ArchPartUi.Listed(\n\t\t(draft.Deck, \"Deck\"), (draft.Rafters, \"Rafters\"), (draft.Braces, \"Braces\"), (draft.Railings, \"Railing\"), (draft.Door, \"Door\") );\n\n\tstring FrameName => ArchPartUi.Listed( (draft.Plinth, \"Plinth\"), (draft.Posts, \"Posts\"), (draft.Beam, \"Beam\") );\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Tool/Subtools/ArchSpanSubtool.cs",
            "FileName": "ArchSpanSubtool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing Editor;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// A span crosses between two piers, so it belongs to the tool that stands them: this is the Pillars subtool's\n// third gesture, composed the way ArchFixtureSubtool is composed by the bool tool, and it holds no shelf entry\n// of its own. Keeping the form in its own file keeps the two-click bridge out of the placement tool it rides in.\n[Title( \"Spans\" ), Icon( \"bridge\" ), Group( \"09\" )]\npublic sealed class ArchSpanSubtool( ArchTool owner ) : ArchSubtool( owner )\n{\n\tprotected override ArchKind? DraftKind => ArchKind.Span;\n\n\tpublic override ArchSurface[] Surfaces => new[] { ArchSurface.PillarCap };\n\n\tArchSpanPart draft = ArchSpanMemo.Recall();\n\n\tArchSpanEnd taken;\n\n\tpublic ArchSpanPart LastPlaced { get; private set; }\n\n\tpublic ArchKind? MergedDraftKind => DraftKind;\n\n\tpublic string MergedAdvice => Advice();\n\n\tpublic ArchRunState MergedRun => Run();\n\n\tpublic bool MergedAdjusts => Adjusts;\n\n\tpublic void MergedCancel() => CancelRun();\n\n\tpublic void MergedHover( Vector2 point ) => DrawHover( point );\n\n\t// The host does the draft bookkeeping and the commit, exactly as the bool tool does for a fixture: this\n\t// returns what it stood and nothing else, so there is one place a placement is finished.\n\tpublic ArchSpanPart MergedClick( Vector2 point )\n\t{\n\t\tLastPlaced = null;\n\n\t\tOnClick( point );\n\n\t\treturn LastPlaced;\n\t}\n\n\tprotected override bool UsesDrag => false;\n\n\t// Only over a span. The selection outlives the tool that made it, so an unconditional yes put the LAST\n\t// pillar's box dragger on screen the moment this tool was picked - and the gizmo takes the press first, so\n\t// every click meant for a pier went into a widget belonging to something else.\n\tprotected override bool Adjusts => Owner.Picked?.Item is ArchSpanPart;\n\n\tprotected override string Title() => \"Spans\";\n\n\tprotected override string Advice() => taken is null\n\t\t? \"Click a column or a wall to take one end. The nearest column within reach wins, so an eyeballed click still lands on the pier.\"\n\t\t: \"Click the far column or wall and it bridges them. An end that lands on nothing stays where it was dropped.\";\n\n\tprotected override ArchRunState Run() => new( taken is not null, \"one end taken\" );\n\n\tprotected override void CancelRun() => taken = null;\n\n\tprotected override void DrawHover( Vector2 point )\n\t{\n\t\tvar room = Owner.RoomOnLevel( point );\n\n\t\tif ( room is null )\n\t\t{\n\t\t\tbase.DrawHover( point );\n\n\t\t\treturn;\n\t\t}\n\n\t\tvar landing = ArchSpanShape.Anchored( Owner.Plan, room, point );\n\n\t\tPier( room, landing, false );\n\n\t\tif ( taken is null )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tPier( room, taken, true );\n\t\tReaching( room, taken, landing );\n\t}\n\n\tprotected override void OnClick( Vector2 point )\n\t{\n\t\tvar room = Owner.RoomOnLevel( point );\n\n\t\tif ( room is null )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar landing = ArchSpanShape.Anchored( Owner.Plan, room, point );\n\n\t\tif ( taken is null )\n\t\t{\n\t\t\ttaken = landing;\n\n\t\t\treturn;\n\t\t}\n\n\t\tBridge( room, taken, landing );\n\t}\n\n\tvoid Bridge( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )\n\t{\n\t\tif ( Standing( room, from, to ) is not { } span || !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out _ ) )\n\t\t{\n\t\t\tLog.Info( \"Architecture: nothing to bridge there \u2014 a span needs two ends apart, each under a pier tall enough to spring off.\" );\n\n\t\t\treturn;\n\t\t}\n\n\t\tspan.Id = Owner.Plan.AllocateId();\n\t\tspan.Name = $\"Span{room.PierSpans.Count + 1}\";\n\n\t\troom.PierSpans.Add( span );\n\t\tOwner.Picked = new ArchSelection { Item = span, Room = room };\n\n\t\tLastPlaced = span;\n\t\ttaken = null;\n\t}\n\n\t// A ring on the pier's HEAD and a stem down to the floor: where the end will spring from and which column it\n\t// grabbed, drawn as a mark on something rather than as a post, which is a pillar ghost by another name.\n\tvoid Pier( ArchRoom room, ArchSpanEnd end, bool held )\n\t{\n\t\tvar pier = ArchSpanShape.Pier( Owner.Plan, Owner.Kit, room, end );\n\t\tvar lift = Lift( room );\n\t\tvar head = new Vector3( pier.At.x, pier.At.y, pier.Head + lift );\n\n\t\tGizmo.Draw.LineThickness = held ? 4f : 3f;\n\t\tGizmo.Draw.Color = held ? ArchGhost.Accent : ArchGhost.Line;\n\t\tGizmo.Draw.LineCircle( head, Vector3.Up, held ? 14f : 10f );\n\t\tGizmo.Draw.Line( head, head.WithZ( room.BaseHeight + lift ) );\n\t\tGizmo.Draw.LineThickness = 2f;\n\n\t\tif ( !pier.Standing )\n\t\t{\n\t\t\tArchGhost.Cursor( pier.At, room.BaseHeight + lift, ArchSpanShape.Grab );\n\t\t}\n\n\t\tGizmo.Draw.Color = ArchGhost.Line;\n\t}\n\n\t// The plan is authored flat and the building is poured onto its grade, so a ghost drawn at plan height stands\n\t// a foundation below the span it is previewing.\n\tfloat Lift( ArchRoom room )\n\t{\n\t\tvar host = Owner.Plan.OwnerOf( room ) ?? Owner.ActiveBuilding();\n\n\t\treturn host is null ? 0f : ArchAsks.Lift( Owner.Plan, host, Owner.Kit );\n\t}\n\n\tvoid Reaching( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )\n\t{\n\t\tvar lift = Lift( room );\n\n\t\tif ( Standing( room, from, to ) is not { } span\n\t\t\t|| !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out var run ) )\n\t\t{\n\t\t\tGizmo.Draw.Line(\n\t\t\t\tnew Vector3( from.At.x, from.At.y, room.BaseHeight + lift ),\n\t\t\t\tnew Vector3( to.At.x, to.At.y, room.BaseHeight + lift ) );\n\n\t\t\treturn;\n\t\t}\n\n\t\tvar start = new Vector3( run.From.x, run.From.y, run.Springing + lift );\n\t\tvar end = new Vector3( run.To.x, run.To.y, run.Springing + lift );\n\n\t\tvar top = run.Top + lift;\n\n\t\tGizmo.Draw.LineThickness = 4f;\n\t\tGizmo.Draw.Color = ArchGhost.Accent;\n\n\t\tGizmo.Draw.Line( start, end );\n\t\tGizmo.Draw.Line( start.WithZ( top ), end.WithZ( top ) );\n\t\tGizmo.Draw.Line( start, start.WithZ( top ) );\n\t\tGizmo.Draw.Line( end, end.WithZ( top ) );\n\n\t\tGizmo.Draw.LineThickness = 2f;\n\t\tGizmo.Draw.Color = ArchGhost.Line;\n\n\t\tArchGhost.Note( Vector3.Lerp( start.WithZ( top ), end.WithZ( top ), 0.5f ),\n\t\t\t$\"{span.Form} \u2014 {(run.To - run.From).Length:0} across, {run.Top - run.Springing:0} deep, springs at {run.Springing - room.BaseHeight:0}\" );\n\t}\n\n\t// ONE resolution the ghost and the placed part both read, so what the gesture showed is what the mesh comes out as.\n\tArchSpanPart Standing( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to )\n\t{\n\t\tif ( room is null )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\tvar span = draft.Dressing();\n\n\t\tspan.Level = room.Floor;\n\t\tspan.From = from;\n\t\tspan.To = to;\n\n\t\treturn span;\n\t}\n\n\t// The host lays the rows: a bridge is the Pillars tool's third gesture, so its two questions belong in that\n\t// tool's one sequence rather than in a second list numbered from 1 underneath it.\n\tpublic void MergedSteps( ArchWorkflow flow )\n\t{\n\t\tvar span = Edited;\n\t\tvar changed = Changed( span );\n\n\t\tflow.Step( \"Form\", span.Form.ToString(), ArchSpanUi.Glyph( span.Form ), step =>\n\t\t\tArchSpanUi.Form( step.Layout, span, step.Chose, changed ) );\n\n\t\tflow.Step( \"Section\", ArchSpanUi.Measured( span ), \"straighten\", step =>\n\t\t\tArchSpanUi.Section( step.Layout, span, changed ) );\n\t}\n\n\t// The picked span if there is one, else the seed for the next bridge - the same two steps either way, which is\n\t// what keeps a tuned span and the next one drawn from agreeing.\n\tpublic ArchSpanPart Edited => Editing() ?? draft;\n\n\tpublic string PickedName => Editing()?.Name;\n\n\tAction Changed( ArchSpanPart span )\n\t{\n\t\treturn Editing() is null\n\t\t\t? () => ArchSpanMemo.Remember( draft )\n\t\t\t: () => { Seed( span ); Owner.Touch( \"Edit Span\" ); };\n\t}\n\n\t// Tuning the span that was just placed IS the author saying how the next one should look - the panel points at\n\t// the selection, so without this every adjustment was spent on one part and the seed never moved.\n\tvoid Seed( ArchSpanPart span )\n\t{\n\t\tdraft = span.Dressing();\n\n\t\tArchSpanMemo.Remember( draft );\n\t}\n\n\tArchSpanPart Editing() => Owner.Picked?.Item as ArchSpanPart;\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Data/ArchPlan.cs",
            "FileName": "ArchPlan.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\npublic sealed class ArchPlan\n{\n\tpublic int Version { get; set; } = 3;\n\tpublic int NextId { get; set; } = 1;\n\tpublic string KitName { get; set; } = \"default\";\n\n\t// Every top-level thing standing on the map, in stack order, whatever kind it is.\n\tpublic List<ArchUnit> Units { get; set; } = new();\n\n\t// Version 2 filed the two kinds in lists of their own. Read back under those names and folded into Units by\n\t// Normalize, so an old plan opens whole and is written back carrying one list.\n\t[JsonPropertyName( \"Buildings\" )]\n\tpublic List<ArchBuilding> LegacyBuildings { get; set; }\n\n\t[JsonPropertyName( \"Roads\" )]\n\tpublic List<ArchRoadPart> LegacyRoads { get; set; }\n\n\t// A view over the one list, so a pass that only cares about buildings still reads naturally. Filed through\n\t// Units - adding to a view would go nowhere, which is why it is not a List. Every other kind's view is shipped\n\t// by the module that owns it, so core names one type here and no more.\n\t[JsonIgnore]\n\tpublic IReadOnlyList<ArchBuilding> Buildings => Units.OfType<ArchBuilding>().ToList();\n\n\t// Cross-building relationships and placed reusable assets.\n\tpublic List<ArchSiteAssembly> Assemblies { get; set; } = new();\n\tpublic List<ArchAssetInstance> Instances { get; set; } = new();\n\t// Explicit layer metadata - written only when a legacy item is reparented, disabled or locked.\n\tpublic List<ArchLayerRecord> Layers { get; set; } = new();\n\t// What an author said about one named PIECE of a layer - a roof's fascia, a porch's balustrade.\n\tpublic List<ArchPartRecord> Parts { get; set; } = new();\n\tpublic List<ArchLayerLink> Links { get; set; } = new();\n\n\tpublic int AllocateId() => NextId++;\n\n\tpublic IEnumerable<ArchRoom> AllRooms() => Buildings.SelectMany( building => building.Rooms );\n\n\tpublic ArchBuilding FindBuilding( int id ) => Buildings.FirstOrDefault( building => building.Id == id );\n\n\tpublic ArchRoom FindRoom( int id ) => AllRooms().FirstOrDefault( room => room.Id == id );\n\n\tpublic ArchBuilding OwnerOf( ArchRoom room ) => Buildings.FirstOrDefault( building => building.Rooms.Contains( room ) );\n\n\t// Buildings first, then roads, the order version 2 wrote them in. Run before anything counts ids, and it\n\t// clears the legacy lists so a plan opened and saved again carries Units alone.\n\tvoid Adopt()\n\t{\n\t\tif ( LegacyBuildings is { Count: > 0 } )\n\t\t{\n\t\t\tUnits.AddRange( LegacyBuildings );\n\t\t}\n\n\t\tif ( LegacyRoads is { Count: > 0 } )\n\t\t{\n\t\t\tUnits.AddRange( LegacyRoads );\n\t\t}\n\n\t\tLegacyBuildings = null;\n\t\tLegacyRoads = null;\n\t}\n\n\t// Empty can never be a count - a placement seeds a target before it knows whether it will fill it. A road being\n\t// drawn holds one node and no content yet, and blanking the file under it is how an authored plan is lost.\n\t[JsonIgnore]\n\tpublic bool HasContent => this.Roads().Count > 0 || Units.Any( unit => unit.HasContent );\n\n\t// A placement target that was seeded and never filled is not authored content, so it must not survive\n\t// the commit - it would stand in the layer stack as an empty Building/Level/Room nobody drew.\n\tpublic int DiscardEmptyTargets()\n\t{\n\t\tvar dropped = 0;\n\n\t\tforeach ( var building in Buildings )\n\t\t{\n\t\t\tdropped += building.Rooms.RemoveAll( room => !room.HasContent );\n\t\t}\n\n\t\treturn dropped + Units.RemoveAll( unit => !unit.HasContent );\n\t}\n\n\tpublic void Normalize()\n\t{\n\t\tAdopt();\n\n\t\tNextId = System.Math.Max( 1, HighestId() + 1 );\n\n\t\tforeach ( var building in Buildings )\n\t\t{\n\t\t\t// Plans saved before fences carried a curve - lift the old path onto nodes once.\n\t\t\tforeach ( var fence in building.Fences.Where( entry => entry.Path.Count >= 2 && entry.Nodes.Count == 0 ) )\n\t\t\t{\n\t\t\t\tfence.Nodes = fence.Path.Select( ArchCurveNode.At ).ToList();\n\t\t\t\tfence.Path = new List<Vector3>();\n\t\t\t}\n\n\t\t\t// Plans saved before a stair was a shaft: the chain of overlapping boxes is read once into the core\n\t\t\t// that bounds them and the flights that stood in it, then let go of. Everything downstream has only\n\t\t\t// ever seen the resolve, so a converted stair builds exactly what it built before.\n\t\t\tforeach ( var room in building.Rooms )\n\t\t\t{\n\t\t\t\tforeach ( var stair in room.Stairs )\n\t\t\t\t{\n\t\t\t\t\tReshaft( stair );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tforeach ( var platform in building.Platforms )\n\t\t\t{\n\t\t\t\tforeach ( var stair in platform.Stairs )\n\t\t\t\t{\n\t\t\t\t\tReshaft( stair );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tforeach ( var porch in building.Rooms.SelectMany( room => room.Porches ) )\n\t\t\t{\n\t\t\t\tforeach ( var stair in porch.Stairs )\n\t\t\t\t{\n\t\t\t\t\tReshaft( stair );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tforeach ( var building in Buildings )\n\t\t{\n\t\t\tforeach ( var room in building.Rooms )\n\t\t\t{\n\t\t\t\troom.Walls.RemoveAll( wall => wall.Length < 1f );\n\t\t\t}\n\n\t\t\tforeach ( var roof in building.Roofs )\n\t\t\t{\n\t\t\t\troof.Walls.RemoveAll( wall => wall.Length < 1f );\n\t\t\t}\n\n\t\t\tforeach ( var roof in building.Roofs.Where( roof => roof.HasFootprint ) )\n\t\t\t{\n\t\t\t\troof.Reshape( roof.Footprint );\n\t\t\t}\n\n\t\t\tSeparateSingleSpanRoofs( building );\n\t\t}\n\n\t\t// And whatever a KIND says its own parts need fixing up, which is how a contributed kind migrates a plan\n\t\t// written before it changed shape. Every pass must be idempotent: Normalize runs on every load and again\n\t\t// after every connection resolve.\n\t\tforeach ( var fixup in ArchKinds.Load().All.OfType<IArchNormalizes>() )\n\t\t{\n\t\t\tfixup.Normalize( this );\n\t\t}\n\t}\n\n\t// Two migrations, each of which runs exactly once per plan however many times Normalize is called, because\n\t// each lets go of what it read.\n\t//\n\t// A stair authored as a chain of overlapping boxes is read into the shaft that bounds them - a stair saved\n\t// before there were legs at all still gets a shaft, because a core of nothing builds nothing. Then a stair\n\t// authored when a landing was DERIVED has that derive run one last time and left behind as real landing\n\t// steps, so nothing downstream ever works a pad out again.\n\tvoid Reshaft( ArchStairPart stair )\n\t{\n\t\tif ( stair.Legs is { Count: > 0 } legs )\n\t\t{\n\t\t\tvar (core, lanes) = ArchStairLanes.FromLegs( legs );\n\n\t\t\tcore.Rise = MathF.Max( 4f, stair.StoredRise > 1f ? stair.StoredRise : stair.Core?.Rise ?? 0f );\n\n\t\t\tstair.Core = core;\n\t\t\tstair.Lanes = lanes;\n\t\t\tstair.Legs = null;\n\t\t}\n\n\t\tstair.StoredRise = 0f;\n\n\t\tArchStairLanes.Settle( this, stair );\n\t\tArchStairLanes.Number( this, stair );\n\t}\n\n\tvoid SeparateSingleSpanRoofs( ArchBuilding building )\n\t{\n\t\tfor ( var roofIndex = building.Roofs.Count - 1; roofIndex >= 0; roofIndex-- )\n\t\t{\n\t\t\tvar roof = building.Roofs[roofIndex];\n\t\t\tvar outline = roof.Outline();\n\n\t\t\tif ( roof.Style is not (RoofStyle.Gable or RoofStyle.Shed or RoofStyle.Sawtooth) ||\n\t\t\t\t!roof.HasFootprint ||\n\t\t\t\toutline.Count == 4 )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar rooms = building.Rooms\n\t\t\t\t.Where( room => room.Floor == roof.Level )\n\t\t\t\t.Select( room => (Room: room, Footprint: ArchFloorGen.Footprint( room )) )\n\t\t\t\t.Where( candidate => candidate.Footprint.Count == 4 )\n\t\t\t\t.Where( candidate => ArchRegion.Covers( new[] { outline }, candidate.Footprint ) )\n\t\t\t\t.ToList();\n\n\t\t\trooms.RemoveAll( candidate => rooms.Any( other =>\n\t\t\t\t!ReferenceEquals( candidate.Room, other.Room ) &&\n\t\t\t\tMathF.Abs( ArchFootprint.SignedArea( other.Footprint ) ) > MathF.Abs( ArchFootprint.SignedArea( candidate.Footprint ) ) &&\n\t\t\t\tArchRegion.Covers( new[] { other.Footprint }, candidate.Footprint ) ) );\n\n\t\t\tvar occupied = ArchFootprint.Union( rooms.Select( candidate => candidate.Footprint ).ToList() );\n\n\t\t\tif ( rooms.Count < 2 ||\n\t\t\t\t!ArchRegion.Covers( occupied, outline ) ||\n\t\t\t\toccupied.Any( loop => !ArchRegion.Covers( new[] { outline }, loop ) ) )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tbuilding.Roofs.RemoveAt( roofIndex );\n\n\t\t\tfor ( var roomIndex = rooms.Count - 1; roomIndex >= 0; roomIndex-- )\n\t\t\t{\n\t\t\t\tvar section = roof.Duplicate( roomIndex == 0 ? roof.Id : AllocateId(), rooms[roomIndex].Footprint );\n\t\t\t\tbuilding.Roofs.Insert( roofIndex, section );\n\t\t\t}\n\t\t}\n\t}\n\n\tint HighestId()\n\t{\n\t\tvar ids = new List<int> { 0 };\n\t\tvar roads = this.Roads();\n\n\t\tids.AddRange( Assemblies.Select( assembly => assembly.Id ) );\n\t\tids.AddRange( Instances.Select( instance => instance.Id ) );\n\t\tids.AddRange( roads.Select( road => road.Id ) );\n\t\tids.AddRange( roads.SelectMany( road => road.Crossings ).Select( crossing => crossing.Id ) );\n\t\tids.AddRange( roads.SelectMany( road => road.Bridges ).Select( bridge => bridge.Id ) );\n\t\tids.AddRange( roads.SelectMany( road => road.Tunnels ).Select( tunnel => tunnel.Id ) );\n\t\tids.AddRange( roads.SelectMany( road => road.Cuts ).Select( cut => cut.Id ) );\n\n\t\tforeach ( var building in Buildings )\n\t\t{\n\t\t\tids.Add( building.Id );\n\t\t\tids.AddRange( building.Rooms.Select( room => room.Id ) );\n\t\t\tids.AddRange( building.Rooms.SelectMany( room => room.Stairs ).Select( stair => stair.Id ) );\n\t\t\tids.AddRange( building.Rooms.SelectMany( room => room.Trims ).Select( trim => trim.Id ) );\n\t\t\tids.AddRange( building.Rooms.SelectMany( room => room.Pillars ).Select( pillar => pillar.Id ) );\n\t\t\tids.AddRange( building.Rooms.SelectMany( room => room.PierSpans ).Select( span => span.Id ) );\n\t\t\tids.AddRange( building.Rooms.SelectMany( room => room.Beams ).Select( beam => beam.Id ) );\n\t\t\t// The porch is a host, so its own children are in here too - miss them and an id is handed out\n\t\t\t// twice, which in the scene is one object standing where two were meant to.\n\t\t\tvar porches = building.Rooms.SelectMany( room => room.Porches ).ToList();\n\n\t\t\tids.AddRange( porches.Select( porch => porch.Id ) );\n\t\t\tids.AddRange( porches.SelectMany( porch => porch.Stairs ).Select( stair => stair.Id ) );\n\t\t\tids.AddRange( porches.SelectMany( porch => porch.Pillars ).Select( pillar => pillar.Id ) );\n\t\t\tids.AddRange( porches.SelectMany( porch => porch.Trims ).Select( trim => trim.Id ) );\n\t\t\tids.AddRange( building.Rooms.SelectMany( room => room.Approaches ).Select( approach => approach.Id ) );\n\t\t\tids.AddRange( building.Roofs.Select( roof => roof.Id ) );\n\t\t\tids.AddRange( building.Roofs.SelectMany( roof => roof.Lights ).Select( light => light.Id ) );\n\t\t\tids.AddRange( building.Fences.Select( fence => fence.Id ) );\n\t\t\tids.AddRange( building.Downpipes.Select( pipe => pipe.Id ) );\n\t\t\tids.AddRange( building.Pipes.Select( run => run.Id ) );\n\t\t\tids.AddRange( building.Pipes.SelectMany( run => run.Nodes ).Select( node => node.Id ) );\n\t\t\tids.AddRange( building.Brackets.Select( bracket => bracket.Id ) );\n\t\t\tids.AddRange( building.Ladders.Select( ladder => ladder.Id ) );\n\t\t\tids.AddRange( building.Balconies.Select( balcony => balcony.Id ) );\n\t\t\tids.AddRange( building.ExteriorStairs.Select( flight => flight.Id ) );\n\t\t\tids.AddRange( building.Cutouts.Select( cutout => cutout.Id ) );\n\t\t\tids.AddRange( building.Cuts.Select( cut => cut.Id ) );\n\t\t\tids.AddRange( building.Platforms.Select( platform => platform.Id ) );\n\t\t\tids.AddRange( building.Platforms.SelectMany( platform => platform.Stairs ).Select( stair => stair.Id ) );\n\t\t}\n\n\t\t// Through AllWalls, or a parapet's id is handed out again the next time a build normalizes, and two\n\t\t// walls sharing an id are one object in the scene - the second stands where the first stood.\n\t\tvar walls = this.AllWalls().ToList();\n\n\t\tids.AddRange( walls.Select( wall => wall.Id ) );\n\t\tids.AddRange( walls.SelectMany( wall => wall.Openings ).Select( opening => opening.Id ) );\n\t\tids.AddRange( walls.SelectMany( wall => wall.Modifiers ).Select( modifier => modifier.Id ) );\n\n\t\t// Every step of a climb carries an id so a railing can name the one it guards, exactly as a pipe's nodes\n\t\t// do - and through Parts, because a stair stands in a room, on a porch and on a platform, and the three\n\t\t// lists that hold them are three chances to forget one.\n\t\tvar steps = this.Parts<ArchStairPart>().SelectMany( stair => stair.Lanes ).ToList();\n\n\t\tids.AddRange( steps.Select( lane => lane.Id ) );\n\t\tids.AddRange( steps.SelectMany( lane => lane.Guards ).Select( guard => guard.Id ) );\n\n\t\t// And through the bytes no type here claims, or the parts of a kind this build cannot read are invisible\n\t\t// to the allocator and the next id handed out is one something already holds.\n\t\tids.AddRange( Units.Select( unit => unit.Id ) );\n\t\tids.AddRange( Units.Select( unit => ArchPlanStore.HighestIdIn( unit.Payloads ) ) );\n\t\tids.AddRange( AllRooms().Select( room => ArchPlanStore.HighestIdIn( room.Payloads ) ) );\n\n\t\treturn ids.Max();\n\t}\n}\n\npublic sealed class ArchPalette\n{\n\tpublic Dictionary<string, string> Materials { get; set; } = new();\n\tpublic Dictionary<string, float> TexelScales { get; set; } = new();\n\tpublic Dictionary<string, Vector2> TextureOffsets { get; set; } = new();\n\tpublic Dictionary<string, ArchFaceUvSet> FaceMappings { get; set; } = new();\n\n\tpublic bool TryGet( ArchSurface surface, out string path )\n\t{\n\t\treturn Materials.TryGetValue( surface.ToString(), out path ) && !string.IsNullOrWhiteSpace( path );\n\t}\n\n\t// The density belongs to the material, so re-pointing a role drops it.\n\tpublic void Set( ArchSurface surface, string path )\n\t{\n\t\tTexelScales.Remove( surface.ToString() );\n\n\t\tif ( string.IsNullOrWhiteSpace( path ) )\n\t\t{\n\t\t\tMaterials.Remove( surface.ToString() );\n\t\t\tTextureOffsets?.Remove( surface.ToString() );\n\t\t\treturn;\n\t\t}\n\n\t\tMaterials[surface.ToString()] = path;\n\t}\n\n\tpublic void Set( ArchSurface surface, string path, float texelScale )\n\t{\n\t\tSet( surface, path );\n\n\t\tif ( !string.IsNullOrWhiteSpace( path ) )\n\t\t{\n\t\t\tSetScale( surface, texelScale );\n\t\t}\n\t}\n\n\t// Zero means \"no override\" - the generator falls back to ArchMesh.TexelScale.\n\tpublic void SetScale( ArchSurface surface, float texelScale )\n\t{\n\t\tif ( texelScale <= 0f )\n\t\t{\n\t\t\tTexelScales.Remove( surface.ToString() );\n\t\t\treturn;\n\t\t}\n\n\t\tTexelScales[surface.ToString()] = texelScale;\n\t}\n\n\tpublic float ScaleFor( ArchSurface surface )\n\t{\n\t\treturn TexelScales.TryGetValue( surface.ToString(), out var scale ) ? scale : 0f;\n\t}\n\n\tpublic void SetOffset( ArchSurface surface, Vector2 offset )\n\t{\n\t\tif ( offset.IsNearZeroLength )\n\t\t{\n\t\t\tTextureOffsets?.Remove( surface.ToString() );\n\t\t\treturn;\n\t\t}\n\n\t\tTextureOffsets ??= new();\n\t\tTextureOffsets[surface.ToString()] = offset;\n\t}\n\n\tpublic Vector2 OffsetFor( ArchSurface surface )\n\t{\n\t\treturn TextureOffsets is not null && TextureOffsets.TryGetValue( surface.ToString(), out var offset ) ? offset : Vector2.Zero;\n\t}\n}\n\npublic interface IArchPainted\n{\n\tArchPalette Palette { get; set; }\n}\n\npublic sealed class ArchFaceUvSet\n{\n\tpublic List<ArchFaceUvVariation> Variations { get; set; } = new();\n}\n\npublic sealed class ArchFaceUvVariation\n{\n\tpublic string Signature { get; set; }\n\tpublic List<ArchFaceUvFace> Faces { get; set; } = new();\n}\n\npublic sealed class ArchFaceUvFace\n{\n\tpublic List<Vector2> Coordinates { get; set; } = new();\n}\n\n// One top-level thing standing on the map. A house and a street are not the same shape and never will be, but\n// everything the STACK does to one it does to the other - name it, disable it, group it, order it, carve it - so\n// they share a base and the plan holds one list. Walking two lists is how a road quietly stopped being reached\n// by half the passes that reach a building.\n[JsonConverter( typeof( ArchUnitConverter ) )]\npublic abstract class ArchUnit : IArchCollides, IArchNamed\n{\n\t// The whole unit at once - what a far-off silhouette is set to None from.\n\tpublic ArchCollisionMode? Collision { get; set; }\n\n\t// Which module's unit this is. Written as an ordinary field rather than a polymorphic discriminator, because\n\t// System.Text.Json throws on a discriminator it does not recognise and an unknown kind is the not-installed case.\n\tpublic ArchKind Kind { get; set; }\n\n\tpublic int Id { get; set; }\n\t// The author's, not the kind's: a unit is renamed in the Plan Layers stack like any other layer.\n\tpublic string Name { get; set; } = \"Unit\";\n\tpublic ArchPalette Palette { get; set; } = new();\n\t// One list - a stairwell, a passage and a service bay are the same part with a different profile.\n\tpublic List<ArchCutPart> Cuts { get; set; } = new();\n\n\t// Whatever a module filed here that this editor has no type for. Written back exactly as it was read, so a plan\n\t// opened without the library that authored it saves whole instead of losing that library's work.\n\t[JsonExtensionData]\n\tpublic Dictionary<string, JsonElement> Payloads { get; set; } = new();\n\n\t[JsonIgnore]\n\tpublic abstract bool HasContent { get; }\n}\n\n// A unit whose kind no installed module claims. It carries nothing this editor can read and everything the file\n// gave it, so it round-trips byte for byte - and it always reports content, or DiscardEmptyTargets would delete\n// the one thing in the plan nobody here is able to see.\npublic sealed class ArchOpaqueUnit : ArchUnit\n{\n\tpublic override bool HasContent => true;\n}\n\npublic sealed class ArchBuilding : ArchUnit, IArchPainted\n{\n\tpublic ArchBuilding()\n\t{\n\t\tName = \"Building\";\n\t\tKind = ArchKind.Building;\n\t}\n\n\t// A wing extended later still comes out the same type, not kit defaults.\n\tpublic string Archetype { get; set; } = \"\";\n\tpublic List<ArchRoom> Rooms { get; set; } = new();\n\tpublic List<ArchRoofPart> Roofs { get; set; } = new();\n\tpublic List<ArchDownpipePart> Downpipes { get; set; } = new();\n\t// Service corridors and the hangers under them. Filed on the unit rather than a room, because a run\n\t// crosses partitions the way a fence crosses a yard - the volume is world-space and answers to no floor.\n\tpublic List<ArchPipePart> Pipes { get; set; } = new();\n\tpublic List<ArchPipeBracketPart> Brackets { get; set; } = new();\n\tpublic List<ArchFencePart> Fences { get; set; } = new();\n\t// Platforms stand in the yard, so they hang off the building like a fence.\n\tpublic List<ArchPlatformPart> Platforms { get; set; } = new();\n\tpublic List<ArchLadderPart> Ladders { get; set; } = new();\n\t// Standing outside a room, so they hang off the building for the same reason a ladder does.\n\tpublic List<ArchBalconyPart> Balconies { get; set; } = new();\n\tpublic List<ArchExteriorStairPart> ExteriorStairs { get; set; } = new();\n\tpublic bool GuttersEnabled { get; set; } = true;\n\t// How far round the shell has been turned since it was drawn. The coordinates are still the truth - this is\n\t// the LEDGER of the turns applied to them, so an angle can be named absolutely instead of only nudged.\n\tpublic float Facing { get; set; }\n\tpublic float StoreyHeight { get; set; }\n\tpublic List<ArchFloorCutout> Cutouts { get; set; } = new();\n\n\t[JsonIgnore]\n\tpublic override bool HasContent => Roofs.Count > 0 || Downpipes.Count > 0 || Fences.Count > 0 || Platforms.Count > 0\n\t\t|| Ladders.Count > 0 || Cuts.Count > 0 || Balconies.Count > 0 || ExteriorStairs.Count > 0\n\t\t|| Pipes.Count > 0 || Brackets.Count > 0\n\t\t|| Rooms.Any( room => room.HasContent );\n}\n\npublic sealed class ArchRoom : IArchCollides, IArchPainted, IArchNamed\n{\n\t// Overrides the kit for the room's own shell, slab and ceiling. A part standing IN it carries its own.\n\tpublic ArchCollisionMode? Collision { get; set; }\n\n\tpublic int Id { get; set; }\n\tpublic string Name { get; set; } = \"Room\";\n\tpublic int Floor { get; set; }\n\tpublic float BaseHeight { get; set; }\n\tpublic float WallHeight { get; set; }\n\tpublic bool HasFloor { get; set; } = true;\n\tpublic bool HasCeiling { get; set; } = true;\n\t// A shell with nothing behind it: every wall it holds is single-sided, so the far face is never emitted and\n\t// ArchCull stands a lined box behind each window instead. Its floor and ceiling are still its own to turn off.\n\tpublic bool Facade { get; set; }\n\tpublic float CeilingDepth { get; set; }\n\tpublic bool FloorBoards { get; set; }\n\tpublic float FloorBoardYaw { get; set; }\n\tpublic bool RaisedFoundation { get; set; } = true;\n\t// A link across a gap, carried by its own piers - not an overhang.\n\tpublic bool Spans { get; set; }\n\t// A row of posts under whatever this storey oversails. Off by default: an upper storey that steps out over\n\t// the one below reads as a cantilever, and propping every one of them is a look, not a rule.\n\tpublic bool OverhangPosts { get; set; }\n\t// The section every one of those posts is cut from - a pillar with no position, exactly as a pillar TYPE\n\t// holds one, so a prop under an overhang is dressed by the Pillars tool's own forms rather than a second set.\n\tpublic ArchPillarPart OverhangPost { get; set; } = new();\n\t// A rising walkway: the far end's floor height. Equal to BaseHeight on a flat link.\n\tpublic float WalkwayTop { get; set; }\n\tpublic WalkwayInterior Interior { get; set; }\n\t// The link draws its own boards so they die square at the mouth corners.\n\tpublic bool WalkwaySkirting { get; set; } = true;\n\tpublic ArchPalette Palette { get; set; } = new();\n\tpublic List<ArchWall> Walls { get; set; } = new();\n\tpublic List<ArchStairPart> Stairs { get; set; } = new();\n\tpublic List<ArchTrimPart> Trims { get; set; } = new();\n\tpublic List<ArchPillarPart> Pillars { get; set; } = new();\n\t// Not \"Spans\" - that is already the walkway's own flag, and a slot name is a json property.\n\tpublic List<ArchSpanPart> PierSpans { get; set; } = new();\n\tpublic List<ArchBeamPart> Beams { get; set; } = new();\n\tpublic List<ArchPorchPart> Porches { get; set; } = new();\n\tpublic List<ArchApproachPart> Approaches { get; set; } = new();\n\tpublic List<Vector2> Footprint { get; set; } = new();\n\n\t[JsonExtensionData]\n\tpublic Dictionary<string, JsonElement> Payloads { get; set; } = new();\n\n\t[JsonIgnore]\n\tpublic bool HasFootprint => Footprint.Count >= 3;\n\n\t[JsonIgnore]\n\tpublic bool HasContent => HasFootprint || Walls.Count > 0 || Stairs.Count > 0 || Pillars.Count > 0\n\t\t|| Trims.Count > 0 || Porches.Count > 0 || Approaches.Count > 0 || Beams.Count > 0 || PierSpans.Count > 0;\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Layers/ArchLayerTree.cs",
            "FileName": "ArchLayerTree.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Sunless.Architecture;\n\n// One authored thing in the plan, shown as a tree row. Payload is the typed part itself; virtual\n// nodes (story groups) carry none. Building/Room carry the selection context so any row can become\n// an ArchSelection without re-hunting ownership.\npublic sealed class ArchLayerNode\n{\n\tpublic ArchLayerRef? Ref { get; init; }\n\tpublic ArchKind Kind { get; init; }\n\tpublic ArchLayerStage Stage { get; init; }\n\tpublic ArchLayerDomain Domain { get; init; }\n\tpublic object Payload { get; init; }\n\tpublic string Name { get; init; } = \"\";\n\t// Records may exclude a layer from generation; absent a record, everything is enabled.\n\tpublic bool Enabled { get; init; } = true;\n\t// Locked rows still select and still generate; they refuse every edit.\n\tpublic bool Locked { get; init; }\n\t// Sibling order within a stage, for the kinds that care. Absent a record it is authoring order.\n\tpublic int Order { get; init; }\n\t// Story headers and room layers name the storey they stand on.\n\tpublic int Floor { get; init; } = int.MinValue;\n\tpublic ArchLayerNode Parent { get; internal set; }\n\tpublic List<ArchLayerNode> Children { get; } = new();\n\t// Selection context: every row knows which building and room it belongs to.\n\tpublic ArchBuilding Building { get; init; }\n\tpublic ArchRoom Room { get; init; }\n\n\tpublic bool Virtual => Payload is null;\n\n\tpublic string DisplayName => ArchLayerNames.DisplayName( this );\n}\n\npublic sealed class ArchLayerDomainGroup\n{\n\tpublic ArchLayerDomain Domain { get; init; }\n\tpublic string Name { get; init; } = \"\";\n\tpublic List<ArchLayerNode> Children { get; } = new();\n}\n\n// The projected layer tree: a metadata-only view of the plan's authored ownership. Building it must\n// never resolve generator shapes - only ids, kinds, names and floors.\npublic sealed class ArchLayerTree\n{\n\tpublic List<ArchLayerDomainGroup> Domains { get; } = new();\n\n\treadonly Dictionary<int, ArchLayerNode> byId = new();\n\treadonly Dictionary<object, ArchLayerNode> byPayload = new();\n\treadonly Dictionary<(int Building, int Floor), ArchLayerNode> stories = new();\n\n\tpublic IReadOnlyList<ArchLayerLink> Links { get; private set; } = Array.Empty<ArchLayerLink>();\n\n\tpublic ArchLayerNode Find( int id ) => byId.TryGetValue( id, out var node ) ? node : null;\n\n\tpublic ArchLayerNode Find( object payload )\n\t{\n\t\treturn payload is null ? null : byPayload.TryGetValue( payload, out var node ) ? node : null;\n\t}\n\n\tpublic object Resolve( ArchLayerRef layer ) => byId.TryGetValue( layer.ItemId, out var node ) ? node.Payload : null;\n\n\t// \"House 2 / Level 1 / Walkway 1\" - the row's authored path, not its object path.\n\tpublic string Breadcrumb( ArchLayerNode node )\n\t{\n\t\tif ( node is null )\n\t\t{\n\t\t\treturn \"\";\n\t\t}\n\n\t\tvar parts = new List<string>();\n\n\t\tfor ( var current = node; current is not null; current = current.Parent )\n\t\t{\n\t\t\tparts.Add( current.DisplayName );\n\t\t}\n\n\t\tparts.Reverse();\n\n\t\treturn string.Join( \" / \", parts );\n\t}\n\n\t// A stable identity for tree widgets: payload rows key by the payload itself (which survives\n\t// commits), story rows by their building and floor.\n\tpublic object StableKey( ArchLayerNode node )\n\t{\n\t\treturn node.Payload ?? $\"story:{node.Building?.Id}:{node.Floor}\";\n\t}\n\n\t// The explicit metadata for a layer, written the first time anything is asked of it that typed\n\t// ownership cannot answer - a parent, an order, a disabled state, a lock.\n\tpublic ArchLayerRecord Record( ArchPlan plan, ArchLayerNode node )\n\t{\n\t\tif ( node?.Ref is not { } layer )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\tvar record = plan.Layers.FirstOrDefault( entry => entry.ItemId == layer.ItemId );\n\n\t\tif ( record is null )\n\t\t{\n\t\t\trecord = new ArchLayerRecord\n\t\t\t{\n\t\t\t\tItemId = layer.ItemId,\n\t\t\t\tParentId = layer.ParentId,\n\t\t\t\tKind = layer.Kind,\n\t\t\t\tStage = node.Stage,\n\t\t\t\tOrder = node.Order,\n\t\t\t\tEnabled = node.Enabled,\n\t\t\t\tLocked = node.Locked,\n\t\t\t};\n\n\t\t\tplan.Layers.Add( record );\n\t\t}\n\n\t\treturn record;\n\t}\n\n\t// Drag-reorder writes an explicit order for the whole sibling run, so the arrangement survives a\n\t// later addition landing at the end of its owner's typed list.\n\tpublic bool Reorder( ArchPlan plan, ArchLayerRef moving, int anchorId, bool below )\n\t{\n\t\tif ( !byId.TryGetValue( moving.ItemId, out var node ) || !byId.TryGetValue( anchorId, out var anchor ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( ReferenceEquals( node, anchor ) || !ReferenceEquals( node.Parent, anchor.Parent ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tvar siblings = (node.Parent?.Children ?? Domains.FirstOrDefault( domain => domain.Domain == node.Domain )?.Children)\n\t\t\t?.Where( child => child.Ref is not null )\n\t\t\t.ToList();\n\n\t\tif ( siblings is null )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tsiblings.Remove( node );\n\n\t\tvar at = siblings.IndexOf( anchor );\n\n\t\tif ( at < 0 )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tsiblings.Insert( below ? at + 1 : at, node );\n\n\t\tfor ( var index = 0; index < siblings.Count; index++ )\n\t\t{\n\t\t\tRecord( plan, siblings[index] ).Order = index;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t// Validated reparent: the capability matrix approves, the payload actually moves between its\n\t// ownership lists, and a layer record is written so the projection keeps the new parent.\n\tpublic bool Reparent( ArchPlan plan, ArchLayerRef layer, int newParentId )\n\t{\n\t\tif ( !byId.TryGetValue( layer.ItemId, out var node ) || node.Payload is null )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\t// Dropping onto the domain header takes the layer out of whatever group held it.\n\t\tif ( newParentId == 0 )\n\t\t{\n\t\t\tArchLayerGroups.Leave( plan, layer.ItemId );\n\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( layer.ItemId == newParentId || !byId.TryGetValue( newParentId, out var parentNode ) || parentNode.Payload is null )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\t// A folder takes anything: membership is a scope, not an ownership claim, so the payload stays\n\t\t// exactly where the generator reads it and only the layer it belongs to changes.\n\t\tif ( parentNode.Payload is ArchSiteAssembly group )\n\t\t{\n\t\t\tArchLayerGroups.Join( plan, group, layer.ItemId );\n\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( !ArchLayerRules.CanParent( parentNode.Kind, layer.Kind ).Allowed )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tArchLayerGroups.Leave( plan, layer.ItemId );\n\n\t\tif ( !MovePayload( plan, node.Payload, parentNode.Payload ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tvar record = plan.Layers.FirstOrDefault( entry => entry.ItemId == layer.ItemId );\n\n\t\tif ( record is null )\n\t\t{\n\t\t\trecord = new ArchLayerRecord { ItemId = layer.ItemId, ParentId = newParentId, Kind = layer.Kind, Stage = node.Stage };\n\t\t\tplan.Layers.Add( record );\n\t\t}\n\n\t\trecord.ParentId = newParentId;\n\n\t\treturn true;\n\t}\n\n\t// A wall lives on a room or on a deck, so it is taken out of whichever holds it before being filed anywhere.\n\tstatic bool Unfile( ArchPlan plan, ArchWall wall )\n\t{\n\t\tforeach ( var room in plan.AllRooms() )\n\t\t{\n\t\t\tif ( room.Walls.Remove( wall ) )\n\t\t\t{\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\tforeach ( var roof in plan.AllRoofs() )\n\t\t{\n\t\t\tif ( roof.Walls.Remove( wall ) )\n\t\t\t{\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// A flight stands in a room, on a platform or on a porch deck, so it is taken out of whichever holds it\n\t// before being filed anywhere. The same three-homed lookup covers columns and runs.\n\tstatic bool Unfile( ArchPlan plan, ArchStairPart stair )\n\t{\n\t\tforeach ( var room in plan.AllRooms() )\n\t\t{\n\t\t\tif ( room.Stairs.Remove( stair ) || room.Porches.Any( porch => porch.Stairs.Remove( stair ) ) )\n\t\t\t{\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn plan.Buildings.SelectMany( building => building.Platforms ).Any( platform => platform.Stairs.Remove( stair ) );\n\t}\n\n\tstatic bool Unfile( ArchPlan plan, ArchPillarPart pillar )\n\t{\n\t\treturn plan.AllRooms().Any( room => room.Pillars.Remove( pillar ) || room.Porches.Any( porch => porch.Pillars.Remove( pillar ) ) );\n\t}\n\n\tstatic bool Unfile( ArchPlan plan, ArchTrimPart trim )\n\t{\n\t\treturn plan.AllRooms().Any( room => room.Trims.Remove( trim ) || room.Porches.Any( porch => porch.Trims.Remove( trim ) ) );\n\t}\n\n\t// The plan's typed lists are the payloads' real homes - a reparent that only rewrote the record\n\t// would show a new tree while the generator read the old ownership.\n\tstatic bool MovePayload( ArchPlan plan, object payload, object newParent )\n\t{\n\t\tswitch ( payload, newParent )\n\t\t{\n\t\t\tcase (ArchRoom room, ArchBuilding building):\n\t\t\t\tif ( plan.OwnerOf( room ) is not { } fromRoom ) return false;\n\t\t\t\tfromRoom.Rooms.Remove( room );\n\t\t\t\tbuilding.Rooms.Add( room );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchWall wall, ArchRoom room):\n\t\t\t\tif ( !Unfile( plan, wall ) ) return false;\n\t\t\t\troom.Walls.Add( wall );\n\t\t\t\treturn true;\n\n\t\t\t// Onto a deck: the wall stops standing on a floor and starts standing on a roof, which is the only\n\t\t\t// difference between a partition and a parapet.\n\t\t\tcase (ArchWall wall, ArchRoofPart roof):\n\t\t\t\tif ( !Unfile( plan, wall ) ) return false;\n\t\t\t\troof.Walls.Add( wall );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchOpening opening, ArchWall wall):\n\t\t\t\tif ( plan.AllWalls().FirstOrDefault( candidate => candidate.Openings.Contains( opening ) ) is not { } fromOpening ) return false;\n\t\t\t\tfromOpening.Openings.Remove( opening );\n\t\t\t\twall.Openings.Add( opening );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchWallModPart modifier, ArchWall host):\n\t\t\t\tif ( plan.AllWalls().FirstOrDefault( candidate => candidate.Modifiers.Contains( modifier ) ) is not { } fromModifier ) return false;\n\t\t\t\tfromModifier.Modifiers.Remove( modifier );\n\t\t\t\thost.Modifiers.Add( modifier );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchStairPart stair, ArchRoom room):\n\t\t\t\tif ( !Unfile( plan, stair ) ) return false;\n\t\t\t\troom.Stairs.Add( stair );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchTrimPart trim, ArchRoom room):\n\t\t\t\tif ( !Unfile( plan, trim ) ) return false;\n\t\t\t\troom.Trims.Add( trim );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchPillarPart pillar, ArchRoom room):\n\t\t\t\tif ( !Unfile( plan, pillar ) ) return false;\n\t\t\t\troom.Pillars.Add( pillar );\n\t\t\t\treturn true;\n\n\t\t\t// Onto a porch: the flight stops standing on a floor and starts standing on a deck, which is all\n\t\t\t// that separates an inside stair from the steps off a veranda.\n\t\t\tcase (ArchStairPart stair, ArchPorchPart porch):\n\t\t\t\tif ( !Unfile( plan, stair ) ) return false;\n\t\t\t\tporch.Stairs.Add( stair );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchPillarPart pillar, ArchPorchPart porch):\n\t\t\t\tif ( !Unfile( plan, pillar ) ) return false;\n\t\t\t\tporch.Pillars.Add( pillar );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchTrimPart trim, ArchPorchPart porch):\n\t\t\t\tif ( !Unfile( plan, trim ) ) return false;\n\t\t\t\tporch.Trims.Add( trim );\n\t\t\t\treturn true;\n\n\t\t\t// A cut is world-space and stays filed where it was: nesting it under a porch says what it belongs\n\t\t\t// to, the way a walkway's deck record does, and moves nothing the generator reads.\n\t\t\tcase (ArchCutPart, ArchPorchPart):\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchBeamPart beam, ArchRoom room):\n\t\t\t\tif ( plan.AllRooms().FirstOrDefault( candidate => candidate.Beams.Contains( beam ) ) is not { } fromBeam ) return false;\n\t\t\t\tfromBeam.Beams.Remove( beam );\n\t\t\t\troom.Beams.Add( beam );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchPorchPart porch, ArchRoom room):\n\t\t\t\tif ( plan.AllRooms().FirstOrDefault( candidate => candidate.Porches.Contains( porch ) ) is not { } fromPorch ) return false;\n\t\t\t\tfromPorch.Porches.Remove( porch );\n\t\t\t\troom.Porches.Add( porch );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchApproachPart approach, ArchRoom room):\n\t\t\t\tif ( plan.AllRooms().FirstOrDefault( candidate => candidate.Approaches.Contains( approach ) ) is not { } fromApproach ) return false;\n\t\t\t\tfromApproach.Approaches.Remove( approach );\n\t\t\t\troom.Approaches.Add( approach );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchRoofPart roof, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Roofs.Contains( roof ) ) is not { } fromRoof ) return false;\n\t\t\t\tfromRoof.Roofs.Remove( roof );\n\t\t\t\tbuilding.Roofs.Add( roof );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchRoofLightPart light, ArchRoofPart roof):\n\t\t\t\tif ( plan.Buildings.SelectMany( candidate => candidate.Roofs ).FirstOrDefault( candidate => candidate.Lights.Contains( light ) ) is not { } fromLight ) return false;\n\t\t\t\tfromLight.Lights.Remove( light );\n\t\t\t\troof.Lights.Add( light );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchPlatformPart platform, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Platforms.Contains( platform ) ) is not { } fromPlatform ) return false;\n\t\t\t\tfromPlatform.Platforms.Remove( platform );\n\t\t\t\tbuilding.Platforms.Add( platform );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchStairPart stair, ArchPlatformPart platform):\n\t\t\t\tif ( !Unfile( plan, stair ) ) return false;\n\t\t\t\tplatform.Stairs.Add( stair );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchDownpipePart pipe, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Downpipes.Contains( pipe ) ) is not { } fromPipe ) return false;\n\t\t\t\tfromPipe.Downpipes.Remove( pipe );\n\t\t\t\tbuilding.Downpipes.Add( pipe );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchPipePart run, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Pipes.Contains( run ) ) is not { } fromRun ) return false;\n\t\t\t\tfromRun.Pipes.Remove( run );\n\t\t\t\tbuilding.Pipes.Add( run );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchPipeBracketPart bracket, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Brackets.Contains( bracket ) ) is not { } fromBracket ) return false;\n\t\t\t\tfromBracket.Brackets.Remove( bracket );\n\t\t\t\tbuilding.Brackets.Add( bracket );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchFencePart fence, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Fences.Contains( fence ) ) is not { } fromFence ) return false;\n\t\t\t\tfromFence.Fences.Remove( fence );\n\t\t\t\tbuilding.Fences.Add( fence );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchLadderPart ladder, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Ladders.Contains( ladder ) ) is not { } fromLadder ) return false;\n\t\t\t\tfromLadder.Ladders.Remove( ladder );\n\t\t\t\tbuilding.Ladders.Add( ladder );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchBalconyPart balcony, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Balconies.Contains( balcony ) ) is not { } fromBalcony ) return false;\n\t\t\t\tfromBalcony.Balconies.Remove( balcony );\n\t\t\t\tbuilding.Balconies.Add( balcony );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchExteriorStairPart flight, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.ExteriorStairs.Contains( flight ) ) is not { } fromFlight ) return false;\n\t\t\t\tfromFlight.ExteriorStairs.Remove( flight );\n\t\t\t\tbuilding.ExteriorStairs.Add( flight );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchCutPart cut, ArchBuilding building):\n\t\t\t\tif ( plan.Buildings.FirstOrDefault( candidate => candidate.Cuts.Contains( cut ) ) is not { } fromCut ) return false;\n\t\t\t\tfromCut.Cuts.Remove( cut );\n\t\t\t\tbuilding.Cuts.Add( cut );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchBridgePart bridge, ArchRoadPart road):\n\t\t\t\tif ( plan.Roads().FirstOrDefault( candidate => candidate.Bridges.Contains( bridge ) ) is not { } fromBridge ) return false;\n\t\t\t\tfromBridge.Bridges.Remove( bridge );\n\t\t\t\troad.Bridges.Add( bridge );\n\t\t\t\treturn true;\n\n\t\t\tcase (ArchTunnelPart tunnel, ArchRoadPart road):\n\t\t\t\tif ( plan.Roads().FirstOrDefault( candidate => candidate.Tunnels.Contains( tunnel ) ) is not { } fromTunnel ) return false;\n\t\t\t\tfromTunnel.Tunnels.Remove( tunnel );\n\t\t\t\troad.Tunnels.Add( tunnel );\n\t\t\t\treturn true;\n\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\t}\n\n\tpublic bool IsEnabled( object payload )\n\t{\n\t\treturn payload is null || byPayload.TryGetValue( payload, out var node ) && node.Enabled;\n\t}\n\n\tpublic bool IsEnabled( int id )\n\t{\n\t\treturn byId.TryGetValue( id, out var node ) && node.Enabled;\n\t}\n\n\tpublic bool IsLocked( int id )\n\t{\n\t\treturn byId.TryGetValue( id, out var node ) && node.Locked;\n\t}\n\n\tpublic bool IsLocked( object payload )\n\t{\n\t\treturn payload is not null && byPayload.TryGetValue( payload, out var node ) && node.Locked;\n\t}\n\n\t// LOCKED HERE OR ANYWHERE ABOVE. A lock is how an author says \"I have hand-edited these faces and the\n\t// generator is finished with them\", so locking a house has to freeze every wall in it - a rebuild that walked\n\t// into one of them would throw the edit away, which is the one thing the lock exists to prevent.\n\tpublic bool Frozen( int itemId )\n\t{\n\t\tif ( itemId == 0 || !byId.TryGetValue( itemId, out var found ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tfor ( var node = found; node is not null; node = node.Parent )\n\t\t{\n\t\t\tif ( node.Locked )\n\t\t\t{\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic bool AnyFrozen() => byId.Values.Any( node => node.Locked );\n\n\t// The messages a layer must surface: unresolved required links, missing children.\n\tpublic IReadOnlyList<string> Problems( ArchLayerNode node )\n\t{\n\t\tvar problems = new List<string>();\n\n\t\tif ( node?.Ref is not { } nodeRef )\n\t\t{\n\t\t\treturn problems;\n\t\t}\n\n\t\tforeach ( var link in Links.Where( link => link.Required && link.SourceId == nodeRef.ItemId ) )\n\t\t{\n\t\t\tif ( !byId.ContainsKey( link.TargetId ) )\n\t\t\t{\n\t\t\t\tproblems.Add( $\"Required link {link.SourcePort} \u2192 {link.TargetPort} (id {link.TargetId}) does not resolve.\" );\n\t\t\t}\n\t\t}\n\n\t\treturn problems;\n\t}\n\n\t// The ids a rebuild must touch when this layer changes: itself, its descendants, the hosts that\n\t// receive its owned effects, and required linked consumers. Generation still rebuilds the whole\n\t// scene today, but the dirty set is what partial regeneration will later replace.\n\tpublic IReadOnlySet<int> DirtyClosure( int itemId )\n\t{\n\t\tvar dirty = new HashSet<int> { itemId };\n\n\t\tif ( byId.TryGetValue( itemId, out var node ) )\n\t\t{\n\t\t\tCollectDescendants( node, dirty );\n\t\t}\n\n\t\tforeach ( var link in Links.Where( link => link.Required && link.TargetId == itemId ) )\n\t\t{\n\t\t\tdirty.Add( link.SourceId );\n\t\t}\n\n\t\tforeach ( var entry in byId.Values )\n\t\t{\n\t\t\tif ( OwnsEffectsOn( entry.Payload, itemId ) && entry.Ref is { } entryRef )\n\t\t\t{\n\t\t\t\tdirty.Add( entryRef.ItemId );\n\t\t\t}\n\t\t}\n\n\t\treturn dirty;\n\t}\n\n\tstatic void CollectDescendants( ArchLayerNode node, HashSet<int> into )\n\t{\n\t\tforeach ( var child in node.Children )\n\t\t{\n\t\t\tif ( child.Ref is { } childRef )\n\t\t\t{\n\t\t\t\tinto.Add( childRef.ItemId );\n\t\t\t}\n\n\t\t\tCollectDescendants( child, into );\n\t\t}\n\t}\n\n\t// A wall opening or slab cutout records who made it; disabling that owner has to dirty the host.\n\tstatic bool OwnsEffectsOn( object payload, int ownerId )\n\t{\n\t\treturn payload switch\n\t\t{\n\t\t\tArchBuilding building => building.Cutouts.Any( cutout => cutout.OwnerId == ownerId ),\n\t\t\tArchRoom room => room.Walls.SelectMany( wall => wall.Openings ).Any( opening => opening.OwnerId == ownerId ),\n\t\t\t_ => false\n\t\t};\n\t}\n\n\t// Builds the tree from typed ownership; explicit records override a payload's parent, kind and\n\t// stage when present. Old plans carry no records and project unchanged.\n\tpublic static ArchLayerTree Project( ArchPlan plan )\n\t{\n\t\tvar tree = new ArchLayerTree();\n\n\t\tif ( plan is null )\n\t\t{\n\t\t\treturn tree;\n\t\t}\n\n\t\tvar kinds = ArchKinds.Load();\n\t\tvar entries = new List<Entry>();\n\t\tvar byId = new Dictionary<int, Entry>();\n\n\t\tvoid Register( object payload, int id, int parent, ArchKind kind, ArchBuilding building, ArchRoom room,\n\t\t\tint floor = int.MinValue, ArchLayerStage? stage = null )\n\t\t{\n\t\t\tvar entry = new Entry\n\t\t\t{\n\t\t\t\tPayload = payload,\n\t\t\t\tId = id,\n\t\t\t\tParentId = parent,\n\t\t\t\tKind = kind,\n\t\t\t\tStage = stage,\n\t\t\t\tName = kinds.NameOf( kind, payload, id ),\n\t\t\t\tBuilding = building,\n\t\t\t\tRoom = room,\n\t\t\t\tFloor = floor,\n\t\t\t};\n\n\t\t\tentries.Add( entry );\n\t\t\tbyId[id] = entry;\n\t\t}\n\n\t\tforeach ( var building in plan.Buildings )\n\t\t{\n\t\t\tRegister( building, building.Id, 0, ArchKind.Building, building, null );\n\n\t\t\tforeach ( var room in building.Rooms )\n\t\t\t{\n\t\t\t\tvar kind = room.Spans ? ArchKind.Walkway : ArchKind.Room;\n\t\t\t\tRegister( room, room.Id, building.Id, kind, building, room, room.Floor );\n\n\t\t\t\tforeach ( var wall in room.Walls )\n\t\t\t\t{\n\t\t\t\t\tRegister( wall, wall.Id, room.Id, ArchKind.Wall, building, room );\n\n\t\t\t\t\tforeach ( var opening in wall.Openings )\n\t\t\t\t\t{\n\t\t\t\t\t\tRegister( opening, opening.Id, wall.Id, ArchKind.Opening, building, room );\n\t\t\t\t\t}\n\n\t\t\t\t\t// The stage is the payload's, not the kind's: a pilaster builds, a recess cuts.\n\t\t\t\t\tforeach ( var modifier in wall.Modifiers )\n\t\t\t\t\t{\n\t\t\t\t\t\tRegister( modifier, modifier.Id, wall.Id, ArchKind.WallMod, building, room,\n\t\t\t\t\t\t\tstage: modifier.Carves ? ArchLayerStage.Void : ArchLayerStage.Structure );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tforeach ( var stair in room.Stairs )\n\t\t\t\t{\n\t\t\t\t\tRegister( stair, stair.Id, room.Id, ArchKind.Stair, building, room );\n\t\t\t\t}\n\n\t\t\t\tforeach ( var trim in room.Trims )\n\t\t\t\t{\n\t\t\t\t\tRegister( trim, trim.Id, room.Id, ArchKind.Trim, building, room );\n\t\t\t\t}\n\n\t\t\t\tforeach ( var pillar in room.Pillars )\n\t\t\t\t{\n\t\t\t\t\tRegister( pillar, pillar.Id, room.Id, ArchKind.Pillar, building, room );\n\t\t\t\t}\n\n\t\t\t\tforeach ( var span in room.PierSpans )\n\t\t\t\t{\n\t\t\t\t\tRegister( span, span.Id, room.Id, ArchKind.Span, building, room );\n\t\t\t\t}\n\n\t\t\t\tforeach ( var beam in room.Beams )\n\t\t\t\t{\n\t\t\t\t\tRegister( beam, beam.Id, room.Id, ArchKind.Beam, building, room );\n\t\t\t\t}\n\n\t\t\t\tforeach ( var porch in room.Porches )\n\t\t\t\t{\n\t\t\t\t\tRegister( porch, porch.Id, room.Id, ArchKind.Porch, building, room );\n\n\t\t\t\t\t// A porch HOSTS, so its flights, columns and runs are rows under it rather than loose\n\t\t\t\t\t// siblings of the room's own - the same shape a walkway's contents take.\n\t\t\t\t\tforeach ( var stair in porch.Stairs )\n\t\t\t\t\t{\n\t\t\t\t\t\tRegister( stair, stair.Id, porch.Id, ArchKind.Stair, building, room );\n\t\t\t\t\t}\n\n\t\t\t\t\tforeach ( var pillar in porch.Pillars )\n\t\t\t\t\t{\n\t\t\t\t\t\tRegister( pillar, pillar.Id, porch.Id, ArchKind.Pillar, building, room );\n\t\t\t\t\t}\n\n\t\t\t\t\tforeach ( var trim in porch.Trims )\n\t\t\t\t\t{\n\t\t\t\t\t\tRegister( trim, trim.Id, porch.Id, ArchKind.Trim, building, room );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tforeach ( var approach in room.Approaches )\n\t\t\t\t{\n\t\t\t\t\tRegister( approach, approach.Id, room.Id, ArchKind.Approach, building, room );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tforeach ( var roof in building.Roofs )\n\t\t\t{\n\t\t\t\tRegister( roof, roof.Id, building.Id, ArchKind.Roof, building, null );\n\n\t\t\t\tforeach ( var light in roof.Lights )\n\t\t\t\t{\n\t\t\t\t\tRegister( light, light.Id, roof.Id, ArchKind.RoofLight, building, null );\n\t\t\t\t}\n\n\t\t\t\t// A wall standing on the deck is a Wall like any other, so it picks, edits and dresses through\n\t\t\t\t// every path a wall in a room already takes.\n\t\t\t\tforeach ( var wall in roof.Walls )\n\t\t\t\t{\n\t\t\t\t\tRegister( wall, wall.Id, roof.Id, ArchKind.Wall, building, null );\n\n\t\t\t\t\tforeach ( var opening in wall.Openings )\n\t\t\t\t\t{\n\t\t\t\t\t\tRegister( opening, opening.Id, wall.Id, ArchKind.Opening, building, null );\n\t\t\t\t\t}\n\n\t\t\t\t\tforeach ( var modifier in wall.Modifiers )\n\t\t\t\t\t{\n\t\t\t\t\t\tRegister( modifier, modifier.Id, wall.Id, ArchKind.WallMod, building, null,\n\t\t\t\t\t\t\tstage: modifier.Carves ? ArchLayerStage.Void : ArchLayerStage.Structure );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tforeach ( var pipe in building.Downpipes )\n\t\t\t{\n\t\t\t\tRegister( pipe, pipe.Id, building.Id, ArchKind.Downpipe, building, null );\n\t\t\t}\n\n\t\t\tforeach ( var run in building.Pipes )\n\t\t\t{\n\t\t\t\tRegister( run, run.Id, building.Id, ArchKind.Pipe, building, null );\n\t\t\t}\n\n\t\t\tforeach ( var bracket in building.Brackets )\n\t\t\t{\n\t\t\t\tRegister( bracket, bracket.Id, building.Id, ArchKind.Bracket, building, null );\n\t\t\t}\n\n\t\t\tforeach ( var fence in building.Fences )\n\t\t\t{\n\t\t\t\tRegister( fence, fence.Id, building.Id, ArchKind.Fence, building, null );\n\t\t\t}\n\n\t\t\tforeach ( var platform in building.Platforms )\n\t\t\t{\n\t\t\t\tRegister( platform, platform.Id, building.Id, ArchKind.Platform, building, null );\n\n\t\t\t\tforeach ( var stair in platform.Stairs )\n\t\t\t\t{\n\t\t\t\t\tRegister( stair, stair.Id, platform.Id, ArchKind.CarvedStair, building, null );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tforeach ( var ladder in building.Ladders )\n\t\t\t{\n\t\t\t\tRegister( ladder, ladder.Id, building.Id, ArchKind.Ladder, building, null );\n\t\t\t}\n\n\t\t\tforeach ( var balcony in building.Balconies )\n\t\t\t{\n\t\t\t\tRegister( balcony, balcony.Id, building.Id, ArchKind.Balcony, building, null );\n\t\t\t}\n\n\t\t\tforeach ( var flight in building.ExteriorStairs )\n\t\t\t{\n\t\t\t\tRegister( flight, flight.Id, building.Id, ArchKind.ExteriorStair, building, null );\n\t\t\t}\n\n\t\t\tforeach ( var cut in building.Cuts )\n\t\t\t{\n\t\t\t\tRegister( cut, cut.Id, building.Id, ArchKind.Cut, building, null,\n\t\t\t\t\tstage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );\n\t\t\t}\n\t\t}\n\n\t\tforeach ( var road in plan.Roads() )\n\t\t{\n\t\t\tRegister( road, road.Id, 0, ArchKind.Road, null, null );\n\n\t\t\tforeach ( var crossing in road.Crossings )\n\t\t\t{\n\t\t\t\tRegister( crossing, crossing.Id, road.Id, ArchKind.Crossing, null, null );\n\t\t\t}\n\n\t\t\tforeach ( var bridge in road.Bridges )\n\t\t\t{\n\t\t\t\tRegister( bridge, bridge.Id, road.Id, ArchKind.Bridge, null, null );\n\t\t\t}\n\n\t\t\tforeach ( var tunnel in road.Tunnels )\n\t\t\t{\n\t\t\t\tRegister( tunnel, tunnel.Id, road.Id, ArchKind.Tunnel, null, null );\n\t\t\t}\n\n\n\t\t\tforeach ( var cut in road.Cuts )\n\t\t\t{\n\t\t\t\tRegister( cut, cut.Id, road.Id, ArchKind.Cut, null, null,\n\t\t\t\t\tstage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );\n\t\t\t}\n\t\t}\n\n\t\t// Every OTHER top-level unit, which is how a kind an addon brought gets a row in the stack without this walk\n\t\t// naming it. A house and a street are walked above only because their children are shapes this assembly knows.\n\t\tforeach ( var unit in plan.Units )\n\t\t{\n\t\t\tif ( unit is ArchBuilding or ArchRoadPart )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Under whatever it says it hangs on, which for a unit stored flat is still the building it attached\n\t\t\t// to - where it is FILED and where it BELONGS are two questions, and the row answers the second.\n\t\t\tvar owner = kinds.Parent( unit.Kind, unit );\n\n\t\t\tRegister( unit, unit.Id, owner, unit.Kind, plan.FindBuilding( owner ), null,\n\t\t\t\tstage: kinds.Stage( unit.Kind, unit ) );\n\n\t\t\tforeach ( var cut in unit.Cuts )\n\t\t\t{\n\t\t\t\tRegister( cut, cut.Id, unit.Id, ArchKind.Cut, null, null,\n\t\t\t\t\tstage: cut.Mode != ArchZoneMode.Carve ? ArchLayerStage.Finish : ArchLayerStage.Void );\n\t\t\t}\n\t\t}\n\n\t\t// Explicit metadata overrides ownership defaults; a self-parent record is nonsense and falls back.\n\t\tforeach ( var record in plan.Layers )\n\t\t{\n\t\t\tif ( byId.TryGetValue( record.ItemId, out var entry ) && record.ParentId != record.ItemId )\n\t\t\t{\n\t\t\t\tentry.ParentId = record.ParentId;\n\t\t\t\tentry.Kind = record.Kind;\n\t\t\t\tentry.Stage = record.Stage;\n\t\t\t\tentry.Enabled = record.Enabled;\n\t\t\t\tentry.Locked = record.Locked;\n\t\t\t\tentry.Order = record.Order;\n\t\t\t}\n\t\t}\n\n\t\tvar nodes = new Dictionary<int, ArchLayerNode>();\n\n\t\tforeach ( var entry in entries )\n\t\t{\n\t\t\tvar node = new ArchLayerNode\n\t\t\t{\n\t\t\t\tRef = new ArchLayerRef { ItemId = entry.Id, Kind = entry.Kind, ParentId = entry.ParentId },\n\t\t\t\tKind = entry.Kind,\n\t\t\t\tStage = entry.Stage ?? kinds.Stage( entry.Kind ),\n\t\t\t\tDomain = kinds.Domain( entry.Kind ),\n\t\t\t\tPayload = entry.Payload,\n\t\t\t\tName = entry.Name,\n\t\t\t\tEnabled = entry.Enabled,\n\t\t\t\tLocked = entry.Locked,\n\t\t\t\tOrder = entry.Order,\n\t\t\t\tFloor = entry.Floor,\n\t\t\t\tBuilding = entry.Building,\n\t\t\t\tRoom = entry.Room,\n\t\t\t};\n\n\t\t\tnodes[entry.Id] = node;\n\t\t\ttree.byId[entry.Id] = node;\n\t\t\ttree.byPayload[entry.Payload] = node;\n\t\t}\n\n\t\t// A group is a folder, so its members MOVE into it rather than being listed twice - the whole\n\t\t// point of the scope is that a layer stands in exactly one of them. A group sits in the domain\n\t\t// its members came from, so grouping two houses does not empty the Buildings branch.\n\t\tvar memberOf = new Dictionary<int, ArchLayerNode>();\n\n\t\tforeach ( var assembly in plan.Assemblies )\n\t\t{\n\t\t\tvar node = new ArchLayerNode\n\t\t\t{\n\t\t\t\tRef = new ArchLayerRef { ItemId = assembly.Id, Kind = ArchKind.Assembly, ParentId = 0 },\n\t\t\t\tKind = ArchKind.Assembly,\n\t\t\t\tStage = kinds.Stage( ArchKind.Assembly ),\n\t\t\t\tDomain = DomainOf( assembly, byId, kinds ),\n\t\t\t\tPayload = assembly,\n\t\t\t\tName = assembly.Name,\n\t\t\t};\n\n\t\t\tnodes[assembly.Id] = node;\n\t\t\ttree.byId[assembly.Id] = node;\n\t\t\ttree.byPayload[assembly] = node;\n\n\t\t\tforeach ( var childId in assembly.Children.Where( childId => childId != assembly.Id ) )\n\t\t\t{\n\t\t\t\tmemberOf[childId] = node;\n\t\t\t}\n\t\t}\n\n\t\tforeach ( var instance in plan.Instances )\n\t\t{\n\t\t\tvar node = new ArchLayerNode\n\t\t\t{\n\t\t\t\tRef = new ArchLayerRef { ItemId = instance.Id, Kind = ArchKind.AssetInstance, ParentId = 0 },\n\t\t\t\tKind = ArchKind.AssetInstance,\n\t\t\t\tStage = kinds.Stage( ArchKind.AssetInstance ),\n\t\t\t\tDomain = ArchLayerDomain.Connections,\n\t\t\t\tPayload = instance,\n\t\t\t\tName = instance.Name,\n\t\t\t};\n\n\t\t\tnodes[instance.Id] = node;\n\t\t\ttree.byId[instance.Id] = node;\n\t\t\ttree.byPayload[instance] = node;\n\t\t\ttree.AddRoot( node );\n\t\t}\n\n\t\t// Story headers exist before any room attaches, so floors sort ascending under their building.\n\t\tforeach ( var building in plan.Buildings )\n\t\t{\n\t\t\tif ( !nodes.TryGetValue( building.Id, out var buildingNode ) )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tforeach ( var floor in entries\n\t\t\t\t.Where( entry => entry.ParentId == building.Id && (entry.Kind == ArchKind.Room || entry.Kind == ArchKind.Walkway) )\n\t\t\t\t.Select( entry => entry.Floor )\n\t\t\t\t.Distinct()\n\t\t\t\t.OrderBy( floor => floor ) )\n\t\t\t{\n\t\t\t\tvar story = new ArchLayerNode\n\t\t\t\t{\n\t\t\t\t\tKind = ArchKind.Story,\n\t\t\t\t\tStage = ArchLayerStage.Shape,\n\t\t\t\t\tDomain = ArchLayerDomain.Buildings,\n\t\t\t\t\tName = $\"Level {floor}\",\n\t\t\t\t\tFloor = floor,\n\t\t\t\t\tBuilding = building,\n\t\t\t\t};\n\n\t\t\t\tstory.Parent = buildingNode;\n\t\t\t\tbuildingNode.Children.Add( story );\n\t\t\t\ttree.stories[(building.Id, floor)] = story;\n\t\t\t}\n\t\t}\n\n\t\t// Where each layer would have stood WITHOUT its group, worked out even for a member that moves into one -\n\t\t// a group hangs where its members came from, so it needs to know where that was.\n\t\tvar typedHost = new Dictionary<int, ArchLayerNode>();\n\n\t\tforeach ( var entry in entries )\n\t\t{\n\t\t\tvar node = nodes[entry.Id];\n\t\t\tvar host = entry.ParentId != 0 && nodes.TryGetValue( entry.ParentId, out var parent ) ? parent : null;\n\n\t\t\tif ( host is not null\n\t\t\t\t&& host.Kind == ArchKind.Building\n\t\t\t\t&& (node.Kind == ArchKind.Room || node.Kind == ArchKind.Walkway)\n\t\t\t\t&& tree.stories.TryGetValue( (entry.ParentId, entry.Floor), out var story ) )\n\t\t\t{\n\t\t\t\thost = story;\n\t\t\t}\n\n\t\t\tif ( host is not null )\n\t\t\t{\n\t\t\t\ttypedHost[entry.Id] = host;\n\t\t\t}\n\n\t\t\t// Group membership outranks typed ownership: the walkway leaves the house that filed it.\n\t\t\tif ( memberOf.TryGetValue( entry.Id, out var group ) )\n\t\t\t{\n\t\t\t\tgroup.Children.Add( node );\n\t\t\t\tnode.Parent = group;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( host is null )\n\t\t\t{\n\t\t\t\ttree.AddRoot( node );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\thost.Children.Add( node );\n\t\t\tnode.Parent = host;\n\t\t}\n\n\t\t// A group may hold another group: the houses a connector merges gather above it, and the\n\t\t// connector is filed beneath the group of what it affects. Nested first and whole, or a\n\t\t// group listed before its holder would root itself and then be adopted as well.\n\t\tforeach ( var assembly in plan.Assemblies )\n\t\t{\n\t\t\tforeach ( var nested in plan.Assemblies.Where( inner => inner.Id != assembly.Id && assembly.Children.Contains( inner.Id ) ) )\n\t\t\t{\n\t\t\t\tvar child = nodes[nested.Id];\n\n\t\t\t\tnodes[assembly.Id].Children.Add( child );\n\t\t\t\tchild.Parent = nodes[assembly.Id];\n\t\t\t}\n\t\t}\n\n\t\t// An anchor names a layer the group reaches without owning - the far end of a connection.\n\t\t// Members already have a row, so only the outside ones are worth stating.\n\t\tforeach ( var assembly in plan.Assemblies )\n\t\t{\n\t\t\tvar node = nodes[assembly.Id];\n\n\t\t\t// Members sit in the order the group recorded them, which is the shape of the join.\n\t\t\tvar member = node.Children.OrderBy( child => Membership( assembly, child ) ).ToList();\n\n\t\t\tnode.Children.Clear();\n\t\t\tnode.Children.AddRange( member );\n\n\t\t\tforeach ( var link in plan.Links.Where( link => link.SourceId == assembly.Id ) )\n\t\t\t{\n\t\t\t\ttree.byId.TryGetValue( link.TargetId, out var targetNode );\n\n\t\t\t\t// An anchor pointing at something already in the group says nothing the rows above it\n\t\t\t\t// do not - and spelling out its whole path is how a tree turns into a wall of text.\n\t\t\t\tif ( targetNode is null || Within( targetNode, node ) )\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tnode.Children.Add( new ArchLayerNode\n\t\t\t\t{\n\t\t\t\t\tKind = ArchKind.Assembly,\n\t\t\t\t\tStage = ArchLayerStage.Reference,\n\t\t\t\t\tDomain = node.Domain,\n\t\t\t\t\tPayload = new ArchLayerReference { SourcePort = link.SourcePort, TargetId = link.TargetId, TargetPort = link.TargetPort },\n\t\t\t\t\tName = $\"{link.SourcePort} \u2192 {targetNode.Name}\",\n\t\t\t\t\tParent = node,\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\tif ( node.Parent is null && Homed( assembly, typedHost ) is { } home )\n\t\t\t{\n\t\t\t\thome.Children.Add( node );\n\t\t\t\tnode.Parent = home;\n\t\t\t}\n\n\t\t\tif ( node.Parent is null )\n\t\t\t{\n\t\t\t\ttree.AddRoot( node );\n\t\t\t}\n\t\t}\n\n\t\tforeach ( var group in tree.Domains )\n\t\t{\n\t\t\tSort( group.Children );\n\t\t}\n\n\t\ttree.Links = plan.Links;\n\n\t\treturn tree;\n\t}\n\n\t// Stage decides evaluation; Order only decides where siblings sit inside their stage, which is\n\t// what a drag in the stack rearranges.\n\tstatic void Sort( List<ArchLayerNode> children )\n\t{\n\t\tif ( children.Count > 1 )\n\t\t{\n\t\t\tvar ordered = children.OrderBy( child => child.Order ).ToList();\n\n\t\t\tchildren.Clear();\n\t\t\tchildren.AddRange( ordered );\n\t\t}\n\n\t\tforeach ( var child in children )\n\t\t{\n\t\t\tSort( child.Children );\n\t\t}\n\t}\n\n\tstatic bool Within( ArchLayerNode node, ArchLayerNode ancestor )\n\t{\n\t\tfor ( var current = node; current is not null; current = current.Parent )\n\t\t{\n\t\t\tif ( ReferenceEquals( current, ancestor ) )\n\t\t\t{\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t// A GROUP STANDS WHERE ITS MEMBERS STOOD. Folding five runs inside one house into a folder is a scope over\n\t// those rows, not a move to the top of the plan - rooting it there took them out of the house with it.\n\t//\n\t// Members drawn from two different hosts have no one home and root at the domain as before, and so does a group\n\t// whose members' host is itself a member, which would otherwise hang the folder inside its own contents.\n\tstatic ArchLayerNode Homed( ArchSiteAssembly assembly, Dictionary<int, ArchLayerNode> typedHost )\n\t{\n\t\tArchLayerNode home = null;\n\n\t\tforeach ( var childId in assembly.Children )\n\t\t{\n\t\t\tif ( !typedHost.TryGetValue( childId, out var host ) )\n\t\t\t{\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\thome ??= host;\n\n\t\t\tif ( !ReferenceEquals( home, host ) )\n\t\t\t{\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tfor ( var walk = home; walk is not null; walk = walk.Parent )\n\t\t{\n\t\t\tif ( walk.Ref is { } layer && assembly.Children.Contains( layer.ItemId ) )\n\t\t\t{\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\treturn home;\n\t}\n\n\tstatic int Membership( ArchSiteAssembly assembly, ArchLayerNode child )\n\t{\n\t\tvar at = child.Ref is { } layer ? assembly.Children.IndexOf( layer.ItemId ) : -1;\n\n\t\treturn at < 0 ? int.MaxValue : at;\n\t}\n\n\t// Where the folder sits: with whatever it holds, so a group of houses stays under Buildings.\n\tstatic ArchLayerDomain DomainOf( ArchSiteAssembly assembly, Dictionary<int, Entry> byId, ArchKinds kinds )\n\t{\n\t\tforeach ( var childId in assembly.Children )\n\t\t{\n\t\t\tif ( byId.TryGetValue( childId, out var entry ) )\n\t\t\t{\n\t\t\t\treturn kinds.Domain( entry.Kind );\n\t\t\t}\n\t\t}\n\n\t\treturn ArchLayerDomain.Connections;\n\t}\n\n\tvoid AddRoot( ArchLayerNode node )\n\t{\n\t\tvar group = Domains.FirstOrDefault( domain => domain.Domain == node.Domain );\n\n\t\tif ( group is null )\n\t\t{\n\t\t\tgroup = new ArchLayerDomainGroup\n\t\t\t{\n\t\t\t\tDomain = node.Domain,\n\t\t\t\tName = node.Domain switch\n\t\t\t\t{\n\t\t\t\t\tArchLayerDomain.Buildings => \"Buildings\",\n\t\t\t\t\tArchLayerDomain.Connections => \"Connections\",\n\t\t\t\t\t_ => \"Infrastructure\"\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tDomains.Add( group );\n\t\t}\n\n\t\tgroup.Children.Add( node );\n\t}\n\n\tsealed class Entry\n\t{\n\t\tpublic object Payload;\n\t\tpublic int Id;\n\t\tpublic int ParentId;\n\t\tpublic ArchKind Kind;\n\t\tpublic ArchLayerStage? Stage;\n\t\tpublic bool Enabled = true;\n\t\tpublic bool Locked;\n\t\tpublic int Order;\n\t\tpublic string Name;\n\t\tpublic int Floor = int.MinValue;\n\t\tpublic ArchBuilding Building;\n\t\tpublic ArchRoom Room;\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Output/ArchCull.Interiors.cs",
            "FileName": "ArchCull.Interiors.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// One shallow room behind one opening on a facade nobody can walk into. DERIVED, so it carries no id and no\n// layer record - exactly as slabs, ceilings, roof decks and platforms carry none.\npublic sealed class ArchFalseInterior\n{\n\tpublic ArchBuilding Building { get; init; }\n\tpublic ArchRoom Room { get; init; }\n\tpublic ArchWall Wall { get; init; }\n\tpublic ArchOpening Opening { get; init; }\n\t// The room face of the wall, in the wall's own frame - the plane the box is measured off.\n\tpublic float Face { get; init; }\n\tpublic float Depth { get; init; }\n\n\tpublic float Back => Face + Depth;\n}\n\npublic static partial class ArchCull\n{\n\t// No way in and nothing above, asked of a whole unit rather than one of its roofs: a shell with no door\n\t// and no archway, capped everywhere, is a facade. Anything else is a room, or becoming one.\n\tpublic static bool Unenterable( ArchBuilding building, ArchPlan plan, ArchKit kit )\n\t{\n\t\tif ( building is null )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tvar walls = building.Rooms.SelectMany( room => room.Walls.Select( wall => (Room: room, Wall: wall) ) ).ToList();\n\n\t\tif ( walls.Count == 0 )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\t// A single-sided shell emits no room face at all, so there is nothing in there to be in.\n\t\tif ( walls.All( standing => ArchWallSection.SingleSided( standing.Wall, standing.Room ) ) )\n\t\t{\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( walls.SelectMany( standing => standing.Wall.Openings ).Any( Entered ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\treturn building.Roofs.Count > 0 && building.Roofs.All( roof => Sealed( roof, building, plan, kit ) );\n\t}\n\n\t// A leaf hangs in a way in and an archway is a hole walked straight through. A window is neither.\n\tstatic bool Entered( ArchOpening opening )\n\t{\n\t\treturn opening.Kind.Hangs() || opening.Kind == OpeningKind.Archway;\n\t}\n\n\t// Every box the plan asks for, resolved without a scene, so the pass, the report and the test all read one\n\t// answer. A unit ArchCull calls enterable contributes none: behind a real room the box would z-fight the\n\t// room it is standing inside.\n\tpublic static List<ArchFalseInterior> Behind( ArchPlan plan, ArchKit kit )\n\t{\n\t\tvar found = new List<ArchFalseInterior>();\n\n\t\tif ( plan is null )\n\t\t{\n\t\t\treturn found;\n\t\t}\n\n\t\tvar depth = MathF.Max( 1f, kit.FalseInteriorDepth );\n\n\t\tforeach ( var building in plan.Buildings.Where( unit => Unenterable( unit, plan, kit ) ) )\n\t\t{\n\t\t\tforeach ( var room in building.Rooms )\n\t\t\t{\n\t\t\t\tforeach ( var wall in room.Walls.Where( wall => !ArchWallJoins.CoveredBy( building, room, wall ) ) )\n\t\t\t\t{\n\t\t\t\t\tvar face = ArchWallSection.Thickness( wall, kit ) * 0.5f;\n\n\t\t\t\t\tfound.AddRange( Showing( wall ).Select( opening => new ArchFalseInterior\n\t\t\t\t\t{\n\t\t\t\t\t\tBuilding = building,\n\t\t\t\t\t\tRoom = room,\n\t\t\t\t\t\tWall = wall,\n\t\t\t\t\t\tOpening = opening,\n\t\t\t\t\t\tFace = face,\n\t\t\t\t\t\tDepth = depth\n\t\t\t\t\t} ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn found;\n\t}\n\n\t// The units the wall generator actually cut a hole for - the same filter it runs, because a box behind a\n\t// hole nothing opened is a box hanging in a solid wall.\n\tstatic IEnumerable<ArchOpening> Showing( ArchWall wall )\n\t{\n\t\treturn wall.Openings\n\t\t\t.Where( opening => opening.Width > 0.5f && opening.Height > 0.5f )\n\t\t\t.Where( opening => ArchLayerGate.On( opening ) && ArchLayerGate.Owned( opening.OwnerId ) )\n\t\t\t.Where( opening => opening.SwallowedBy == 0 || !ArchLayerGate.Owned( opening.SwallowedBy ) );\n\t}\n\n\t// The box, in the wall's own frame, wound to face the hole. Its front IS the hole: a face on the wall's\n\t// room plane would be the sticker this exists to remove.\n\tpublic static void Line( ArchMesh canvas, ArchFalseInterior interior, ArchBrush brush )\n\t{\n\t\tvar opening = interior.Opening;\n\t\tvar left = opening.Left;\n\t\tvar right = opening.Right;\n\t\tvar bottom = MathF.Max( 0f, opening.SillHeight );\n\t\tvar top = opening.Top;\n\t\tvar face = interior.Face;\n\t\tvar back = interior.Back;\n\n\t\tif ( right - left < 0.5f || top - bottom < 0.5f || interior.Depth < 0.5f )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tcanvas.Quad(\n\t\t\tnew Vector3( left, back, bottom ),\n\t\t\tnew Vector3( right, back, bottom ),\n\t\t\tnew Vector3( right, back, top ),\n\t\t\tnew Vector3( left, back, top ),\n\t\t\tbrush );\n\n\t\tcanvas.Quad(\n\t\t\tnew Vector3( left, face, bottom ),\n\t\t\tnew Vector3( left, back, bottom ),\n\t\t\tnew Vector3( left, back, top ),\n\t\t\tnew Vector3( left, face, top ),\n\t\t\tbrush );\n\n\t\tcanvas.Quad(\n\t\t\tnew Vector3( right, back, bottom ),\n\t\t\tnew Vector3( right, face, bottom ),\n\t\t\tnew Vector3( right, face, top ),\n\t\t\tnew Vector3( right, back, top ),\n\t\t\tbrush );\n\n\t\tcanvas.Quad(\n\t\t\tnew Vector3( left, face, top ),\n\t\t\tnew Vector3( left, back, top ),\n\t\t\tnew Vector3( right, back, top ),\n\t\t\tnew Vector3( right, face, top ),\n\t\t\tbrush );\n\n\t\tcanvas.Quad(\n\t\t\tnew Vector3( left, back, bottom ),\n\t\t\tnew Vector3( left, face, bottom ),\n\t\t\tnew Vector3( right, face, bottom ),\n\t\t\tnew Vector3( right, back, bottom ),\n\t\t\tbrush );\n\t}\n\n\t// The pass on its own, for when the faces have already been cleaned. A rebuild prunes what it emits, the\n\t// same way it takes back the faces Clean removed.\n\tpublic static int Interiors( Scene scene, ArchPlan plan, ArchKit kit )\n\t{\n\t\tvar root = ArchScene.FindRoot( scene );\n\n\t\tif ( !root.IsValid() )\n\t\t{\n\t\t\tLog.Warning( \"Architecture: nothing to line - no generated root in this scene.\" );\n\n\t\t\treturn 0;\n\t\t}\n\n\t\tusing ( SceneEditorSession.Active.UndoScope( \"Line False Interiors\" ).WithGameObjectChanges( root, GameObjectUndoFlags.All ).Push() )\n\t\t{\n\t\t\treturn Lined( root, plan, kit );\n\t\t}\n\t}\n\n\t// Re-derived every run - a door added to a facade makes it a room, and the box behind its window has to GO rather\n\t// than be left standing inside it - but only HANDED OVER where the box actually changed. Drawing one is five\n\t// quads; giving it to the engine cooks a collision hull, a physics mesh and a trace mesh, and this pass reaches\n\t// every window on every facade in the plan.\n\t//\n\t// A null cache lines them all, which is what the menu action and a scene nobody has built through mean.\n\tpublic static int Lined( GameObject root, ArchPlan plan, ArchKit kit, ArchBuildCache cache = null )\n\t{\n\t\tvar nodes = Walls( root );\n\t\tvar style = new ArchStyle( kit );\n\t\tvar wanted = Behind( plan, kit ).GroupBy( interior => interior.Wall.Id ).ToDictionary( group => group.Key, group => group.ToList() );\n\t\tvar lined = 0;\n\n\t\tforeach ( var bare in nodes.Where( entry => !wanted.ContainsKey( entry.Key ) ) )\n\t\t{\n\t\t\tStrip( bare.Value );\n\t\t\tcache?.Unlined( bare.Key );\n\t\t}\n\n\t\tforeach ( var group in wanted )\n\t\t{\n\t\t\tif ( !nodes.TryGetValue( group.Key, out var node ) )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// The wall's own frame, so the boxes are drawn in the coordinates the wall generator uses and the\n\t\t\t// projection puts them on the same grain as the room face they hang behind.\n\t\t\tvar canvas = new ArchMesh( node.WorldTransform );\n\n\t\t\tforeach ( var interior in group.Value )\n\t\t\t{\n\t\t\t\tLine( canvas, interior, style.Brush(\n\t\t\t\t\tArchSurface.WallInterior, interior.Wall.Palette, interior.Room.Palette, interior.Building.Palette ) );\n\n\t\t\t\tlined++;\n\t\t\t}\n\n\t\t\t// Asked of the box's own key, the way a part is - and of the scene as well, because a hand-delete leaves\n\t\t\t// the cache saying something is standing that is not.\n\t\t\tif ( cache?.Lined( group.Key, canvas.Content ) == true && Standing( node ) )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tStrip( node );\n\t\t\tFit( node, canvas );\n\t\t}\n\n\t\treturn lined;\n\t}\n\n\tstatic void Strip( GameObject wall )\n\t{\n\t\tif ( wall.Children.FirstOrDefault( child => child.Name == ArchPieces.Interior ) is { } standing )\n\t\t{\n\t\t\tstanding.DestroyImmediate();\n\t\t}\n\t}\n\n\tstatic bool Standing( GameObject wall )\n\t{\n\t\treturn wall.Children.Any( child => child.Name == ArchPieces.Interior );\n\t}\n\n\tstatic Dictionary<int, GameObject> Walls( GameObject root )\n\t{\n\t\tvar nodes = new Dictionary<int, GameObject>();\n\n\t\tforeach ( var node in ArchScene.Descendants( root ) )\n\t\t{\n\t\t\tif ( ArchNames.TryParseId( node.Name, \"Wall\", out var id ) )\n\t\t\t{\n\t\t\t\tnodes[id] = node;\n\t\t\t}\n\t\t}\n\n\t\treturn nodes;\n\t}\n\n\tstatic void Fit( GameObject wall, ArchMesh canvas )\n\t{\n\t\tif ( canvas.IsEmpty )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tvar node = wall.Scene.CreateObject();\n\n\t\tnode.Name = ArchPieces.Interior;\n\t\tnode.SetParent( wall, false );\n\t\tnode.Tags.Add( ArchScene.GeneratedTag );\n\n\t\tvar renderer = node.Components.GetOrCreate<MeshComponent>();\n\n\t\trenderer.Color = Color.White;\n\t\trenderer.SmoothingAngle = 0f;\n\n\t\t// It must not be traceable: the cleaning pass reads cover by ray, and a box behind a window it could\n\t\t// hit would have the elevation in front of it stripped as buried.\n\t\tArchCollision.Write( node, renderer, canvas, ArchCollisionMode.None );\n\n\t\trenderer.Mesh = canvas.Finish();\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Carve/ArchCarveVolume.cs",
            "FileName": "ArchCarveVolume.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// The fall is what makes a raked deck a carveable solid - not a lofted quad.\npublic readonly struct ArchCarvePlane\n{\n\tpublic float Datum { get; init; }\n\tpublic Vector2 Origin { get; init; }\n\tpublic Vector2 Fall { get; init; }\n\n\tpublic static ArchCarvePlane Level( float height ) => new() { Datum = height };\n\n\tpublic static ArchCarvePlane Through( Vector2 origin, float datum, Vector2 fall )\n\t{\n\t\treturn new ArchCarvePlane { Origin = origin, Datum = datum, Fall = fall };\n\t}\n\n\tpublic bool Rakes => Fall.Length > 0.0001f;\n\n\tpublic float At( Vector2 point ) => Datum + Vector2.Dot( Fall, point - Origin );\n\n\tpublic ArchCarvePlane Raised( float by ) => new() { Datum = Datum + by, Origin = Origin, Fall = Fall };\n\n\t// Where a ray meets this plane. A raked plane and a ray are both linear, so the crossing solves in closed\n\t// form; every deck a cursor can come to rest on is measured through here so a roof and a ramp cannot answer\n\t// the same ray two different ways.\n\tpublic bool Crosses( Ray ray, out float reach, out Vector3 hit )\n\t{\n\t\treach = 0f;\n\t\thit = default;\n\n\t\tvar closing = ray.Forward.z - Vector2.Dot( Fall, new Vector2( ray.Forward.x, ray.Forward.y ) );\n\n\t\tif ( MathF.Abs( closing ) < 0.0001f )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\treach = (At( new Vector2( ray.Position.x, ray.Position.y ) ) - ray.Position.z) / closing;\n\n\t\tif ( reach < 0f )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\thit = ray.Position + ray.Forward * reach;\n\n\t\treturn true;\n\t}\n}\n\n// A ruined edge, carried by the volume that takes the bite. The seed is the cut's own id, so the same shaft\n// hands back the same ruin after a hotload and the build cache is not defeated by a vertex that moved.\npublic readonly struct ArchCarveBreak\n{\n\tpublic int Seed { get; init; }\n\tpublic float Jitter { get; init; }\n\n\tpublic bool Breaks => Seed != 0 && Jitter > ArchGridService.FinestSize;\n}\n\n// Both sides of a carve - the solid and the cut - are this same volume.\npublic readonly struct ArchCarveVolume\n{\n\tpublic IReadOnlyList<Vector2> Footprint { get; init; }\n\tpublic ArchCarvePlane Floor { get; init; }\n\tpublic ArchCarvePlane Ceiling { get; init; }\n\tpublic ArchCarveBreak Break { get; init; }\n\n\tpublic static ArchCarveVolume Over( IReadOnlyList<Vector2> footprint, float from, float to )\n\t{\n\t\treturn new ArchCarveVolume\n\t\t{\n\t\t\tFootprint = footprint,\n\t\t\tFloor = ArchCarvePlane.Level( MathF.Min( from, to ) ),\n\t\t\tCeiling = ArchCarvePlane.Level( MathF.Max( from, to ) )\n\t\t};\n\t}\n\n\tpublic ArchCarveVolume Breaking( ArchCarveBreak breaking )\n\t{\n\t\treturn new ArchCarveVolume { Footprint = Footprint, Floor = Floor, Ceiling = Ceiling, Break = breaking };\n\t}\n\n\t// A level base under a raked top - the wedge a ramp is. Not Raked(): that is two parallel planes a constant\n\t// thickness apart, which is a sloping slab and not something whose foot meets the ground it stands on.\n\tpublic static ArchCarveVolume Under( IReadOnlyList<Vector2> footprint, float from, ArchCarvePlane top )\n\t{\n\t\treturn new ArchCarveVolume { Footprint = footprint, Floor = ArchCarvePlane.Level( from ), Ceiling = top };\n\t}\n\n\t// The same wedge upside down, and the shape a SUBTRACTED ramp is: nothing taken out at the head, the whole\n\t// band gone at the foot, so what is left under it keeps the raked floor as its own top.\n\tpublic static ArchCarveVolume Above( IReadOnlyList<Vector2> footprint, ArchCarvePlane floor, float to )\n\t{\n\t\treturn new ArchCarveVolume { Footprint = footprint, Floor = floor, Ceiling = ArchCarvePlane.Level( to ) };\n\t}\n\n\t// The two faces are parallel, which is what keeps the interval algebra one-dimensional.\n\tpublic static ArchCarveVolume Raked( IReadOnlyList<Vector2> footprint, ArchCarvePlane plane, float thickness )\n\t{\n\t\tvar depth = MathF.Max( 0.05f, thickness );\n\n\t\treturn new ArchCarveVolume { Footprint = footprint, Floor = plane, Ceiling = plane.Raised( depth ) };\n\t}\n\n\tpublic bool Rakes => Floor.Rakes || Ceiling.Rakes;\n\n\tpublic bool Covers( Vector2 point ) => ArchFootprint.Encloses( new[] { Footprint }, point );\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Data/ArchArchetypeSteps.cs",
            "FileName": "ArchArchetypeSteps.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "namespace Sunless.Architecture;\n\npublic readonly record struct ArchArchetypeStepContext(\n\tArchPlan Plan,\n\tArchBuilding Building,\n\tArchKit Kit,\n\tArchArchetypeStep Step,\n\tVector2 Min,\n\tVector2 Max,\n\tArchRoom Room,\n\tList<string> Skipped );\n\npublic interface IArchArchetypeStepBuilder\n{\n\tArchStepKind Kind { get; }\n\n\tstring Build( ArchArchetypeStepContext context );\n}\n\npublic sealed class ArchArchetypeSteps : ArchTable<ArchArchetypeSteps, ArchStepKind, IArchArchetypeStepBuilder>\n{\n\tprotected override IEnumerable<IArchArchetypeStepBuilder> Standing()\n\t{\n\t\tyield return new ArchPorchArchetypeStep();\n\t}\n\n\tprotected override ArchStepKind KeyOf( IArchArchetypeStepBuilder builder ) => builder.Kind;\n\n\tprotected override string Collision( IArchArchetypeStepBuilder standing, IArchArchetypeStepBuilder builder )\n\t{\n\t\treturn $\"{builder.Kind} is built by both {standing.GetType().Name} and {builder.GetType().Name}\";\n\t}\n\n\tpublic IArchArchetypeStepBuilder For( ArchStepKind kind ) => Held( kind );\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Data/ArchBuild.cs",
            "FileName": "ArchBuild.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// Auto lays the ridge along the longer side, so the flat ends land on the short walls.\npublic enum RidgeRun\n{\n\tAuto,\n\tAlongX,\n\tAlongY\n}\n\npublic enum SectionRoof\n{\n\tContinue,\n\tHip,\n\tGable,\n\tCapped,\n\tNone\n}\n\npublic sealed class ArchSection\n{\n\tpublic ArchRoom Room { get; init; }\n\tpublic ArchRoofPart Roof { get; init; }\n\tpublic int SharedWalls { get; init; }\n\tpublic bool Merged { get; init; }\n}\n\n// Shared by the Building subtool and the arch_* tools - both grow a plan the same way.\npublic static class ArchBuild\n{\n\t// THE STOREY GRID, and the only answer to it. A building's own override first; otherwise the storey it\n\t// actually STANDS - its ground rooms' real plate plus a floor - never the one the kit would have stood. An\n\t// archetype gives its shell taller walls than the kit's, and a grid that never learns seats the next floor\n\t// mid-wall inside the storey below, with every column under it standing straight through the slab.\n\tpublic static float StoreyHeight( ArchBuilding building, ArchKit kit )\n\t{\n\t\tif ( building is { StoreyHeight: > 1f } )\n\t\t{\n\t\t\treturn building.StoreyHeight;\n\t\t}\n\n\t\treturn Ground( building, kit ) + kit.FloorThickness;\n\t}\n\n\tpublic static float FloorOf( ArchBuilding building, ArchKit kit, int level )\n\t{\n\t\treturn level * StoreyHeight( building, kit ) + kit.GroundClearance;\n\t}\n\n\t// The tallest room on the ground, because a storey is as tall as the walls the floor above lands on. A\n\t// building with nothing standing yet is the kit's, which is what the first drag needs.\n\tstatic float Ground( ArchBuilding building, ArchKit kit )\n\t{\n\t\tvar standing = 0f;\n\n\t\tforeach ( var room in building?.Rooms ?? Enumerable.Empty<ArchRoom>() )\n\t\t{\n\t\t\tif ( room.Floor != 0 || room.Spans )\n\t\t\t{\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tstanding = MathF.Max( standing, ArchFloorGen.WallHeight( room, kit ) );\n\t\t}\n\n\t\treturn standing > 1f ? standing : kit.WallHeight;\n\t}\n\n\tpublic static ArchSection Shell(\n\t\tArchPlan plan,\n\t\tArchBuilding building,\n\t\tArchKit kit,\n\t\tint level,\n\t\tfloat baseHeight,\n\t\tVector2 min,\n\t\tVector2 max,\n\t\tbool floor,\n\t\tRoofStyle? style,\n\t\tbool gutters,\n\t\tRidgeRun ridge = RidgeRun.Auto )\n\t{\n\t\tvar placement = new ArchBoundaryPlacementService( plan, kit ).Outside( level, min, max );\n\n\t\tif ( !placement.IsUsable )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\tmin = placement.Min;\n\t\tmax = placement.Max;\n\t\tvar shell = CreateRoom( plan, building, \"Shell\", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );\n\n\t\tif ( style is null )\n\t\t{\n\t\t\treturn new ArchSection { Room = shell };\n\t\t}\n\n\t\tvar roof = Roof( plan, kit, level, style.Value, min, max, baseHeight + kit.WallHeight, gutters, false, ridge );\n\t\tbuilding.Roofs.Add( roof );\n\n\t\treturn new ArchSection { Room = shell, Roof = roof };\n\t}\n\n\tpublic static ArchSection Partition(\n\t\tArchPlan plan,\n\t\tArchBuilding building,\n\t\tArchKit kit,\n\t\tint level,\n\t\tfloat baseHeight,\n\t\tVector2 min,\n\t\tVector2 max,\n\t\tbool floor )\n\t{\n\t\t( min, max ) = new ArchGridService().Rectangle( min, max );\n\t\tvar party = new List<(Vector2 From, Vector2 To)>();\n\t\tvar room = CreateRoom( plan, building, $\"Room{building.Rooms.Count + 1}\", level, baseHeight, kit.WallHeight, min, max, floor, party, kit.WallThickness );\n\n\t\treturn new ArchSection { Room = room, SharedWalls = party.Count };\n\t}\n\n\t// Two buildings on one plot each roof their own storey and the pair clip each other into nonsense.\n\tpublic static bool Stacks( ArchBuilding building, ArchKit kit, Vector2 min, Vector2 max )\n\t{\n\t\tif ( building is null || !building.HasContent )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tvar covered = ArchRegion.Shell( ArchRegion.Footprints( building.Rooms ), kit.WallThickness );\n\n\t\treturn ArchRegion.Covers( covered, ArchFootprint.Rect( min, max ) );\n\t}\n\n\tpublic static ArchSection Storey(\n\t\tArchPlan plan,\n\t\tArchBuilding building,\n\t\tArchKit kit,\n\t\tint level,\n\t\tfloat baseHeight,\n\t\tVector2 min,\n\t\tVector2 max,\n\t\tbool floor,\n\t\tRoofStyle? style,\n\t\tbool gutters,\n\t\tRidgeRun ridge = RidgeRun.Auto )\n\t{\n\t\t( min, max ) = new ArchGridService().Rectangle( min, max );\n\t\tvar snapped = new ArchWingPlacementService( building, kit, level ).Snap( min, max );\n\t\tmin = snapped.Min;\n\t\tmax = snapped.Max;\n\n\t\tvar room = CreateRoom( plan, building, $\"Storey{level}\", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );\n\t\tvar plate = baseHeight + kit.WallHeight;\n\t\tvar outline = ArchFootprint.Rect( min, max );\n\n\t\tif ( DeckBuiltOverBy( building, outline ) is { } below )\n\t\t{\n\t\t\tbelow.Level = level;\n\t\t\tbelow.BaseHeight = plate;\n\t\t\tbelow.Reshape( outline );\n\n\t\t\treturn new ArchSection { Room = room, Roof = below, Merged = true };\n\t\t}\n\n\t\tif ( style is null )\n\t\t{\n\t\t\treturn new ArchSection { Room = room };\n\t\t}\n\n\t\tvar roof = Roof( plan, kit, level, style.Value, min, max, plate, gutters, false, ridge );\n\t\tbuilding.Roofs.Add( roof );\n\n\t\treturn new ArchSection { Room = room, Roof = roof };\n\t}\n\n\t// Only a FULLY covered deck is rebuilt - a partial one is a real stepped building.\n\tstatic ArchRoofPart DeckBuiltOverBy( ArchBuilding building, List<Vector2> outline )\n\t{\n\t\treturn building.Roofs.FirstOrDefault( roof => ArchRegion.Covers( new[] { outline }, roof.Outline() ) );\n\t}\n\n\t// Continuing folds the wing into the section's footprint (valleys inside); otherwise its own roof, stepped by the eave drop.\n\tpublic static ArchSection Extend(\n\t\tArchPlan plan,\n\t\tArchBuilding building,\n\t\tArchKit kit,\n\t\tint level,\n\t\tfloat baseHeight,\n\t\tVector2 min,\n\t\tVector2 max,\n\t\tSectionRoof choice,\n\t\tfloat drop,\n\t\tbool floor,\n\t\tbool gutters,\n\t\tRidgeRun ridge = RidgeRun.Auto )\n\t{\n\t\t( min, max ) = new ArchGridService().Rectangle( min, max );\n\n\t\treturn new ArchWingGenerationService( plan, building, kit )\n\t\t\t.On( level, baseHeight, min, max )\n\t\t\t.WithRoof( choice, drop, gutters, ridge )\n\t\t\t.WithFloor( floor )\n\t\t\t.Create();\n\t}\n\n\tpublic static ArchRoofPart Roof(\n\t\tArchPlan plan,\n\t\tArchKit kit,\n\t\tint level,\n\t\tRoofStyle style,\n\t\tVector2 min,\n\t\tVector2 max,\n\t\tfloat baseHeight,\n\t\tbool gutters,\n\t\tbool parapet,\n\t\tRidgeRun ridge = RidgeRun.Auto )\n\t{\n\t\treturn new ArchRoofPart\n\t\t{\n\t\t\tId = plan.AllocateId(),\n\t\t\tName = \"Roof\",\n\t\t\tLevel = level,\n\t\t\tStyle = style,\n\t\t\tMin = min,\n\t\t\tMax = max,\n\t\t\tFootprint = ArchFootprint.Rect( min, max ),\n\t\t\tBaseHeight = baseHeight,\n\t\t\tPitch = kit.RoofPitch,\n\t\t\tRidgeAlongX = Ridged( ridge, min, max ),\n\t\t\t// A parapet stands on the wall line - an overhang would leave the coping floating.\n\t\t\tOverhang = parapet ? 0f : kit.RoofOverhang,\n\t\t\tThickness = kit.RoofThickness,\n\t\t\tGutters = gutters && !parapet,\n\t\t\tFascia = !parapet,\n\t\t\tSoffit = !parapet,\n\t\t\tParapet = parapet,\n\t\t\tCeiling = Ceiling( style )\n\t\t};\n\t}\n\n\t// Shared with the ghost, so what is drawn is what gets built.\n\tpublic static bool Ridged( RidgeRun ridge, Vector2 min, Vector2 max ) => ridge switch\n\t{\n\t\tRidgeRun.AlongX => true,\n\t\tRidgeRun.AlongY => false,\n\t\t_ => max.x - min.x >= max.y - min.y\n\t};\n\n\t// Hip/gable leave a void, so they get a ceiling; shed/sawtooth are meant to be seen from underneath.\n\tstatic bool Ceiling( RoofStyle style )\n\t{\n\t\treturn style is RoofStyle.Hip or RoofStyle.Gable;\n\t}\n\n\t// The one mapping for a wing's roof choice - the ghost, the generator and the re-dress must agree.\n\tpublic static RoofStyle Winged( SectionRoof choice )\n\t{\n\t\treturn choice switch\n\t\t{\n\t\t\tSectionRoof.Gable => RoofStyle.Gable,\n\t\t\tSectionRoof.Capped => RoofStyle.Flat,\n\t\t\t_ => RoofStyle.Hip\n\t\t};\n\t}\n\n\tinternal static ArchRoom CreateRoom(\n\t\tArchPlan plan,\n\t\tArchBuilding building,\n\t\tstring name,\n\t\tint level,\n\t\tfloat baseHeight,\n\t\tfloat wallHeight,\n\t\tVector2 min,\n\t\tVector2 max,\n\t\tbool floor,\n\t\tList<(Vector2 From, Vector2 To)> shared,\n\t\tfloat thickness )\n\t{\n\t\tvar room = new ArchRoom\n\t\t{\n\t\t\tId = plan.AllocateId(),\n\t\t\tName = name,\n\t\t\tFloor = level,\n\t\t\tBaseHeight = baseHeight,\n\t\t\tWallHeight = wallHeight,\n\t\t\tHasFloor = floor,\n\t\t\tFootprint = ArchFootprint.Rect( min, max )\n\t\t};\n\n\t\tfor ( var index = 0; index < room.Footprint.Count; index++ )\n\t\t{\n\t\t\tvar start = room.Footprint[index];\n\t\t\tvar end = room.Footprint[(index + 1) % room.Footprint.Count];\n\n\t\t\troom.Walls.Add( new ArchWall\n\t\t\t{\n\t\t\t\tId = plan.AllocateId(),\n\t\t\t\tStart = start,\n\t\t\t\tEnd = end,\n\t\t\t\tExterior = true,\n\t\t\t\tCap = true\n\t\t\t} );\n\n\t\t\tif ( shared is not null && ArchWallJoins.PartyWallExists( building, room.BaseHeight, start, end, thickness ) )\n\t\t\t{\n\t\t\t\tshared.Add( (start, end) );\n\t\t\t}\n\t\t}\n\n\t\tbuilding.Rooms.Add( room );\n\n\t\treturn room;\n\t}\n\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Data/ArchCatalogs.cs",
            "FileName": "ArchCatalogs.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "namespace Sunless.Architecture;\n\n// Which shelf a catalog stands on. A role rather than an interface per catalog, because the catalogs differ only in\n// what they hold - a module brings its own with new ArchShelf( role ), and the reservation only stops two of them\n// meaning the same thing.\npublic readonly record struct ArchShelf( string Role )\n{\n\tpublic static readonly ArchShelf Openings = new( \"openings\" );\n\tpublic static readonly ArchShelf Walls = new( \"walls\" );\n\tpublic static readonly ArchShelf Pillars = new( \"pillars\" );\n\tpublic static readonly ArchShelf RoadLines = new( \"roadlines\" );\n\tpublic static readonly ArchShelf Profiles = new( \"profiles\" );\n\tpublic static readonly ArchShelf Cornices = new( \"cornices\" );\n\tpublic static readonly ArchShelf Types = new( \"types\" );\n\n\tpublic override string ToString() => Role;\n}\n\n// One catalog of authored types, owned by the module whose subtool authors them. The TYPE itself stays in core: a\n// porch fits an opening preset it never authored, so a preset is shared geometry by the same rule a payload is,\n// while the catalog around it belongs to one module alone.\npublic interface IArchCatalog\n{\n\tArchShelf Shelf { get; }\n\n\tstring Named( object entry );\n\n\t// What the module brings with it; an authored entry of the same name wins, so a tuned preset survives upgrades.\n\tIEnumerable<object> Shipped();\n\n\t// What stands on disk beside it, which tops what ships.\n\tIEnumerable<object> Authored();\n\n\tbool Save( object entry );\n}\n\npublic sealed class ArchCatalogs : ArchTable<ArchCatalogs, ArchShelf, IArchCatalog>\n{\n\tprotected override IEnumerable<IArchCatalog> Standing()\n\t{\n\t\tyield return new ArchArchetypeCatalog();\n\t\tyield return new ArchProfileCatalog();\n\t\tyield return new ArchCorniceCatalog();\n\t\tyield return new ArchOpeningCatalog();\n\t\tyield return new ArchWallCatalog();\n\t\tyield return new ArchPillarCatalog();\n\t\tyield return new ArchRoadLineCatalog();\n\t}\n\n\tprotected override ArchShelf KeyOf( IArchCatalog catalog ) => catalog.Shelf;\n\n\tprotected override bool Accepts( IArchCatalog catalog ) => !string.IsNullOrWhiteSpace( catalog.Shelf.Role );\n\n\tprotected override string Collision( IArchCatalog standing, IArchCatalog catalog )\n\t{\n\t\treturn $\"{catalog.Shelf} is claimed by both {standing.GetType().Name} and {catalog.GetType().Name}\";\n\t}\n\n\t// A shelf no catalog claims stands empty rather than erroring.\n\tpublic IArchCatalog On( ArchShelf shelf ) => Held( shelf );\n\n\tpublic List<T> Ships<T>( ArchShelf shelf ) where T : class\n\t{\n\t\treturn On( shelf ) is { } catalog ? catalog.Shipped().OfType<T>().ToList() : new List<T>();\n\t}\n\n\t// For a catalog whose authored folder IS the offering - a picker that browses what an author has on disk.\n\tpublic List<T> Authors<T>( ArchShelf shelf ) where T : class\n\t{\n\t\treturn On( shelf ) is { } catalog ? catalog.Authored().OfType<T>().ToList() : new List<T>();\n\t}\n\n\t// What ships never displaces what is already held, and what is authored on disk always does - so an upgrade\n\t// brings new presets in beside the tuned ones and a written file is the last word on its own name.\n\tpublic void Stock<T>( ArchShelf shelf, List<T> saved ) where T : class\n\t{\n\t\tif ( saved is null || On( shelf ) is not { } catalog )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tforeach ( var entry in catalog.Shipped().OfType<T>() )\n\t\t{\n\t\t\tif ( !saved.Any( existing => Same( catalog.Named( existing ), catalog.Named( entry ) ) ) )\n\t\t\t{\n\t\t\t\tsaved.Add( entry );\n\t\t\t}\n\t\t}\n\n\t\tforeach ( var entry in catalog.Authored().OfType<T>() )\n\t\t{\n\t\t\tvar index = saved.FindIndex( existing => Same( catalog.Named( existing ), catalog.Named( entry ) ) );\n\n\t\t\tif ( index >= 0 )\n\t\t\t{\n\t\t\t\tsaved[index] = entry;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tsaved.Add( entry );\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic bool Save( ArchShelf shelf, object entry ) => On( shelf ) is { } catalog && catalog.Save( entry );\n\n\tstatic bool Same( string first, string second ) => string.Equals( first, second, StringComparison.OrdinalIgnoreCase );\n}\n\n// For a one-off caller holding no table of its own. Reaching through it in a loop loads the shelf every time.\npublic static class ArchShelved\n{\n\tpublic static List<T> Ships<T>( ArchShelf shelf ) where T : class => ArchCatalogs.Load().Ships<T>( shelf );\n\n\tpublic static List<T> Authors<T>( ArchShelf shelf ) where T : class => ArchCatalogs.Load().Authors<T>( shelf );\n\n\tpublic static bool Save( ArchShelf shelf, object entry ) => ArchCatalogs.Load().Save( shelf, entry );\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Designers/ArchOpeningKinds.cs",
            "FileName": "ArchOpeningKinds.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Sunless.Architecture;\n\n// The one table: generators, designer and property sheet ask here, so no control lies.\npublic static class ArchOpeningKinds\n{\n\tpublic static bool Glazes( this OpeningKind kind ) => kind == OpeningKind.Window;\n\n\tpublic static bool Sills( this OpeningKind kind ) => kind == OpeningKind.Window;\n\n\tpublic static bool Hangs( this OpeningKind kind ) => kind is OpeningKind.Door or OpeningKind.DoubleDoor or OpeningKind.Garage;\n\n\t// Furniture is fixed to the frame the hole wears, so an archway carries none of it: its jambs are the wall's\n\t// own finish carried through the thickness and there is nothing there to bolt a grille to.\n\tpublic static bool Carries( this OpeningKind kind, OpeningFurniture furniture ) => furniture switch\n\t{\n\t\tOpeningFurniture.Bars => kind is OpeningKind.Window or OpeningKind.Hatch,\n\t\tOpeningFurniture.Boarded => kind is OpeningKind.Window or OpeningKind.Door or OpeningKind.DoubleDoor or OpeningKind.Hatch,\n\t\tOpeningFurniture.Shutter => kind is OpeningKind.Garage or OpeningKind.Window,\n\t\tOpeningFurniture.Gate => kind is OpeningKind.Door or OpeningKind.DoubleDoor,\n\t\tOpeningFurniture.Leaves => kind == OpeningKind.Window,\n\t\t_ => false\n\t};\n\n\t// A hood stands on the wall over the head, so it wants a hole with a frame under it to look bolted to. An\n\t// archway has none, a hatch is too small to carry one and a garage's head is where the door stacks.\n\tpublic static bool Wears( this OpeningKind kind, OpeningHood hood )\n\t{\n\t\treturn hood != OpeningHood.None && kind is OpeningKind.Window or OpeningKind.Door or OpeningKind.DoubleDoor;\n\t}\n\n\tpublic static IEnumerable<OpeningHood> Hoods( this OpeningKind kind )\n\t{\n\t\treturn Enum.GetValues<OpeningHood>().Where( hood => kind.Wears( hood ) );\n\t}\n\n\t// What a picker may offer, read off the same rows the generator builds from - so a form and a wall cannot\n\t// disagree about what a kind has.\n\tpublic static IEnumerable<OpeningFurniture> Furnishings( this OpeningKind kind )\n\t{\n\t\treturn Enum.GetValues<OpeningFurniture>().Where( furniture => kind.Carries( furniture ) );\n\t}\n\n\tpublic static bool Furnishes( this OpeningKind kind ) => kind.Furnishings().Any();\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Designers/ArchOpeningPlacement.cs",
            "FileName": "ArchOpeningPlacement.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\npublic readonly struct ArchOpeningPoint\n{\n\tpublic ArchWall Wall { get; init; }\n\tpublic ArchRoom Room { get; init; }\n\tpublic ArchBuilding Building { get; init; }\n\tpublic float Along { get; init; }\n\tpublic float Height { get; init; }\n\tpublic bool WallPlane { get; init; }\n}\n\n// The four answers a drag needs beyond the cursor, held here because both opening tools drag the\n// same way and a toggle that lived on one of them would be a second set of rules.\npublic sealed class ArchOpeningDrag\n{\n\tpublic bool RestrictStartHeight { get; set; }\n\tpublic bool RestrictEndHeight { get; set; }\n\tpublic bool PadSides { get; set; } = true;\n\tpublic bool ClearCornice { get; set; }\n}\n\npublic sealed class ArchOpeningPlacement\n{\n\tpublic ArchOpeningDrag Drag { get; } = new();\n\n\tArchOpeningPoint anchor;\n\tbool dragging;\n\n\tpublic void Update(\n\t\tArchTool tool,\n\t\tbool focused,\n\t\tAction<ArchOpeningPoint, ArchOpeningPoint, bool> preview,\n\t\tAction<ArchOpeningPoint, ArchOpeningPoint, bool> place )\n\t{\n\t\tif ( !focused || !Point( tool, out var current ) )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Gizmo.WasLeftMousePressed )\n\t\t{\n\t\t\tanchor = current;\n\t\t\tdragging = true;\n\t\t\treturn;\n\t\t}\n\n\t\tusing ( ArchGhost.Begin() )\n\t\t{\n\t\t\tpreview( dragging ? anchor : current, current, dragging );\n\t\t}\n\n\t\tif ( !dragging || !Gizmo.WasLeftMouseReleased )\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tdragging = false;\n\t\ttool.EnsureTarget();\n\n\t\tvar unit = tool.Grid.SubgridSize( 4 );\n\t\tvar resized = MathF.Abs( current.Along - anchor.Along ) >= unit\n\t\t\t|| MathF.Abs( current.Height - anchor.Height ) >= unit;\n\n\t\tplace( anchor, current, resized );\n\t}\n\n\tpublic static ArchOpeningShape Shape(\n\t\tArchOpeningPoint from,\n\t\tArchOpeningPoint to,\n\t\tArchOpeningPreset preset,\n\t\tArchGridService grid,\n\t\tArchKit kit,\n\t\tArchOpeningDrag drag )\n\t{\n\t\tvar unit = grid.SubgridSize( 4 );\n\t\tvar wallHeight = ArchWallSection.Height( from.Wall, from.Room, kit );\n\t\tvar along = MathF.Abs( to.Along - from.Along );\n\t\tvar vertical = MathF.Abs( to.Height - from.Height );\n\t\tvar resizedWidth = along >= unit;\n\t\tvar resizedHeight = vertical >= unit;\n\t\tvar resizedVerticalSpan = resizedHeight && (!drag.RestrictStartHeight || !drag.RestrictEndHeight);\n\t\tvar minimumWidth = resizedWidth ? unit : ArchGridService.FinestSize;\n\t\tvar minimumHeight = resizedVerticalSpan ? unit : ArchGridService.FinestSize;\n\t\tvar headroom = drag.ClearCornice\n\t\t\t? ArchOpeningClearance.Head( from.Room, from.Building, from.Wall, kit, wallHeight )\n\t\t\t: wallHeight;\n\n\t\t// Both ends are clamped into the run BEFORE the width is taken from them, so a drag that\n\t\t// overshoots a corner lands exactly on the margin instead of leaving whatever the cursor\n\t\t// happened to be short by. Measuring the width first and centring it afterwards is what\n\t\t// left an uneven sliver at each end of a full-length window.\n\t\tArchOpeningSeats.Usable( from.Wall, preset, kit, drag.PadSides, minimumWidth, out var runFrom, out var runTo );\n\n\t\tfloat left;\n\t\tfloat right;\n\n\t\tif ( resizedWidth )\n\t\t{\n\t\t\tleft = Math.Clamp( MathF.Min( from.Along, to.Along ), runFrom, runTo );\n\t\t\tright = Math.Clamp( MathF.Max( from.Along, to.Along ), runFrom, runTo );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tArchOpeningSeats.Centre( from.Along, ArchGridService.Fine( preset.Width ), runFrom, runTo, out left, out right );\n\t\t}\n\n\t\tvar width = MathF.Max( minimumWidth, right - left );\n\t\tvar offset = (left + right) * 0.5f;\n\n\t\tvar wallPlaneDrag = resizedHeight && from.WallPlane && to.WallPlane;\n\t\tvar presetSill = ArchGridService.Fine( preset.SillHeight );\n\t\tvar presetTop = ArchGridService.Fine( preset.SillHeight + preset.Height );\n\t\tvar sill = wallPlaneDrag && !drag.RestrictStartHeight\n\t\t\t? grid.Subgrid( MathF.Min( from.Height, to.Height ), 4 )\n\t\t\t: presetSill;\n\t\tvar top = wallPlaneDrag && !drag.RestrictEndHeight\n\t\t\t? grid.Subgrid( MathF.Max( from.Height, to.Height ), 4 )\n\t\t\t: presetTop;\n\n\t\tsill = Math.Clamp( sill, 0f, MathF.Max( 0f, headroom - minimumHeight ) );\n\t\ttop = Math.Clamp( top, minimumHeight, headroom );\n\n\t\tif ( top - sill < minimumHeight )\n\t\t{\n\t\t\tif ( drag.RestrictEndHeight && !drag.RestrictStartHeight )\n\t\t\t{\n\t\t\t\tsill = MathF.Max( 0f, top - minimumHeight );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\ttop = MathF.Min( headroom, sill + minimumHeight );\n\t\t\t}\n\t\t}\n\n\t\treturn new ArchOpeningShape\n\t\t{\n\t\t\tOffset = offset,\n\t\t\tWidth = width,\n\t\t\tHeight = top - sill,\n\t\t\tSillHeight = sill\n\t\t};\n\t}\n\n\tbool Point( ArchTool tool, out ArchOpeningPoint point )\n\t{\n\t\tif ( dragging )\n\t\t{\n\t\t\treturn FixedWallPoint( tool, anchor, out point );\n\t\t}\n\n\t\treturn NearestWallPoint( tool, out point );\n\t}\n\n\tstatic bool NearestWallPoint( ArchTool tool, out ArchOpeningPoint point )\n\t{\n\t\tpoint = default;\n\n\t\tif ( WallPlanePoint( tool, Gizmo.CurrentRay, null, out point ) )\n\t\t{\n\t\t\treturn true;\n\t\t}\n\n\t\t// The AIM, not the settled point: which wall is being pointed at is a measurement, and the station along it\n\t\t// is put on the ladder below anyway. Asked of a snapped point, the search reads whichever wall the snap had\n\t\t// already chosen rather than the one under the cursor.\n\t\tif ( !tool.AimPoint( out var ground ) )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tvar wall = tool.NearestWall( ground, out var room, out var along, out var distance );\n\n\t\tif ( wall is null || room is null || distance > 64f )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tpoint = new ArchOpeningPoint\n\t\t{\n\t\t\tWall = wall,\n\t\t\tRoom = room,\n\t\t\tBuilding = tool.Plan.OwnerOf( room ),\n\t\t\tAlong = tool.Grid.Subgrid( along, 4 ),\n\t\t\tHeight = 0f,\n\t\t\tWallPlane = false\n\t\t};\n\n\t\treturn true;\n\t}\n\n\tstatic bool FixedWallPoint( ArchTool tool, ArchOpeningPoint anchor, out ArchOpeningPoint point )\n\t{\n\t\tif ( WallPlanePoint( tool, Gizmo.CurrentRay, anchor.Wall, out point ) )\n\t\t{\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( !tool.AimPoint( out var ground ) )\n\t\t{\n\t\t\tpoint = anchor;\n\t\t\treturn true;\n\t\t}\n\n\t\tvar along = Math.Clamp(\n\t\t\tVector2.Dot( ground - anchor.Wall.Start, anchor.Wall.Direction ),\n\t\t\t0f,\n\t\t\tanchor.Wall.Length );\n\t\tvar wallPoint = anchor.Wall.PointAt( along );\n\n\t\tpoint = new ArchOpeningPoint\n\t\t{\n\t\t\tWall = anchor.Wall,\n\t\t\tRoom = anchor.Room,\n\t\t\tBuilding = anchor.Building,\n\t\t\tAlong = tool.Grid.Subgrid( along, 4 ),\n\t\t\tHeight = tool.Grid.Subgrid( MathF.Abs( Vector2.Dot( ground - wallPoint, anchor.Wall.Normal ) ), 4 ),\n\t\t\tWallPlane = false\n\t\t};\n\n\t\treturn true;\n\t}\n\n\tstatic bool WallPlanePoint( ArchTool tool, Ray ray, ArchWall fixedWall, out ArchOpeningPoint point )\n\t{\n\t\tpoint = default;\n\t\tvar nearest = float.MaxValue;\n\n\t\tforeach ( var building in tool.Plan.Buildings )\n\t\t{\n\t\t\tvar lift = ArchAsks.Lift( tool.Plan, building, tool.Kit );\n\n\t\t\tforeach ( var room in building.Rooms )\n\t\t\t{\n\t\t\t\tif ( room.Floor != tool.Level )\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tvar baseHeight = room.BaseHeight + lift;\n\t\t\t\tvar wallHeight = room.WallHeight > 0f ? room.WallHeight : tool.Kit.WallHeight;\n\n\t\t\t\tforeach ( var wall in room.Walls )\n\t\t\t\t{\n\t\t\t\t\tif ( fixedWall is not null && wall != fixedWall )\n\t\t\t\t\t{\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tvar denominator = ray.Forward.x * wall.Normal.x + ray.Forward.y * wall.Normal.y;\n\n\t\t\t\t\tif ( MathF.Abs( denominator ) < 0.0001f )\n\t\t\t\t\t{\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tvar offset = new Vector2( ray.Position.x, ray.Position.y ) - wall.Start;\n\t\t\t\t\tvar distance = -Vector2.Dot( offset, wall.Normal ) / denominator;\n\n\t\t\t\t\tif ( distance < 0f || distance >= nearest )\n\t\t\t\t\t{\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tvar hit = ray.Position + ray.Forward * distance;\n\t\t\t\t\tvar flat = new Vector2( hit.x, hit.y );\n\t\t\t\t\tvar along = Vector2.Dot( flat - wall.Start, wall.Direction );\n\t\t\t\t\tvar height = hit.z - baseHeight;\n\n\t\t\t\t\tif ( fixedWall is null\n\t\t\t\t\t\t&& (along < -8f || along > wall.Length + 8f || height < -8f || height > wallHeight + 8f) )\n\t\t\t\t\t{\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tnearest = distance;\n\t\t\t\t\tpoint = new ArchOpeningPoint\n\t\t\t\t\t{\n\t\t\t\t\t\tWall = wall,\n\t\t\t\t\t\tRoom = room,\n\t\t\t\t\t\tBuilding = building,\n\t\t\t\t\t\tAlong = tool.Grid.Subgrid( Math.Clamp( along, 0f, wall.Length ), 4 ),\n\t\t\t\t\t\tHeight = tool.Grid.Subgrid( Math.Clamp( height, 0f, wallHeight ), 4 ),\n\t\t\t\t\t\tWallPlane = true\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nearest < float.MaxValue;\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Geometry/ArchGuards.cs",
            "FileName": "ArchGuards.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// One run of edge protection, resolved for a generator and read back by the tests.\npublic sealed class ArchGuardRun\n{\n\tpublic ArchBarrierShape Shape { get; init; }\n\tpublic ArchBarrierSpec Spec { get; init; }\n\tpublic bool Closed { get; init; }\n\tpublic float Length { get; init; }\n\n\t// A closed run's last station IS its first, and posted twice it stands two posts inside each other.\n\tpublic Func<float, bool> Doubled()\n\t{\n\t\tif ( !Closed )\n\t\t{\n\t\t\treturn null;\n\t\t}\n\n\t\tvar closing = Length - 0.5f;\n\n\t\treturn distance => distance > closing;\n\t}\n}\n\n// The barrier engine already owns posts, bays and rails; a deck only says where its edge is and how high to stand\n// on it. A roof's guard and a platform's are the same rail, so the spec is core geometry.\npublic static class ArchGuards\n{\n\t// Bays are authored EDGE BY EDGE, so every station lands either on a corner or inside one straight stretch.\n\t// Divided by arc length alone a bay spans a corner and its rail cuts the corner off.\n\tpublic static ArchBarrierSpec Spec( float height, ArchKit kit, ArchRunPath run, BarrierStyle style = BarrierStyle.PostAndRail )\n\t{\n\t\tvar spec = new ArchBarrierSpec\n\t\t{\n\t\t\tStyle = style,\n\t\t\tGround = BarrierGround.Level,\n\t\t\tHeight = height,\n\t\t\tPanelLength = ArchGuardStyles.Bay( style, kit ),\n\t\t\tPostSize = MathF.Max( 1f, kit.RoofGuardPost ),\n\t\t\tPanelThickness = kit.SolidGuardThickness,\n\t\t\tRails = 2\n\t\t};\n\n\t\tfor ( var edge = 0; edge < run.Edges; edge++ )\n\t\t{\n\t\t\tvar division = ArchDivide.AtMost( (run.At( edge + 1 ) - run.At( edge )).Length, spec.PanelLength );\n\n\t\t\tfor ( var bay = 0; bay < division.Count; bay++ )\n\t\t\t{\n\t\t\t\tspec.Bays.Add( new ArchBarrierBay { Span = division.Step } );\n\t\t\t}\n\t\t}\n\n\t\treturn spec;\n\t}\n}\n"
        },
        {
            "Ident": "sunless.lib_architecture",
            "Path": "Editor/Geometry/ArchWallFaces.cs",
            "FileName": "ArchWallFaces.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 343545,
            "Code": "using System;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace Sunless.Architecture;\n\n// Which way a wall faces out of its building, decided by probing both sides for a room rather than by the winding\n// the wall was authored in. Fixtures, approaches and anything else seated on an exterior face reads this.\npublic static class ArchWallFaces\n{\n\tpublic static bool TryOutward( ArchWall wall, ArchRoom room, ArchBuilding building, out Vector2 outward )\n\t{\n\t\tvar normal = wall.Normal;\n\t\tvar midpoint = wall.PointAt( wall.Length * 0.5f );\n\t\tvar probe = MathF.Max( 4f, wall.Thickness );\n\t\tvar positiveOccupied = Occupied( building, room.Floor, midpoint + normal * probe );\n\t\tvar negativeOccupied = Occupied( building, room.Floor, midpoint - normal * probe );\n\n\t\tif ( positiveOccupied == negativeOccupied )\n\t\t{\n\t\t\toutward = default;\n\n\t\t\treturn false;\n\t\t}\n\n\t\toutward = positiveOccupied ? -normal : normal;\n\n\t\treturn true;\n\t}\n\n\tpublic static Vector2 Outward( ArchWall wall, ArchRoom room, ArchBuilding building )\n\t{\n\t\tif ( TryOutward( wall, room, building, out var outward ) )\n\t\t{\n\t\t\treturn outward;\n\t\t}\n\n\t\tvar normal = wall.Normal;\n\t\tvar midpoint = wall.PointAt( wall.Length * 0.5f );\n\n\t\treturn ArchFloorGen.Contains( ArchFloorGen.Footprint( room ), midpoint + normal * 4f ) ? -normal : normal;\n\t}\n\n\tstatic bool Occupied( ArchBuilding building, int level, Vector2 point )\n\t{\n\t\tif ( building is null )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\n\t\tforeach ( var room in building.Rooms )\n\t\t{\n\t\t\tif ( room.Floor == level && ArchFloorGen.Contains( ArchFloorGen.Footprint( room ), point ) )\n\t\t\t{\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n}\n"
        }
    ]
}