Editor/Designers/ArchOpeningDesigner.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchOpeningDesigner : ArchDesignerDialog {
readonly ArchKit kit;
readonly Action<ArchOpeningPreset> applied;
readonly bool windowsOnly;
ArchOpeningPreset draft;
float startHeight;
float endHeight;
bool aiming;
public ArchOpeningDesigner(
Widget parent,
ArchKit kit,
ArchOpeningPreset initial,
bool windowsOnly,
Action<ArchOpeningPreset> applied )
: base( parent, windowsOnly ? "Window Designer" : "Opening Designer", windowsOnly ? "window" : "sensor_door" ) {
this.kit = kit;
this.applied = applied;
this.windowsOnly = windowsOnly;
draft = Copy( initial );
startHeight = draft.SillHeight;
endHeight = draft.SillHeight + draft.Height;
if ( windowsOnly ) {
draft.Kind = OpeningKind.Window;
draft.Glazed = true;
draft.Leaf = false;
}
Assemble(
kit,
"Live generated unit",
"Drag the preview to orbit. The wall cut, trim, sill, leaf, sashes and glass use the production generators.",
$"Preset assets: Assets/{ArchOpeningCatalog.Directory}" );
}
protected override string SaveLabel => "Save Preset";
// The second viewport, standing in the same slot as the generated unit and shown only while the leaf's art is
// being aimed. Reading the draft means switching the prefab re-aims without rebuilding anything.
protected override Widget Aiming( Widget parent ) {
return new ArchAimedPrefabStage( parent, () => draft.LeafArt, Anchored, LeafRoom );
}
// The leaf's own frame, exactly as the scene pass lays it out: the hinge at the foot, x across the opening,
// y through the wall, z up.
BBox LeafRoom() {
var width = draft.Kind == OpeningKind.DoubleDoor ? draft.Width * 0.5f : draft.Width;
var half = MathF.Max( 0.25f, kit.DoorLeafThickness ) * 0.5f;
var height = MathF.Abs( endHeight - startHeight ) - kit.DoorUndercut;
return new BBox( new Vector3( 0f, -half, 0f ),
new Vector3( MathF.Max( 1f, width ), half, MathF.Max( 1f, height ) ) );
}
bool Aimed => aiming && draft.IsHung && draft.LeafArt.Standing;
// The kind decides which rows exist: a control the generator never reads is left out.
protected override void Populate() {
Frame();
var form = Form.Layout;
form.Add( Heading( "Identity" ) );
form.Add( Text( "Name", draft.Name, value => draft.Name = value ) );
if ( !windowsOnly ) {
form.Add( Row( "Type", Kinds() ) );
}
form.Add( Heading( "Frame" ) );
form.Add( Number( "Width", draft.Width, value => draft.Width = MathF.Max( ArchGridService.FinestSize, value ) ) );
form.Add( Number( "Start height", startHeight, value => startHeight = MathF.Max( 0f, value ) ) );
form.Add( Number( "End height", endHeight, value => endHeight = MathF.Max( 0f, value ) ) );
form.Add( Gate( "Casing", draft.Cased, value => draft.Cased = value ) );
if ( draft.Cased ) {
form.Add( Number( "Casing width", draft.CasingWidth > 0f ? draft.CasingWidth : kit.CasingWidth,
value => draft.CasingWidth = MathF.Max( 0f, value ) ) );
}
if ( draft.Kind.Sills() ) {
form.Add( Check( "Sill board under it", draft.HasSill, value => draft.HasSill = value ) );
}
if ( draft.Kind.Furnishes() || draft.Kind.Hoods().Any() ) {
using var fittings = ArchIconGrid.In( form );
ArchOpeningUi.Furniture( fittings, draft.Kind, draft.Furniture, value => {
draft.Furniture = value;
Stage.Restage();
} );
ArchOpeningUi.Hood( fittings, draft.Kind, draft.Hood, value => {
draft.Hood = value;
Stage.Restage();
} );
}
if ( draft.Kind.Hangs() ) {
form.Add( Heading( "Leaf" ) );
form.Add( Gate( "Hang a leaf in it", draft.Leaf, value => draft.Leaf = value ) );
}
if ( draft.IsHung ) {
form.Add( Check( "Starts open", draft.StartOpen, value => draft.StartOpen = value ) );
form.Add( new ArchPrefabWell( Form, () => draft.LeafPrefab, Took ) );
BuildArt( form );
}
if ( draft.Kind.Glazes() ) {
form.Add( Heading( "Glazing" ) );
form.Add( Gate( "Glazed", draft.Glazed, value => draft.Glazed = value ) );
}
if ( !draft.IsGlazed ) {
return;
}
form.Add( Check( "Automatic layout", draft.AutoLayout, value => draft.AutoLayout = value ) );
form.Add( Integer( "Lights", draft.Lights, value => draft.Lights = value ) );
form.Add( Integer( "Sashes", draft.Sashes, value => draft.Sashes = value ) );
form.Add( Integer( "Pane columns", draft.PanesAcross, value => draft.PanesAcross = value ) );
form.Add( Integer( "Pane rows", draft.PanesDown, value => draft.PanesDown = value ) );
form.Add( Check( "Breakable", draft.Breakable, value => draft.Breakable = value ) );
form.Add( Check( "Already broken", draft.Glass == GlassState.Broken, value => {
draft.Glass = value ? GlassState.Broken : GlassState.Intact;
} ) );
}
// Art the author drew, standing where the extruded leaf would have. The whole leaf is either one or the other,
// so dropping a prefab in is what turns the tool's own box off.
void BuildArt( Layout form ) {
form.Add( Note( Standing() ) );
if ( !draft.LeafArt.Standing ) {
return;
}
form.Add( new Button( aiming ? "Back to the unit" : "Aim the art", aiming ? "view_in_ar" : "my_location" ) {
Clicked = () => {
aiming = !aiming;
Repopulate();
}
} );
form.Add( Note( AnchorNote() ) );
form.Add( new Button( "Clear the anchor", "backspace" ) { Clicked = () => Anchored( Vector3.Zero, Vector3.Zero ) } );
form.Add( new Button( "Back to the extruded leaf", "delete" ) { Clicked = () => Took( "" ) } );
using ( var grid = ArchIconGrid.In( form ) ) {
Turn( grid, "Left — a quarter turn anticlockwise", "leaf_turn_left", "rotate_left", new Angles( 0f, 90f, 0f ) );
Turn( grid, "Right — a quarter turn clockwise", "leaf_turn_right", "rotate_right", new Angles( 0f, -90f, 0f ) );
Turn( grid, "Flip — face the other way", "leaf_flip", "flip", new Angles( 0f, 180f, 0f ) );
Turn( grid, "Tip — a quarter turn onto its face", "leaf_tip", "screen_rotation", new Angles( 90f, 0f, 0f ) );
Turn( grid, "Roll — a quarter turn on its own axis", "leaf_roll", "rotate_90_degrees_ccw", new Angles( 0f, 0f, 90f ) );
grid.Pick( "Square — back to the tool's own guess", "leaf_untwist", "restart_alt",
draft.LeafAnchorTurn == default, () => {
draft.LeafAnchorTurn = default;
Repopulate();
} );
}
using ( var sizing = ArchIconGrid.In( form ) ) {
sizing.Pick( "Modelled — hangs at the size it was drawn", "leaf_modelled", "straighten", !draft.LeafFits,
() => Sized( false ) );
sizing.Pick( "Stretched — scaled into the opening it fills", "leaf_stretched", "open_in_full", draft.LeafFits,
() => Sized( true ) );
}
BuildFinish( form );
}
void BuildFinish( Layout form ) {
var offered = ArchPrefabGroups.Offered( draft.LeafPrefab );
if ( offered.Count == 0 ) {
return;
}
using var grid = ArchIconGrid.In( form );
grid.Pick( "As modelled — whatever the prefab was authored with", "leaf_group_none", "block",
string.IsNullOrWhiteSpace( draft.LeafMaterialGroup ), () => Finished( "" ) );
foreach ( var group in offered ) {
var standing = group;
grid.Pick( $"{standing} — every leaf of this preset wears it", $"leaf_group_{standing}".ToLowerInvariant(),
"palette", string.Equals( draft.LeafMaterialGroup, standing, StringComparison.OrdinalIgnoreCase ),
() => Finished( standing ) );
}
}
void Turn( ArchIconGrid grid, string tooltip, string slug, string glyph, Angles step ) {
grid.Pick( tooltip, slug, glyph, false, () => {
draft.LeafAnchorTurn = (draft.LeafAnchorTurn.ToRotation() * step.ToRotation()).Angles();
Repopulate();
} );
}
void Sized( bool fits ) {
draft.LeafFits = fits;
Repopulate();
}
void Finished( string group ) {
draft.LeafMaterialGroup = group;
Repopulate();
}
// A dropped prefab is a fresh aim: an anchor picked on the last one names a vertex this one does not have.
void Took( string path ) {
draft.LeafPrefab = path ?? "";
draft.LeafAnchor = Vector3.Zero;
draft.LeafAnchorAlong = Vector3.Zero;
aiming = draft.LeafArt.Standing;
Repopulate();
}
void Anchored( Vector3 anchor, Vector3 along ) {
draft.LeafAnchor = anchor;
draft.LeafAnchorAlong = along;
Repopulate();
}
string Standing() {
return draft.LeafArt.Standing
? draft.LeafPrefab
: "No art — the tool extrudes the leaf. Drop a prefab in to hang your own instead.";
}
string AnchorNote() {
if ( !draft.LeafArt.Anchored ) {
return "Not anchored — the prefab's own origin lands on the hinge. Click an edge or a corner to change that.";
}
var at = $"{draft.LeafAnchor.x:0.##}, {draft.LeafAnchor.y:0.##}, {draft.LeafAnchor.z:0.##}";
return draft.LeafArt.OnAnEdge ? $"Anchored on an edge at {at}." : $"Anchored on a corner at {at}.";
}
// Which viewport the right-hand side shows. The generated unit is the resting state, so leaving the aim behind
// needs nothing remembered.
void Frame() {
if ( Aimed ) {
Aim( true, "Your prefab, hanging in the opening",
"Left-click an edge or a corner to anchor it — the aim snaps to real geometry. Right-drag to orbit." );
return;
}
Aim( false, "Live generated unit",
"Drag the preview to orbit. The wall cut, trim, sill, leaf, sashes and glass use the production generators." );
}
protected override ArchStaged Staged() => ArchOpeningStage.Of( Snapshot(), kit );
protected override string Diagnose() {
var unit = Snapshot();
return string.Join( "\n",
$"preset: {unit.Name}, kind {unit.Kind}",
$"hole: {unit.Width:0.##} wide, {unit.SillHeight:0.##} to {unit.SillHeight + unit.Height:0.##} ({unit.Height:0.##} tall)",
$"kind offers: glazing {unit.Kind.Glazes()}, sill {unit.Kind.Sills()}, leaf {unit.Kind.Hangs()}, "
+ $"furniture {string.Join( "/", unit.Kind.Furnishings() )}",
$"resolved: glazed {unit.IsGlazed}, silled {unit.IsSilled}, hung {unit.IsHung}, fitted {unit.Fitted}",
$"casing: {unit.Cased} width {(unit.CasingWidth > 0f ? unit.CasingWidth : kit.CasingWidth):0.##}",
$"leaf: starts open {unit.StartOpen}",
$"leaf art: {(unit.LeafArt.Standing ? unit.LeafPrefab : "extruded")}, anchored {unit.LeafArt.Anchored}, "
+ $"turn {unit.LeafAnchorTurn}, stretched {unit.LeafFits}, "
+ $"group {(string.IsNullOrWhiteSpace( unit.LeafMaterialGroup ) ? "as modelled" : unit.LeafMaterialGroup)}",
$"glazing: auto {unit.AutoLayout}, {unit.Lights} lights, {unit.Sashes} sashes, "
+ $"{unit.PanesAcross} x {unit.PanesDown} panes, {unit.Glass}, breakable {unit.Breakable}" );
}
Widget Kinds() {
var kinds = new ComboBox( Form );
foreach ( var option in Enum.GetValues<OpeningKind>().Where( value => !value.Glazes() ) ) {
var captured = option;
kinds.AddItem( option.ToString(), ArchIcons.OpeningGlyph( new ArchOpeningPreset { Kind = option } ), () => {
draft.Kind = captured;
Repopulate();
}, selected: option == draft.Kind );
}
return kinds;
}
protected override void Apply() {
applied?.Invoke( Snapshot() );
Close();
}
protected override void SaveTemplate() {
var saved = Snapshot();
if ( !new ArchOpeningCatalog().Save( saved ) ) {
Log.Warning( "Architecture: the opening preset needs a name before it can be saved." );
return;
}
var index = kit.Openings.FindIndex( preset => string.Equals( preset.Name, saved.Name, StringComparison.OrdinalIgnoreCase ) );
if ( index >= 0 ) {
kit.Openings[index] = saved;
} else {
kit.Openings.Add( saved );
}
applied?.Invoke( Copy( saved ) );
Close();
}
ArchOpeningPreset Snapshot() {
var result = Copy( draft );
result.Width = ArchGridService.Least( result.Width, ArchGridService.FinestSize );
var bottom = MathF.Min( startHeight, endHeight );
var top = MathF.Max( startHeight, endHeight );
result.SillHeight = ArchGridService.Least( bottom, 0f );
result.Height = ArchGridService.Least( top - bottom, ArchGridService.FinestSize );
result.Lights = Math.Max( 1, result.Lights );
result.Sashes = Math.Max( 1, result.Sashes );
result.PanesAcross = Math.Max( 1, result.PanesAcross );
result.PanesDown = Math.Max( 1, result.PanesDown );
if ( windowsOnly ) {
result.Kind = OpeningKind.Window;
}
Settle( result );
return result;
}
// Retyped kinds keep stale fields that would read as live in saves and reports.
static void Settle( ArchOpeningPreset preset ) {
if ( !preset.Kind.Carries( preset.Furniture ) ) {
preset.Furniture = OpeningFurniture.None;
}
if ( !preset.Kind.Wears( preset.Hood ) ) {
preset.Hood = OpeningHood.None;
}
if ( !preset.Kind.Glazes() ) {
preset.Glazed = false;
preset.Breakable = false;
preset.Glass = GlassState.Intact;
}
if ( !preset.Kind.Sills() ) {
preset.HasSill = false;
}
if ( preset.Kind.Hangs() ) {
return;
}
preset.Leaf = false;
preset.StartOpen = false;
preset.LeafPrefab = "";
preset.LeafAnchor = Vector3.Zero;
preset.LeafAnchorAlong = Vector3.Zero;
preset.LeafAnchorTurn = default;
preset.LeafFits = false;
preset.LeafMaterialGroup = "";
}
public static ArchOpeningPreset Copy( ArchOpeningPreset preset ) => preset?.Copy() ?? new ArchOpeningPreset();
}