Editor/Tool/Subtools/ArchOpeningPlacingSubtool.cs

Editor subtool for placing architectural openings (doors/windows). It handles previewing a ghosted opening on a wall by ray-picking, computes shape/position from two points, shows a label, and spawns/configures the real opening into the wall when placed.

File Access
namespace Sunless.Architecture;

// The one placement flow both hole-authoring tools share: ray-pick the wall, preview the framed unit, spawn it
// into the wall's own list. What differs is only the preset family, the ghost's label and what the placed unit
// wears - two copies of this flow drifted apart once already.
public abstract class ArchOpeningPlacingSubtool( ArchTool owner ) : ArchSubtool( owner )
{
	protected readonly ArchOpeningPlacement placement = new();

	protected override ArchKind? DraftKind => ArchKind.Opening;

	// Placement ray-picks the wall plane rather than the work plane, so an elevation is where a unit is most
	// naturally dragged. Left at the plan-only default the sidebar refused the view the tool actually works in
	// and dropped the preset browser with it.
	public override ArchViewAxis[] Works => new[] { ArchViewAxis.Top, ArchViewAxis.Front, ArchViewAxis.Side };

	protected abstract ArchOpeningPreset Preset();

	protected abstract ArchOpeningPreset Shape( ArchOpeningPreset preset );

	protected virtual string Label( ArchOpeningPreset preset, ArchOpeningPreset unit ) => preset.Name;

	protected virtual void Adorn( ArchOpening opening ) { }

	public override void OnUpdate()
	{
		placement.Update( Owner, Manager?.IsCurrentViewFocused == true, Preview, Place );
	}

	void Preview( ArchOpeningPoint from, ArchOpeningPoint to, bool resized )
	{
		var preset = Preset();

		if ( preset is null || from.Wall != to.Wall )
		{
			return;
		}

		var unit = Shape( preset );
		var shape = ArchOpeningPlacement.Shape( from, to, unit, Owner.Grid, Owner.Kit, placement.Drag );
		var thickness = from.Wall.Thickness > 0f ? from.Wall.Thickness : Owner.Kit.WallThickness;
		var half = shape.Width * 0.5f;
		var lift = from.Building is null ? 0f : ArchAsks.Lift( Owner.Plan, from.Building, Owner.Kit );
		var baseHeight = from.Room.BaseHeight + lift;

		ArchGhost.Opening(
			from.Wall.PointAt( shape.Offset - half ), from.Wall.PointAt( shape.Offset + half ),
			baseHeight, shape.SillHeight, shape.Height, thickness );

		var centre = from.Wall.PointAt( shape.Offset );
		var label = resized
			? $"{Label( preset, unit )}  {shape.Width:0} wide  {shape.SillHeight:0} to {shape.SillHeight + shape.Height:0}"
			: Label( preset, unit );

		ArchGhost.Note( new Vector3( centre.x, centre.y, baseHeight + shape.SillHeight + shape.Height + 6f ), label );
	}

	void Place( ArchOpeningPoint from, ArchOpeningPoint to, bool resized )
	{
		var preset = Preset();

		if ( preset is null || from.Wall != to.Wall )
		{
			return;
		}

		var unit = Shape( preset );
		var shape = ArchOpeningPlacement.Shape( from, to, unit, Owner.Grid, Owner.Kit, placement.Drag );
		var opening = unit.Spawn( Owner.Plan.AllocateId(), shape.Offset );

		opening.Width = shape.Width;
		opening.Height = shape.Height;
		opening.SillHeight = shape.SillHeight;

		Adorn( opening );

		from.Wall.Openings.Add( opening );
		FinishDraft( opening.Id );
		Owner.Commit( $"Place {opening.Preset}" );
	}
}