Editor/Tool/Subtools/ArchWindowSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// A sash unit is authored, not just a hole - hence the separate tool.
[Title( "Window" ), Icon( "window" ), Group( "06" )]
public sealed class ArchWindowSubtool( ArchTool owner ) : ArchOpeningPlacingSubtool( owner ) {
public override ArchSurface[] Surfaces => new[] { ArchSurface.Glass, ArchSurface.WindowFrame, ArchSurface.WindowSash, ArchSurface.Sill, ArchSurface.Trim, ArchSurface.Reveal };
float width;
float? startHeight;
float? endHeight;
bool? broken;
bool? breakable;
OpeningFurniture? furniture;
ArchOpeningPreset custom;
protected override string Title() => "Window";
protected override string Advice() => "Click a wall, or drag a frame.";
protected override string Label( ArchOpeningPreset preset, ArchOpeningPreset unit ) => Describe( unit );
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.Kind = OpeningKind.Window;
shaped.Width = width > 0f ? width : preset.Width;
shaped.Height = MathF.Max( ArchGridService.FinestSize, top - bottom );
shaped.SillHeight = bottom;
shaped.Leaf = false;
shaped.StartOpen = false;
shaped.Glass = Broken( preset ) ? GlassState.Broken : GlassState.Intact;
shaped.Breakable = Breaks( preset );
shaped.Furniture = Fitted( preset );
return shaped;
}
static string Describe( ArchOpeningPreset unit ) {
var lights = unit.Lights > 1 ? $"{unit.Lights} lights, " : "";
var panes = unit.PanesAcross * unit.PanesDown;
return unit.Glazed
? $"{unit.Name} — {lights}{panes} over {panes}"
: $"{unit.Name} — {unit.Width:0}x{unit.Height:0}";
}
protected override ArchOpeningPreset Preset() {
return custom ?? Owner.Kit.FindOpening( Owner.ActiveWindow ) ?? Windows( Owner.Kit ).FirstOrDefault();
}
static List<ArchOpeningPreset> Windows( ArchKit kit ) {
return kit.Openings.Where( preset => preset.Kind == OpeningKind.Window ).ToList();
}
// Which unit, what is bolted over it, what state its glass is in, then how big it is - picked steps first,
// so the sequence carries itself as far as the toggles and the typed sizes, which can never answer.
//
// Lights, sashes, panes and casing are not here. They are what a WINDOW IS - the card just picked already
// says "2 lights, 4 over 4" on it - so they are cut in the Window Designer behind the shelf's design chip,
// and re-cut on a standing unit through its own sheet in Select. Kit glazing materials are not here either:
// they skin every window in the map, not the one about to be dragged.
protected override void BuildOptions( ToolSidebarWidget panel ) {
var chosen = Preset();
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Window", chosen?.Name ?? "None", Glyph( chosen ), step => Browse( panel, step ) );
flow.Step( "Fitted", ArchOpeningUi.Name( Fitted( chosen ) ), ArchOpeningUi.Glyph( Fitted( chosen ) ), PickFitted );
if ( chosen?.IsGlazed == true ) {
flow.Step( "Glass", GlassName( chosen ), GlassGlyph( chosen ), BuildGlass );
}
flow.Step( "Placement", PlacementName( chosen ), "straighten", BuildPlacement );
}
static string Glyph( ArchOpeningPreset preset ) => preset is null ? "window" : ArchIcons.OpeningGlyph( preset );
OpeningFurniture Fitted( ArchOpeningPreset preset ) => furniture ?? preset?.Furniture ?? OpeningFurniture.None;
bool Broken( ArchOpeningPreset preset ) => broken ?? preset?.Glass == GlassState.Broken;
bool Breaks( ArchOpeningPreset preset ) => breakable ?? preset?.Breakable == true;
void PickFitted( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
ArchOpeningUi.Furniture( grid, OpeningKind.Window, Fitted( Preset() ), value => Fit( value, step ) );
}
void Fit( OpeningFurniture fitted, ArchWorkflowStep step ) {
furniture = fitted;
step.Chose();
}
string GlassName( ArchOpeningPreset preset ) {
var state = Broken( preset ) ? "Broken" : "Intact";
return Breaks( preset ) ? $"{state}, breakable" : state;
}
string GlassGlyph( ArchOpeningPreset preset ) => Broken( preset ) ? "broken_image" : Breaks( preset ) ? "shield" : "window";
// Two states of the same panes rather than one three-way answer: what is left of a broken window is still
// glass that can be shot out.
void BuildGlass( ArchWorkflowStep step ) {
var chosen = Preset();
using var grid = ArchIconGrid.In( step.Layout );
grid.Toggle( "Already broken", "opt_broken", "broken_image", Broken( chosen ), value => broken = value );
grid.Toggle( "Breakable — panes on their own object", "opt_breakable", "shield", Breaks( chosen ), value => breakable = value );
}
// 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.Width:0} x {unit.Height:0} 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 ) );
step.Layout.Add( ArchPartUi.Number( "Width", width, chosen?.Width ?? 0f, value => width = 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" ), "Windows" ) {
Chosen = preset => string.Equals( Owner.ActiveWindow, preset.Name, StringComparison.OrdinalIgnoreCase ),
Choose = preset => Use( preset, step ),
Design = () => Author( panel, step ),
Exports = "window"
};
shelf.DesignButton.Visible = true;
shelf.DesignButton.ToolTip = "Design a window 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 = Family( preset ),
Badge = preset.Glass == GlassState.Broken ? "broken" : null,
Glyph = ArchIcons.OpeningGlyph( preset ),
Tags = $"{preset.Lights} lights {preset.Sashes} sashes {preset.PanesAcross}x{preset.PanesDown} panes",
Recommended = Owner.Recommends( preset ),
View = ArchPresetPreview.Elevation,
Interest = ArchOpeningStage.Interest( preset, Owner.Kit ),
Recipe = () => ArchOpeningStage.Of( preset, Owner.Kit )
} )
.ToList();
}
static string Family( ArchOpeningPreset preset ) {
if ( preset.Glass == GlassState.Broken ) {
return "Damaged";
}
if ( preset.Sashes > 1 ) {
return "Sash";
}
return preset.Width > preset.Height ? "Wide" : "Tall";
}
void Use( ArchOpeningPreset preset, ArchWorkflowStep step ) {
custom = null;
Owner.ActiveWindow = preset.Name;
Inherit();
step.Chose();
}
void Author( Widget panel, ArchWorkflowStep step ) {
var initial = Preset() is { } preset
? Shape( preset )
: new ArchOpeningPreset { Kind = OpeningKind.Window, Glazed = true, HasSill = true };
new ArchOpeningDesigner( panel, Owner.Kit, initial, true, designed => ApplyDesigned( designed, step ) ).Show();
}
// Every override drops back to the preset's own value: a number typed against the last window is not
// an opinion about this one.
void Inherit() {
width = 0f;
startHeight = null;
endHeight = null;
broken = null;
breakable = null;
furniture = null;
}
void ApplyDesigned( ArchOpeningPreset preset, ArchWorkflowStep step ) {
custom = ArchOpeningDesigner.Copy( preset );
Owner.RegisterOpeningPreset( ArchOpeningDesigner.Copy( preset ) );
Owner.ActiveWindow = preset.Name;
Inherit();
ArchPresetPreview.Invalidate( ArchOpeningStage.Identity( preset ) );
step.Chose();
}
}