Editor/Tool/Subtools/ArchWallSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Wall" ), Icon( "horizontal_rule" ), Group( "04" )]
public sealed class ArchWallSubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => ArchKind.Wall;
public override ArchSurface[] Surfaces => new[]
{
ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.WallBase, ArchSurface.Baseboard, ArchSurface.WallCap
};
ArchWallPreset draft;
protected override string Title() => "Wall";
protected override string Advice() => "Drag to draw inside a room, or across a roof deck to stand a parapet on it.";
// Picking the tool starts the sequence over, so the first step is open and asking which treatment this is.
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
// Seeded off the kit on first ask - the kit isn't loaded until the tool is up.
ArchWallPreset Treatment() {
return draft ??= ArchWallPreset.Copy( Owner.Kit.Walls.FirstOrDefault() );
}
// A post at the section and rise of the wall about to be stood, on the surface it will stand on, so what the
// cursor came to rest on is a thing you can see rather than a thing you find out by releasing the mouse.
protected override void DrawHover( Vector2 point ) {
var treatment = Treatment();
var deck = Pointer.OnDeck ?? ArchRoofWalls.Under( Owner.Plan, Owner.Level, point );
var room = deck is null ? Owner.Contained( point ) : null;
var seat = deck is not null ? ArchRoofWalls.Seat( deck, Owner.Kit ) : room?.BaseHeight ?? Owner.LevelHeight;
var height = treatment.Height > 1f
? treatment.Height
: deck is not null ? ArchRoofWalls.ParapetHeight
: room is { WallHeight: > 1f } ? room.WallHeight : Owner.Kit.WallHeight;
ArchGhost.Marker( point, Thickness( treatment ), seat, seat + height, Pointer.Snap, Reach() );
// Tag, not Note: WorldText lies in a plane, so the label reads back to front from half the angles a
// building is drawn from - which is what "plain wall" mirrored over the deck was.
ArchGhost.Tag( new Vector3( point.x, point.y, seat + height ), Landing( treatment, deck, room ),
Pointer.Snap == ArchSnapKind.None ? ArchGhost.Line : ArchGhost.Accent );
}
// The marker ring shows how far the point will reach, so it draws the widest of the two bands it really has.
float Reach() => Owner.SnapReach().Face;
string Landing( ArchWallPreset treatment, ArchRoofPart deck, ArchRoom room ) {
var on = ArchWallSnap.Describe( Pointer.Snap )
?? (deck is not null ? $"parapet on {deck.Name}" : room is not null ? $"in {room.Name}" : null);
return on is null ? treatment.Name : $"{treatment.Name} — {on}";
}
protected override void DrawPreview() {
var treatment = Treatment();
if ( Pointer.Snap != ArchSnapKind.None ) {
ArchGhost.Marker( DragCurrent, Thickness( treatment ), DragHeight, DragHeight, Pointer.Snap, Reach() );
}
if ( DragDeck is { } deck ) {
var seat = ArchRoofWalls.Seat( deck, Owner.Kit );
var parapet = treatment.Height > 1f ? treatment.Height : ArchRoofWalls.ParapetHeight;
ArchGhost.Ring( deck.Outline(), seat );
ArchGhost.Wall( DragStart, DragCurrent, seat, parapet, Thickness( treatment ) );
ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, seat + parapet ),
$"parapet on {deck.Name} at deck {seat:0} — {(DragCurrent - DragStart).Length:0}" );
return;
}
var room = Standing();
var height = treatment.Height > 1f
? treatment.Height
: room is { WallHeight: > 1f } ? room.WallHeight : Owner.Kit.WallHeight;
if ( room is null ) {
ArchGhost.Wall( DragStart, DragCurrent, Owner.LevelHeight, height, Thickness( treatment ) );
ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, Owner.LevelHeight + height ),
$"no room on level {Owner.Level} here — draw a shell first, or drag on a deck for a parapet" );
return;
}
ArchGhost.Wall( DragStart, DragCurrent, room.BaseHeight, height, Thickness( treatment ) );
ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, room.BaseHeight + height ),
$"{treatment.Name} — {(DragCurrent - DragStart).Length:0}" );
}
float Thickness( ArchWallPreset treatment ) {
return treatment.Thickness > 0.1f ? treatment.Thickness : Owner.Kit.WallThickness;
}
// RoomAt falls back to the active room, which on any other storey is a wall filed where it cannot be seen. The
// only honest host is one standing on the level being edited.
ArchRoom Standing() {
var room = Owner.RoomAt( DragStart, out _ );
return room is not null && room.Floor == Owner.Level ? room : null;
}
protected override void OnDrag( Vector2 from, Vector2 to ) {
if ( (to - from).Length < 1f ) {
return;
}
// The deck the gesture BEGAN on, not one looked up from here: a drag pulled off the edge of the roof is
// still that roof's parapet.
if ( DragDeck is { } deck ) {
var parapet = ArchRoofWalls.Add( Owner.Plan, deck, from, to, Treatment() );
FinishDraft( parapet.Id );
Owner.Commit( $"Stand parapet on {deck.Name}" );
return;
}
if ( Standing() is not { } room ) {
Log.Info( $"Architecture: nothing on level {Owner.Level} to hang that wall on. Draw a shell or a room first,"
+ " or drag across a roof deck to stand a parapet on it." );
return;
}
var wall = Treatment().Spawn( Owner.Plan.AllocateId(), from, to );
room.Walls.Add( wall );
FinishDraft( wall.Id );
Owner.Commit();
}
// A drawn wall is a partition, a parapet on a deck or a boundary wall - never the fenestrated elevation of a
// house, which the Building tool's shell stands and the Opening tools cut. So the sequence asks only what is
// true of THIS wall: which treatment, how its head finishes, what its two faces wear, how big it is.
//
// Cladding, band courses, trim bands and pilasters are not here. They are what a TREATMENT is - the card you
// picked already says "brick, 2 bands, pilasters" on it - so they are authored in the Wall Designer behind the
// shelf's design chip, and edited on a standing wall through its own sheet in Select. A third copy down here
// is how the panel came to say everything twice.
protected override void BuildOptions( ToolSidebarWidget panel ) {
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Treatment", Treatment().Name, TreatmentGlyph, step => Browse( panel, step ) );
flow.Step( "Head", HeadName, HeadGlyph, BuildHead );
flow.Step( "Faces", FacesName, "format_paint", BuildFaces );
flow.Step( "Placement", PlacementName, "straighten", BuildPlacement );
}
string TreatmentGlyph => ArchIcons.Get( ArchIcons.WallSlug( Treatment().Name ), "villa" );
string HeadName {
get {
var treatment = Treatment();
return ArchWallUi.Crowning( Owner.Kit, treatment ) is { } crown ? crown.Name : treatment.Cap ? "Coping" : "Bare";
}
}
string HeadGlyph {
get {
var treatment = Treatment();
return ArchWallUi.Crowning( Owner.Kit, treatment ) is not null ? "view_stream" : treatment.Cap ? "border_top" : "block";
}
}
// Chose IS the refresh, and the head grid needs one either way - picking coping opens two numbers under it
// and picking a cornice opens the style shelf.
void BuildHead( ArchWorkflowStep step ) {
ArchSidebarLayout.Form( step.Layout, form => ArchWallUi.Head( form, Owner.Kit, Treatment(), step.Chose, Restage ) );
}
string FacesName {
get {
var treatment = Treatment();
var worn = new List<string> { treatment.Exterior ? "Exterior" : "Inside" };
if ( treatment.SingleSided ) {
worn.Add( "one face" );
}
if ( treatment.Baseboard ) {
worn.Add( "skirting" );
}
if ( treatment.Wainscot ) {
worn.Add( "wainscot" );
}
return string.Join( ", ", worn );
}
}
void BuildFaces( ArchWorkflowStep step ) {
ArchSidebarLayout.Form( step.Layout, form => ArchWallUi.Faces( form, Treatment(), Restage ) );
}
// What the two boxes below it read, kit placeholders and all, so the closed row and the open one agree. The
// room's own wall height beats the kit's at drag time, which is what the hover tag on the marker says.
string PlacementName {
get {
var treatment = Treatment();
var rise = treatment.Height > 1f ? treatment.Height : Owner.Kit.WallHeight;
return $"{rise:0} × {Thickness( treatment ):0}";
}
}
void BuildPlacement( ArchWorkflowStep step ) {
var treatment = Treatment();
step.Layout.Add( ArchPartUi.Number( "Height", treatment.Height, Owner.Kit.WallHeight, value => treatment.Height = value ) );
step.Layout.Add( ArchPartUi.Number( "Thickness", treatment.Thickness, Owner.Kit.WallThickness, value => treatment.Thickness = value ) );
}
// 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<ArchWallPreset>( panel, Owner.Kit, Scope( "presets" ), "Treatments" ) {
Chosen = preset => string.Equals( Treatment().Name, preset.Name, StringComparison.OrdinalIgnoreCase ),
Choose = preset => Adopt( preset, step ),
Design = () => new ArchWallDesigner( panel, Owner.Kit, Treatment(), designed => ApplyDesigned( designed, step ) ).Show()
};
shelf.DesignButton.Visible = true;
shelf.DesignButton.ToolTip = "Design a wall treatment";
shelf.Set( Offered() );
step.Add( shelf );
}
List<ArchPresetItem<ArchWallPreset>> Offered() {
return Owner.Kit.Walls
.Select( preset => new ArchPresetItem<ArchWallPreset> {
Value = preset,
Name = preset.Name,
Detail = Worn( preset ),
Identity = ArchWallStage.Identity( preset ),
Category = preset.Cladding != WallCladding.None ? preset.Cladding.ToString() : preset.Bands.Count > 0 ? "Banded" : "Plain",
Badge = preset.Exterior ? null : "inside",
Tags = $"{preset.Cladding} {preset.Bands.Count} bands {preset.Trims.Count} trims",
View = ArchPresetPreview.Elevation,
// A treatment IS the wall, so unlike an opening its card keeps the elevation it is worn on.
Focus = ArchPreviewFocus.Whole,
Recipe = () => ArchWallStage.Of( preset, Owner.Kit )
} )
.ToList();
}
static string Worn( ArchWallPreset preset ) {
var worn = new List<string>();
if ( preset.Bands.Count > 0 ) {
worn.Add( $"{preset.Bands.Count} band{(preset.Bands.Count == 1 ? "" : "s")}" );
}
if ( preset.Cladding != WallCladding.None ) {
worn.Add( preset.Cladding.ToString().ToLowerInvariant() );
}
if ( preset.Pilasters.Stands ) {
worn.Add( "pilasters" );
}
return worn.Count == 0 ? "plain field" : string.Join( ", ", worn );
}
// Editing the draft changes what its own card should show, so the stale one is dropped.
void Restage() {
ArchPresetPreview.Invalidate( ArchWallStage.Identity( Treatment() ) );
}
void Adopt( ArchWallPreset preset, ArchWorkflowStep step ) {
draft = ArchWallPreset.Copy( preset );
step.Chose();
}
void ApplyDesigned( ArchWallPreset designed, ArchWorkflowStep step ) {
draft = designed;
Owner.RegisterWallPreset( ArchWallPreset.Copy( designed ) );
ArchPresetPreview.Invalidate( ArchWallStage.Identity( designed ) );
step.Chose();
}
}