Editor/Tool/Subtools/ArchCabinetSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Cabinets" ), Icon( "kitchen" ), Group( "16" )]
public sealed class ArchCabinetSubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => ArchKind.Cabinet;
public override ArchSurface[] Surfaces => new[]
{
ArchSurface.CabinetCarcass, ArchSurface.CabinetFront, ArchSurface.Worktop
};
readonly ArchCabinetPart draft = new();
ArchCabinetType drafted;
ArchCabinetPart adopted;
CabinetStanding stands;
int quarters;
ArchCabinetPart Edited => Lent ? adopted : Owner.Picked?.Item as ArchCabinetPart;
ArchCabinetPart Editing => Edited ?? draft;
protected override bool UsesDrag => true;
protected override bool Adjusts => true;
public override bool Adopts => true;
// Backed runs use wide snap band; free runs use tight to avoid spurious wall snap
public override ArchSnapReach SnapReach( float authored ) {
return stands == CabinetStanding.Backed ? ArchWallSnap.Flush( authored ) : base.SnapReach( authored );
}
protected override string Title() => "Cabinets";
protected override string Advice() => "Drag along a wall, or stand a run free anywhere on the floor — R turns it a quarter.";
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
protected override void Adopt( ArchSelection picked ) {
adopted = picked?.Item as ArchCabinetPart;
}
public override bool Turns() {
if ( stands == CabinetStanding.Free && Placing ) {
quarters = (quarters + 1) % 4;
return true;
}
if ( Edited is not { } run ) {
return false;
}
run.Outward = ArchCabinets.Turned( run.Facing, 1 );
Owner.Commit( $"Turn {run.Name}" );
return true;
}
ArchCabinetType Type() {
return drafted ??= ArchCabinetTypes.Copy( ArchCabinetTypes.Find( Owner.Kit, Editing.Type ) );
}
protected override void DrawHover( Vector2 point ) {
base.DrawHover( point );
Sketch( point, point );
}
protected override void DrawPreview() {
base.DrawPreview();
Sketch( DragStart, DragCurrent );
}
CabinetTier Aimed() {
if ( draft.Tier != CabinetTier.Auto ) {
return draft.Tier;
}
return Pointer.Face == ArchCursorFace.Ceiling || Pointer.SnapAbove ? CabinetTier.Hanging : CabinetTier.Auto;
}
float Module() {
return Editing.ModuleWidth > 1f ? Editing.ModuleWidth : Type().Width;
}
ArchFixtureStation? Station( Vector2 from, Vector2 to ) {
if ( stands == CabinetStanding.Free ) {
return null;
}
return ArchFixtureAnchor.Station( Owner.Plan.Buildings, Owner.Level, Owner.Grid, from, to,
Module(), Owner.Plan, false, ArchWallSnap.Flush( Module() ).Face );
}
void Sketch( Vector2 from, Vector2 to ) {
if ( Resolved( from, to, out var room ) is not { } sketch ) {
return;
}
var shown = Continued( sketch, room ) is { } standing ? ArchCabinets.Joined( standing, sketch ) : sketch;
ArchCabinets.Preview( Owner.Plan, Owner.Kit, shown, room );
}
ArchCabinetPart Continued( ArchCabinetPart placed, ArchRoom room ) {
return ArchCabinets.Continues( Owner.Plan, room, placed, Module() );
}
ArchCabinetPart Resolved( Vector2 from, Vector2 to, out ArchRoom room ) {
Seed();
room = null;
if ( Station( from, to ) is { } station
&& ArchCabinets.Place( Owner.Plan, Owner.Kit, station, draft, out room ) is { } against ) {
return against;
}
room = Owner.Contained( from );
return room is not null && room.Floor == Owner.Level
? ArchCabinets.Free( Owner.Plan, room, Owner.Grid, from, to, quarters, Module(), draft )
: null;
}
void Seed() {
draft.Type = Type().Name;
draft.Tier = Aimed();
}
protected override void OnDrag( Vector2 from, Vector2 to ) {
if ( Resolved( from, to, out var room ) is not { } placed ) {
Log.Info( $"Architecture: nothing on level {Owner.Level} to stand cabinets on — draw a room first,"
+ " then drag inside it, along a wall or across its floor." );
return;
}
if ( Continued( placed, room ) is { } standing ) {
ArchCabinets.Absorb( standing, placed );
Owner.Picked = new ArchSelection { Item = standing, Room = room, Building = Owner.Plan.OwnerOf( room ) };
Drew( standing );
FinishDraft( standing.Id );
Owner.Commit( $"Extend {standing.Name}" );
return;
}
if ( ArchCabinets.Shaped( Owner.Plan, Owner.Kit, placed, room ) is null ) {
return;
}
room.Cabinets.Add( placed );
Owner.Picked = new ArchSelection { Item = placed, Room = room, Building = Owner.Plan.OwnerOf( room ) };
Drew( placed );
FinishDraft( placed.Id );
Owner.Commit( "Place Cabinets" );
}
protected override void BuildOptions( ToolSidebarWidget panel ) {
Inherit();
var type = Type();
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Standing", StandingName, StandingGlyph, PickStanding );
flow.Step( "Type", ArchCabinetUi.Titled( type ), ArchCabinetStage.Glyph( type ), step => PickType( panel, step ) );
flow.Step( "Tier", TierName, ArchCabinetUi.Glyph( Aimed() ), PickTier );
if ( type.Prefabbed ) {
flow.Step( "Finish", FinishName, "palette", BuildFinish );
flow.Step( "Sizes", SizesName, "straighten", BuildSizes );
return;
}
flow.Step( "Front", FrontName, "border_all", PickFront );
flow.Step( "Inside", InsideName, "inventory_2", BuildInside );
flow.Step( "Sizes", SizesName, "straighten", BuildSizes );
flow.Step( "Worktop", WorktopName, "countertops", BuildWorktop );
}
string FinishName {
get {
var group = string.IsNullOrWhiteSpace( Editing.MaterialGroup ) ? Type().MaterialGroup : Editing.MaterialGroup;
return string.IsNullOrWhiteSpace( group ) ? "As modelled" : group;
}
}
void BuildFinish( ArchWorkflowStep step ) {
ArchCabinetUi.Finish( step.Layout, Editing, Type(), Restage, step.Chose );
}
void Inherit() {
if ( Edited is { } run && !string.Equals( run.Type, Type().Name, StringComparison.OrdinalIgnoreCase ) ) {
drafted = ArchCabinetTypes.Copy( ArchCabinetTypes.Find( Owner.Kit, run.Type ) );
}
}
string StandingName {
get {
if ( stands == CabinetStanding.Backed ) {
return "Against a wall";
}
return quarters == 0 ? "Free, facing in" : $"Free, turned {quarters * 90}";
}
}
string StandingGlyph => stands == CabinetStanding.Backed ? "align_horizontal_left" : "rotate_90_degrees_cw";
void PickStanding( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "Against a wall — lies flush on whatever elevation the drag lands on, snapping to corners and jambs",
"cabinet_backed", "align_horizontal_left", stands == CabinetStanding.Backed,
() => Stand( CabinetStanding.Backed, step ) );
grid.Pick( "Free-standing — anywhere on the floor, facing into the room; R turns it a quarter",
"cabinet_free", "rotate_90_degrees_cw", stands == CabinetStanding.Free,
() => Stand( CabinetStanding.Free, step ) );
}
void Stand( CabinetStanding standing, ArchWorkflowStep step ) {
stands = standing;
step.Chose();
}
void PickType( ToolSidebarWidget panel, ArchWorkflowStep step ) {
var hidden = ArchStorage.HiddenCabinetTypes();
var shelf = new ArchPresetBrowser<ArchCabinetType>( panel, Owner.Kit, Scope( "types" ), "Cabinet types" ) {
Chosen = type => string.Equals( Type().Name, type.Name, StringComparison.OrdinalIgnoreCase ),
Choose = type => Adopt( type, step ),
Design = () => new ArchCabinetDesigner( panel, Owner, Type(), designed => Designed( designed, step ) ).Show(),
Reload = Refresh,
Exports = "cabinet",
Removes = item => Ships( item.Value ) ? $"Hide {item.Name}" : $"Remove {item.Name}",
Remove = item => Remove( item.Value ),
Restore = () => {
ArchStorage.RestoreCabinetTypes();
Reread();
}
};
shelf.DesignButton.Visible = true;
shelf.DesignButton.ToolTip = "Design a cabinet type";
shelf.ReloadButton.Visible = true;
shelf.RestoreButton.Visible = hidden.Count > 0;
shelf.RestoreButton.ToolTip = $"Bring back {string.Join( ", ", hidden )}";
shelf.Set( Offered() );
step.Add( shelf );
}
static bool Ships( ArchCabinetType type ) {
return ArchCabinetTypes.Shipped()
.Any( shipped => string.Equals( shipped.Name, type.Name, StringComparison.OrdinalIgnoreCase ) );
}
void Remove( ArchCabinetType type ) {
if ( !ArchStorage.RemoveCabinetType( type.Name ) ) {
Log.Warning( $"Architecture: could not remove the cabinet type '{type.Name}'." );
return;
}
Reread();
}
void Reread() {
if ( Owner.Kit?.Cabinets is { } offered ) {
offered.Clear();
ArchCatalogs.Load().Stock( ArchShelf.Cabinets, offered );
offered.RemoveAll( type => type.Hidden );
}
if ( !Kinds().Any( type => string.Equals( type.Name, draft.Type, StringComparison.OrdinalIgnoreCase ) ) ) {
drafted = null;
draft.Type = Kinds().FirstOrDefault()?.Name ?? "";
}
Refresh();
}
List<ArchPresetItem<ArchCabinetType>> Offered() {
return Kinds()
.Select( type => new ArchPresetItem<ArchCabinetType> {
Value = type,
Name = ArchCabinetUi.Titled( type ),
Detail = ArchCabinetStage.Describe( type ),
Identity = ArchCabinetStage.Identity( type ),
Category = ArchCabinetStage.Category( type ),
Glyph = ArchCabinetStage.Glyph( type ),
Tags = $"{type.Name} {type.Detail}",
View = ArchPresetPreview.Quarter,
Focus = ArchPreviewFocus.Whole,
Recipe = () => ArchCabinetStage.Of( type )
} )
.ToList();
}
List<ArchCabinetType> Kinds() {
var offered = Owner.Kit?.Cabinets;
return offered is { Count: > 0 } ? offered : ArchCabinetTypes.Shipped();
}
void Adopt( ArchCabinetType type, ArchWorkflowStep step ) {
Answer( () => {
drafted = ArchCabinetTypes.Copy( type );
Editing.Type = type.Name;
}, step );
}
void Designed( ArchCabinetType designed, ArchWorkflowStep step ) {
Owner.RegisterCabinetType( designed );
Adopt( designed, step );
}
string TierName => Aimed() switch {
CabinetTier.Base => "Base",
CabinetTier.Hanging => "Hanging",
CabinetTier.Tall => "Tall",
_ => $"Following {Type().Tier}"
};
void PickTier( ArchWorkflowStep step ) {
ArchCabinetUi.Tier( step.Layout, Editing.Tier, tier => Answer( () => Editing.Tier = tier, step ) );
}
string FrontName => ArchCabinetUi.Named( Editing.On( ArchCabinetMounts.Front ) ?? Type().On( ArchCabinetMounts.Front ) );
void PickFront( ArchWorkflowStep step ) {
ArchCabinetUi.Front( step.Layout, Editing, Type(), ArchCabinetMounts.Front, step.Chose, Restage );
}
string InsideName {
get {
var type = Type();
var part = Editing;
return ArchPartUi.Listed( (part.Interior || type.Interior, "interior"),
(part.Handles ?? type.Handles, "handles"),
((part.DrawerRows ?? type.DrawerRows) > 0, "drawers") );
}
}
void BuildInside( ArchWorkflowStep step ) {
ArchCabinetUi.Inside( step.Layout, Editing, Type(), Restage, Refresh );
}
string SizesName {
get {
var type = Type();
var module = Editing.ModuleWidth > 1f ? Editing.ModuleWidth : type.Width;
var depth = Editing.Depth > 1f ? Editing.Depth : type.Depth;
return $"{module:0} × {depth:0}";
}
}
void BuildSizes( ArchWorkflowStep step ) {
ArchCabinetUi.Sizes( step.Layout, Editing, Type(), Restage, Settled(), Refresh );
}
string WorktopName => $"{Editing.TopThickness ?? Type().TopThickness:0.##} thick";
void BuildWorktop( ArchWorkflowStep step ) {
ArchCabinetUi.Worktop( step.Layout, Editing, Type(), Restage, Settled(), Refresh );
}
ArchCabinetShape Settled() {
return Edited is { } run
? ArchCabinetShape.Resolve( Owner.Plan, Owner.Kit, Owner.Plan.HostOf<ArchRoom>( run ), null, run )
: null;
}
void Answer( Action change, ArchWorkflowStep step ) {
change();
Restage();
step.Chose();
}
void Restage() {
if ( Edited is not null ) {
Owner.Touch();
}
}
}