Editor/Tool/Subtools/ArchBuildingSubtool.cs

Editor subtool for building placement in the architecture editor. Handles modes (New, Extend, Prefab, Row), draws placement previews, resolves placements, commits building/wing/row placements, and provides sidebar UI for options and archetype selection.

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

namespace Sunless.Architecture;

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

[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;

	ArchPresetBrowser<ArchArchetype> browser;

	// 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;

	ArchArchetype Chosen => Owner.FindArchetype( archetype );

	protected override string Title() => "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.",
		_ => "Drag the outer shell. The chosen type decides its heights, roof and trims."
	};

	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;
		}

		ArchGhost.Volume( placement.Min, placement.Max, Owner.LevelHeight, Owner.LevelHeight + height );
		ArchGhost.Note( new Vector3( placement.Max.x, placement.Max.y, Owner.LevelHeight + height ),
			$"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}" );

		if ( !withRoof && !Extending )
		{
			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 );
		Owner.ActiveBuildingId = building.Id;
		Owner.ActiveRoomId = shell.Room.Id;
		FinishDraft( building.Id );
		Owner.Commit();
		Remember( shell );
	}

	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 );
	}

	protected override void BuildOptions( ToolSidebarWidget panel )
	{
		ArchSidebarSection.Show( panel, Scope( "mode" ), "Mode", group =>
		{
			using var grid = ArchIconGrid.In( group );

			Mode( grid, BuildingMode.New, "mode_new_building", "New building — drag the outer shell", "domain_add" );
			Mode( grid, BuildingMode.Extend, "mode_extend_building", "Extend the active building — drag a wing onto it", "add_home_work" );
			Mode( grid, BuildingMode.Prefab, "mode_archetype", "Place the type's canned layout — one drag lays the whole unit out", "auto_awesome_motion" );
			Mode( grid, 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" );
		} );

		Types( panel );

		// A canned layout decides its own steps; the shell's knobs would contradict it.
		if ( mode == BuildingMode.Prefab )
		{
			Orientation( panel );
			return;
		}

		// 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 section, because a Pick is exclusive within its own grid.
		ArchSidebarSection.Show( panel, Scope( "include" ), "Include", group =>
		{
			if ( Wants( ArchOptionGroup.Foundation ) )
			{
				using var footing = ArchIconGrid.In( group );

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

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

			using var grid = ArchIconGrid.In( group );

			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 ) );
			}
		} );

		if ( Wants( ArchOptionGroup.Section ) )
		{
			Section( panel );
		}

		if ( !Extending )
		{
			// Nothing under here describes a section with no roof over it.
			if ( !withRoof )
			{
				return;
			}

			ArchSidebarSection.Show( panel, Scope( "roof" ), "Roof", group =>
			{
				if ( Wants( ArchOptionGroup.RoofStyle ) )
				{
					using var grid = ArchIconGrid.In( group );

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

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

				Ridge( group );
			} );

			return;
		}

		if ( !Wants( ArchOptionGroup.WingRoof ) )
		{
			return;
		}

		ArchSidebarSection.Show( panel, Scope( "wing" ), "Wing roof", wings =>
		{
			using ( var grid = ArchIconGrid.In( wings ) )
			{
				foreach ( var value in Enum.GetValues<SectionRoof>() )
				{
					var captured = value;

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

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

			Ridge( wings );
		} );
	}

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

	void Mode( ArchIconGrid grid, 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;
			Refresh();
		} );
	}

	// The type's own numbers, editable - a wing must match the shell it joins. The two that decide where the
	// drag lands stay open; the lining you would judge on a section already standing folds away.
	void Section( ToolSidebarWidget panel )
	{
		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." ) );
		}

		ArchSidebarSection.Show( panel, Scope( "placement" ), "Placement", group =>
		{
			group.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 )
			{
				group.Add( ArchPartUi.Number( "Roof pitch", roofPitch, 0f, value => Set( () => roofPitch = value ) ) );
			}
		} );

		ArchSidebarSection.Disclosure( panel, Scope( "construction" ), "Construction", true, group =>
		{
			using var grid = ArchIconGrid.In( group );

			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 ) );
		} );
	}

	// 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.
	void Types( ToolSidebarWidget panel )
	{
		Inherit();

		var offered = Offered();
		var removed = ArchStorage.HiddenArchetypes();

		if ( offered.Count == 0 && removed.Count == 0 )
		{
			panel.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;
		}

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

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

		panel.Layout.Add( browser );
	}

	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 )
	{
		archetype = type.Name;

		Seed();
		Restyle();
		Refresh();
	}

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

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

	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;
	}

	void Orientation( ToolSidebarWidget panel )
	{
		ArchSidebarSection.Show( panel, Scope( "orientation" ), "Orientation", group =>
		{
			using var grid = ArchIconGrid.In( group );

			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 );
		} );
	}

	// 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"
	};
}