Editor/Tool/Subtools/ArchOpeningSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Opening" ), Icon( "sensor_door" ), Group( "05" )]
public sealed class ArchOpeningSubtool( ArchTool owner ) : ArchOpeningPlacingSubtool( owner ) {
public override ArchSurface[] Surfaces => new[] { ArchSurface.Reveal, ArchSurface.Trim, ArchSurface.Threshold, ArchSurface.DoorLeaf };
bool withLeaf = true;
bool slidOpen;
float? startHeight;
float? endHeight;
OpeningFurniture? furniture;
ArchOpeningPreset custom;
protected override string Title() => "Opening";
protected override string Advice() => "Click a wall, or drag a frame.";
// Only a kind that hangs is ever asked, so anything else keeps the hole exactly as the preset cut it.
protected override void Adorn( ArchOpening opening ) {
if ( !opening.Kind.Hangs() ) {
return;
}
opening.Leaf = withLeaf;
opening.StartOpen = withLeaf && slidOpen;
}
protected override ArchOpeningPreset Shape( ArchOpeningPreset preset ) {
var firstHeight = startHeight ?? preset.SillHeight;
var secondHeight = endHeight ?? preset.SillHeight + preset.Height;
var bottom = MathF.Min( firstHeight, secondHeight );
var top = MathF.Max( firstHeight, secondHeight );
var shaped = preset.Copy();
shaped.Height = MathF.Max( ArchGridService.FinestSize, top - bottom );
shaped.SillHeight = bottom;
shaped.Furniture = furniture ?? preset.Furniture;
return shaped;
}
protected override ArchOpeningPreset Preset() {
return custom
?? Owner.Kit.FindOpening( Owner.ActivePreset )
?? Owner.Kit.Openings.FirstOrDefault( preset => preset.Kind != OpeningKind.Window );
}
// Which unit, what hangs in it, what is bolted over it, then where it sits - picked steps first so the
// sequence carries itself as far as the two typed heights, which can never say they have been answered.
// An archway asks neither of the middle two: it hangs nothing and carries nothing, and the kind table says so.
protected override void BuildOptions( ToolSidebarWidget panel ) {
var chosen = Preset();
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Opening", chosen?.Name ?? "None", Glyph( chosen ), step => Browse( panel, step ) );
if ( chosen?.Kind.Hangs() == true ) {
flow.Step( "Leaf", LeafName, LeafGlyph, PickLeaf );
}
if ( chosen?.Kind.Furnishes() == true ) {
flow.Step( "Fitted", ArchOpeningUi.Name( Fitted( chosen ) ), ArchOpeningUi.Glyph( Fitted( chosen ) ), PickFitted );
}
flow.Step( "Placement", PlacementName( chosen ), "straighten", BuildPlacement );
}
static string Glyph( ArchOpeningPreset preset ) => preset is null ? "sensor_door" : ArchIcons.OpeningGlyph( preset );
OpeningFurniture Fitted( ArchOpeningPreset preset ) => furniture ?? preset?.Furniture ?? OpeningFurniture.None;
string LeafName => withLeaf ? slidOpen ? "Standing open" : "Shut" : "Bare";
string LeafGlyph => withLeaf ? slidOpen ? "door_open" : "door_front" : "block";
// Hung, and how it starts, is one three-state answer rather than a toggle governing a second toggle - the
// generator has only ever read it as one, since a leaf standing open is the only way StartOpen means anything.
void PickLeaf( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "Bare — the hole is left empty", "opt_leaf_none", "block",
!withLeaf, () => Hang( false, false, step ) );
grid.Pick( "Shut — a leaf hung in it, closed when the level starts", "opt_leaf", "door_front",
withLeaf && !slidOpen, () => Hang( true, false, step ) );
grid.Pick( "Standing open — a leaf hung in it, open when the level starts", "opt_start_open", "door_open",
withLeaf && slidOpen, () => Hang( true, true, step ) );
}
void Hang( bool leaf, bool open, ArchWorkflowStep step ) {
withLeaf = leaf;
slidOpen = open;
step.Chose();
}
void PickFitted( ArchWorkflowStep step ) {
var chosen = Preset();
using var grid = ArchIconGrid.In( step.Layout );
ArchOpeningUi.Furniture( grid, chosen.Kind, Fitted( chosen ), value => Fit( value, step ) );
}
void Fit( OpeningFurniture fitted, ArchWorkflowStep step ) {
furniture = fitted;
step.Chose();
}
// What the ghost will read, so the closed row and the released drag agree.
string PlacementName( ArchOpeningPreset preset ) {
if ( preset is null ) {
return "";
}
var unit = Shape( preset );
return $"{unit.Height:0} high at {unit.SillHeight:0}";
}
void BuildPlacement( ArchWorkflowStep step ) {
var chosen = Preset();
step.Layout.Add( ArchPartUi.OptionalNumber( "Start height", startHeight, chosen?.SillHeight ?? 0f, value => startHeight = value ) );
step.Layout.Add( ArchPartUi.OptionalNumber( "End height", endHeight, chosen?.SillHeight + chosen?.Height ?? 0f, value => endHeight = value ) );
ArchOpeningDragUi.DragOptions( step.Layout, placement.Drag );
}
// 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 Browse( ToolSidebarWidget panel, ArchWorkflowStep step ) {
var shelf = new ArchPresetBrowser<ArchOpeningPreset>( panel, Owner.Kit, Scope( "presets" ), "Openings" ) {
Chosen = preset => string.Equals( Owner.ActivePreset, preset.Name, StringComparison.OrdinalIgnoreCase ),
Choose = preset => Use( preset, step ),
Design = () => Author( panel, step ),
Exports = "door"
};
shelf.DesignButton.Visible = true;
shelf.DesignButton.ToolTip = "Design an opening preset";
shelf.Set( Offered() );
step.Add( shelf );
}
List<ArchPresetItem<ArchOpeningPreset>> Offered() {
return Owner.Offered( preset => preset.Kind != OpeningKind.Window )
.Select( preset => new ArchPresetItem<ArchOpeningPreset> {
Value = preset,
Name = preset.Name,
Detail = ArchOpeningStage.Describe( preset ),
Identity = ArchOpeningStage.Identity( preset ),
Category = preset.Kind.ToString(),
Badge = preset.Leaf ? null : "no leaf",
Glyph = ArchIcons.OpeningGlyph( preset ),
Tags = $"{preset.Kind} {(preset.Leaf ? "leaf" : "open")}",
Recommended = Owner.Recommends( preset ),
View = ArchPresetPreview.Elevation,
Interest = ArchOpeningStage.Interest( preset, Owner.Kit ),
Recipe = () => ArchOpeningStage.Of( preset, Owner.Kit )
} )
.ToList();
}
// A retyped preset can no longer honour the last one's leaf state, and custom sizing described a hole
// this one does not have.
void Use( ArchOpeningPreset preset, ArchWorkflowStep step ) {
custom = null;
Owner.ActivePreset = preset.Name;
startHeight = null;
endHeight = null;
furniture = null;
withLeaf = preset.Leaf;
slidOpen = preset.StartOpen;
step.Chose();
}
void Author( Widget panel, ArchWorkflowStep step ) {
var initial = Preset() is { } preset ? Shape( preset ) : new ArchOpeningPreset();
new ArchOpeningDesigner( panel, Owner.Kit, initial, false, designed => ApplyDesigned( designed, step ) ).Show();
}
void ApplyDesigned( ArchOpeningPreset preset, ArchWorkflowStep step ) {
custom = ArchOpeningDesigner.Copy( preset );
Owner.RegisterOpeningPreset( ArchOpeningDesigner.Copy( preset ) );
Owner.ActivePreset = preset.Name;
startHeight = null;
endHeight = null;
furniture = preset.Furniture;
withLeaf = preset.Leaf;
slidOpen = preset.StartOpen;
ArchPresetPreview.Invalidate( ArchOpeningStage.Identity( preset ) );
step.Chose();
}
}