Editor/Tool/Subtools/ArchBoolSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Boolean Modifiers" ), Icon( "join_inner" ), Group( "11" )]
public sealed class ArchBoolSubtool( ArchTool owner ) : ArchSubtool( owner ) {
public override ArchSurface[] Surfaces => new[]
{
ArchSurface.Deck, ArchSurface.Foundation, ArchSurface.WallCap, ArchSurface.Pillar, ArchSurface.WallBase, ArchSurface.Trim,
ArchSurface.Sign, ArchSurface.Railing, ArchSurface.Soffit
};
readonly ArchPlatformPart standing = new();
readonly ArchCutPart sinking = new() {
Damage = ArchDamageKind.Masonry,
BreakEdges = true,
DamageAmount = 1f,
DamageCellWidth = 16f,
DamageCellLength = 8f,
DamageDepth = 6f,
MasonryPattern = ArchMasonryPattern.ExposedEdge,
MasonryCarves = false,
Extrude = ArchExtrudeKind.Quoins,
ExtrudeDepth = 4f,
ExtrudeCourse = 16f,
Affects = ArchDamage.Targets( ArchDamageKind.Masonry )
};
readonly ArchWallModPart detail = new() { Side = ArchWallSide.OnSurface };
readonly ArchTrimPart edging = new() { Profile = "gutter", Surface = ArchSurface.Trim };
readonly ArchBeamPart timber = new();
ArchPillarPart column = new();
ArchFixtureSubtool fixtures;
ArchFixtureSubtool Fixtures => fixtures ??= new ArchFixtureSubtool( Owner, Refresh );
ArchBoolOp op = ArchBoolOp.Add;
ArchBoolOp zone = ArchBoolOp.Damage;
ArchBoolAdd kind = ArchBoolAdd.Slab;
ArchBoolReach aim = ArchBoolReach.Dragged;
ArchBoolForm form = ArchBoolForm.Box;
ArchCursorFace face = ArchCursorFace.Floor;
int segments = ArchFootprint.LeastSegments;
float rise = 24f;
float recess = 4f;
float opening = 96f;
bool wallFace;
bool edged;
protected override ArchKind? DraftKind => op switch {
ArchBoolOp.Add => kind switch {
ArchBoolAdd.Slab or ArchBoolAdd.Ramp => ArchKind.Platform,
ArchBoolAdd.Pillar => ArchKind.Pillar,
_ => ArchKind.Beam
},
ArchBoolOp.Subtract or ArchBoolOp.Damage or ArchBoolOp.Extrude => ArchKind.Cut,
ArchBoolOp.Fixture => Fixtures.MergedDraftKind,
_ => ArchKind.WallMod
};
public override bool Adopts => true;
public override bool Hosts( object payload ) {
return payload is ArchPillarPart or ArchWallModPart || base.Hosts( payload );
}
protected override void Adopt( ArchSelection picked ) {
if ( picked?.Item is ArchLadderPart or ArchBalconyPart or ArchExteriorStairPart ) {
op = ArchBoolOp.Fixture;
Fixtures.MergedAdopt( picked );
}
}
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
Fixtures.MergedEnable();
}
ArchPillarType Pillar => Owner.FindPillarType( column.Type ) ?? Owner.DefaultPillar();
bool Hangs => op == ArchBoolOp.Add && kind is ArchBoolAdd.Beam or ArchBoolAdd.Slats;
// Slabs/ramps generate at ground level; everything else rides the building's lift.
bool Lifted => op != ArchBoolOp.Add || kind is not (ArchBoolAdd.Slab or ArchBoolAdd.Ramp);
protected override bool Adjusts => true;
protected override string Title() => "Boolean Modifiers";
protected override string Advice() => op switch {
ArchBoolOp.Add => kind switch {
ArchBoolAdd.Slab => "Drag a box or a circle on open ground — it stands as a solid.",
ArchBoolAdd.Ramp => "Drag from the foot to the head — the deck climbs the way you dragged, from the ground up to the rise.",
ArchBoolAdd.Pillar => "Drag the column's footprint — it stands from this storey's floor, dressed by the pillar type.",
ArchBoolAdd.Beam => "Drag the member — it hangs under the ceiling of the room you drew it in.",
_ => "Drag the zone — it fills with members under the ceiling of the room you drew it in."
},
ArchBoolOp.Subtract when sinking.Ramp
=> "Drag from the foot to the head — the floor it leaves behind climbs the way you dragged, so what is left under it is a ramp.",
ArchBoolOp.Subtract => Reach() switch {
ArchBoolReach.IntoWall => "Drag across a wall to open only that wall and its earlier fittings between the sill and head.",
ArchBoolReach.IntoCeiling => "Drag a box or a circle — it is taken up into the ceiling body only, so a shallow one leaves a coffer.",
ArchBoolReach.IntoFloor => "Drag a box or a circle — it is sunk into the floor slab only, so a shallow one leaves a tray.",
_ => "Drag a box or a circle — it opens this storey's floor and everything standing over it, out through the roof. Pull the top face down for a pit."
},
ArchBoolOp.Damage => $"Drag a damage zone, then choose which supported parts receive its {sinking.Damage.ToString().ToLowerInvariant()} response.",
ArchBoolOp.Extrude => "Drag over a corner, the middle of an elevation or its head — every wall face the zone covers stands proud, parting round the openings.",
ArchBoolOp.Fixture => Fixtures.MergedAdvice,
_ => detail.Kind.Plated()
? "Drag through a wall — every wall the box covers takes a plate at its head on the face you came from."
: "Drag through a wall — every wall the box covers takes a block on the face you came from."
};
ArchBoolReach Reach() => ArchBool.Aimed( aim, face, wallFace );
void Aiming() {
if ( Owner.Cursor( out var cursor ) ) {
face = cursor.Face;
wallFace = cursor.SnappedTo is not null;
if ( Dragging && Owner.NearestWall( DragStart, out _, out _, out var distance ) is not null && distance < 0.5f ) {
wallFace = true;
}
}
}
ArchBoolDrag Sketch( Vector2 from, Vector2 to ) => Sketch( from, to, Host( from, to ) );
protected override void DrawHover( Vector2 point ) {
if ( op == ArchBoolOp.Fixture ) {
Fixtures.MergedHover( point );
return;
}
base.DrawHover( point );
}
ArchBoolDrag Sketch( Vector2 from, Vector2 to, ArchBoolHost host ) {
var floor = Owner.LevelHeight;
var min = Min( from, to );
var max = Max( from, to );
var plane = Plane( host, min, max, floor );
return ArchBool.Sketch( Owner.Grid, Owner.Kit, op, from, to, plane, Climb( min, max, plane ),
form, segments, Reach(), Depth(), Surface( min, max, floor ) );
}
float Depth() => Reach() == ArchBoolReach.IntoWall ? opening : recess;
ArchBoolHost Host( Vector2 from, Vector2 to ) {
return ArchBool.Host( Owner.Plan, Owner.ActiveBuilding(), (Min( from, to ) + Max( from, to )) * 0.5f );
}
float Plane( ArchBoolHost host, Vector2 min, Vector2 max, float floor ) {
if ( op != ArchBoolOp.Add ) {
return host.Road is null ? floor : ArchPlatform.Grade( Owner.Scene, min, max, floor );
}
return kind switch {
ArchBoolAdd.Slab or ArchBoolAdd.Ramp => ArchPlatform.Grade( Owner.Scene, min, max, floor ),
ArchBoolAdd.Pillar => floor,
_ => Soffit( min, max, floor ) - MathF.Max( 1f, timber.Drop )
};
}
float Climb( Vector2 min, Vector2 max, float plane ) {
if ( op == ArchBoolOp.Add ) {
return Hangs ? MathF.Max( 1f, timber.Drop ) : rise;
}
return ArchBool.Through( Owner.Plan, Owner.Kit, Owner.Level, min, max, plane );
}
float Surface( Vector2 min, Vector2 max, float floor ) {
return Reach() == ArchBoolReach.IntoCeiling ? Soffit( min, max, floor ) : floor;
}
float Soffit( Vector2 min, Vector2 max, float floor ) {
var centre = (min + max) * 0.5f;
if ( Owner.RoomOnLevel( centre ) is { } room && Owner.Plan.OwnerOf( room ) is { } building ) {
return ArchFloorGen.Overhead( Owner.Plan, building, room, Owner.Kit, centre );
}
return floor + Owner.Kit.WallHeight - ArchFloorGen.CeilingDepth( Owner.Kit );
}
protected override void DrawPreview() {
if ( op == ArchBoolOp.Fixture ) {
Fixtures.MergedPreview( DragStart, DragCurrent );
return;
}
Aiming();
if ( op == ArchBoolOp.Detail ) {
DrawRectPreview();
DrawCovered( Covered( DragStart, DragCurrent ).ToList() );
return;
}
var host = Host( DragStart, DragCurrent );
var drag = Sketch( DragStart, DragCurrent, host );
var loop = drag.Loop();
var lift = host.Building is null || !Lifted ? 0f : ArchAsks.Lift( Owner.Plan, host.Building, Owner.Kit );
ArchGhost.Solid( loop, Floor( drag, lift ), Deck( drag, lift ) );
ArchGhost.Footprint( loop, Plate( drag ) + lift, ArchGhost.Nth( (int)op ) );
ArchGhost.Note( new Vector3( drag.Max.x, drag.Max.y, drag.Top + lift ),
$"{Named()} — {drag.Max.x - drag.Min.x:0} x {drag.Max.y - drag.Min.y:0}, {drag.Rise:0} {(op == ArchBoolOp.Add ? "high" : "deep")}{Sides()}{Members( drag )}{Gradient( drag )}" );
}
bool Ramps => op == ArchBoolOp.Add ? kind == ArchBoolAdd.Ramp : op == ArchBoolOp.Subtract && sinking.Ramp;
float Plate( ArchBoolDrag drag ) => op == ArchBoolOp.Add && !Ramps ? drag.Top : drag.Bottom;
ArchCarvePlane Deck( ArchBoolDrag drag, float lift ) {
return Ramps && op == ArchBoolOp.Add
? Raked( drag, drag.Top + lift )
: ArchCarvePlane.Level( drag.Top + lift );
}
ArchCarvePlane Floor( ArchBoolDrag drag, float lift ) {
return Ramps && op == ArchBoolOp.Subtract
? Raked( drag, drag.Top + lift )
: ArchCarvePlane.Level( drag.Bottom + lift );
}
ArchCarvePlane Raked( ArchBoolDrag drag, float head ) {
return ArchRamp.Deck( drag.Loop(), head, Fall( drag ), drag.Climb );
}
float Fall( ArchBoolDrag drag ) => op == ArchBoolOp.Subtract ? ArchRamp.Fall( sinking, drag.Rise ) : drag.Rise;
string Gradient( ArchBoolDrag drag ) {
if ( !Ramps ) {
return "";
}
var run = ArchRamp.Run( drag.Loop(), ArchRamp.Climb( drag.Climb ) );
var fall = Fall( drag );
return run < 1f || fall < 1f ? "" : $" · 1:{run / fall:0.#}";
}
string Named() => op switch {
ArchBoolOp.Add => kind.ToString().ToLowerInvariant(),
ArchBoolOp.Subtract when sinking.Ramp => "ramp cut",
ArchBoolOp.Subtract => Reach() switch {
ArchBoolReach.Through => "cut",
ArchBoolReach.IntoWall => "wall cut",
_ => "recess"
},
ArchBoolOp.Damage => "damage",
_ => "block"
};
string Members( ArchBoolDrag drag ) {
if ( kind != ArchBoolAdd.Slats || op != ArchBoolOp.Add ) {
return "";
}
return $" · {ArchBeamGen.Lanes( ArchBool.Hung( drag, Placement( null ) ) )} members";
}
string Sides() => form == ArchBoolForm.Circle ? $" · {segments} sides" : "";
protected override void OnDrag( Vector2 from, Vector2 to ) {
if ( op == ArchBoolOp.Fixture ) {
if ( Fixtures.MergedPlace( from, to ) is { } fixture ) {
var fixtureId = fixture switch {
ArchLadderPart ladder => ladder.Id,
ArchBalconyPart balcony => balcony.Id,
ArchExteriorStairPart flight => flight.Id,
_ => 0
};
Drew( fixture );
FinishDraft( fixtureId );
}
return;
}
if ( op == ArchBoolOp.Detail ) {
PlaceDetail( from, to );
return;
}
var host = Host( from, to );
var drag = Sketch( from, to, host );
var room = Owner.RoomOnLevel( (drag.Min + drag.Max) * 0.5f );
if ( ArchBool.Place( Owner.Plan, host, Owner.Level, drag, Placement( room ) ) is not { } placed ) {
Log.Info( Refused( room ) );
return;
}
var id = placed switch {
ArchPlatformPart platform => platform.Id,
ArchCutPart cut => cut.Id,
ArchPillarPart pillar => pillar.Id,
ArchBeamPart beam => beam.Id,
_ => 0
};
if ( edged ) {
Edge( room, id );
}
FinishDraft( id );
if ( host.Building is not null ) {
Owner.ActiveBuildingId = host.Building.Id;
}
Owner.Picked = new ArchSelection {
Item = placed,
Room = room,
Building = placed is ArchPlatformPart placedPlatform ? Owner.Plan.OwnerOf( placedPlatform ) : host.Building
};
Drew( placed );
Owner.Commit( $"Boolean {op}" );
Refresh();
}
ArchBoolPlacement Placement( ArchRoom room ) {
return new ArchBoolPlacement {
Op = op,
Kind = kind,
Reach = Reach(),
Room = room,
Platform = standing,
Cut = sinking,
Pillar = column,
Beam = timber
};
}
string Refused( ArchRoom room ) {
if ( op == ArchBoolOp.Add && Host( DragStart, DragCurrent ).Building is null ) {
return $"Architecture: a {kind.ToString().ToLowerInvariant()} stands on a storey and a road has none — out there only Subtract applies.";
}
if ( room is null && op == ArchBoolOp.Add && kind is not (ArchBoolAdd.Slab or ArchBoolAdd.Ramp) ) {
return $"Architecture: a {kind.ToString().ToLowerInvariant()} is filed under a room, and there is none on this storey under that drag.";
}
return $"Architecture: that drag is smaller than {ArchBool.Least( op )} inches on a side, so there is nothing in it.";
}
void Edge( ArchRoom under, int hostId ) {
var room = under ?? Owner.ActiveRoom();
if ( room is null || hostId == 0 ) {
return;
}
var trim = new ArchTrimPart {
Id = Owner.Plan.AllocateId(),
Name = "Edge Trim",
Profile = edging.Profile,
Surface = edging.Surface,
FollowsId = hostId,
Closed = true
};
if ( ArchTrimAffector.Refollow( Owner.Plan, trim ) ) {
room.Trims.Add( trim );
}
}
void PlaceDetail( Vector2 from, Vector2 to ) {
var placing = Covered( from, to ).ToList();
if ( placing.Count == 0 ) {
Log.Info( "Architecture: no wall under that box — drag one that covers a wall, or start it in the room and pull it through." );
return;
}
foreach ( var pending in placing ) {
pending.Part.Id = Owner.Plan.AllocateId();
pending.Part.Name = $"{pending.Part.Kind} {pending.Wall.Modifiers.Count + 1}";
pending.Wall.Modifiers.Add( pending.Part );
}
FinishDraft( placing[0].Part.Id );
Owner.Commit( $"Place {placing.Count} {detail.Kind}" );
}
void DrawCovered( IReadOnlyList<Pending> pending ) {
foreach ( var block in pending ) {
ArchWallModBlocks.Preview( block.Part, block.Wall, Owner.Kit, block.Height, block.Floor );
}
}
IEnumerable<Pending> Covered( Vector2 from, Vector2 to ) {
var min = Min( from, to );
var max = Max( from, to );
if ( MathF.Max( max.x - min.x, max.y - min.y ) < Owner.Grid.SubgridSize() ) {
var nearest = Owner.NearestWall( from, out var room, out var along, out var distance );
if ( nearest is not null && room is not null && distance <= Reach( nearest ) ) {
yield return Placed( nearest, room, along, MathF.Max( ArchGridService.FinestSize, detail.Width ), from );
}
yield break;
}
foreach ( var room in Owner.Plan.AllRooms().Where( entry => entry.Floor == Owner.Level ) ) {
foreach ( var wall in room.Walls ) {
if ( Crosses( wall, min, max, out var along, out var width ) ) {
yield return Placed( wall, room, along, width, (min + max) * 0.5f );
}
}
}
}
bool Crosses( ArchWall wall, Vector2 min, Vector2 max, out float along, out float width ) {
along = 0f;
width = 0f;
if ( wall.Length < 1f ) {
return false;
}
var corners = new[] { min, new Vector2( max.x, min.y ), max, new Vector2( min.x, max.y ) };
var runs = corners.Select( corner => Vector2.Dot( corner - wall.Start, wall.Direction ) ).ToList();
var sides = corners.Select( corner => Vector2.Dot( corner - wall.Start, wall.Normal ) ).ToList();
if ( sides.Min() > Reach( wall ) || sides.Max() < -Reach( wall ) ) {
return false;
}
var start = MathF.Max( runs.Min(), 0f );
var finish = MathF.Min( runs.Max(), wall.Length );
if ( finish - start < ArchGridService.FinestSize ) {
return false;
}
along = (start + finish) * 0.5f;
width = finish - start;
return true;
}
float Reach( ArchWall wall ) => ArchWallSection.Thickness( wall, Owner.Kit ) * 0.5f + Owner.Kit.GridSize;
Pending Placed( ArchWall wall, ArchRoom room, float along, float width, Vector2 at ) {
var building = Owner.Plan.OwnerOf( room );
var lift = building is null ? 0f : ArchAsks.Lift( Owner.Plan, building, Owner.Kit );
var standing = ArchWallSection.Height( wall, room, Owner.Kit );
return new Pending {
Wall = wall,
Room = room,
Height = standing,
Floor = room.BaseHeight + lift,
Part = new ArchWallModPart {
Kind = detail.Kind,
Side = Facing( wall, at ),
Along = ArchGridService.Fine( along ),
Width = ArchGridService.Fine( width ),
Base = detail.Kind.Seat( detail.Base, standing ),
Height = detail.Kind.Rise( 0f ),
Depth = detail.Depth,
Field = detail.Field
}
};
}
ArchWallSide Facing( ArchWall wall, Vector2 at ) {
if ( detail.Side != ArchWallSide.OnSurface ) {
return detail.Side;
}
return Vector2.Dot( at - wall.Start, wall.Normal ) < 0f ? ArchWallSide.Outside : ArchWallSide.Inside;
}
static List<Vector2> Loop( ArchWall wall, float from, float to, float near, float far ) {
return new List<Vector2>
{
Point( wall, from, near ),
Point( wall, to, near ),
Point( wall, to, far ),
Point( wall, from, far )
};
}
static Vector2 Point( ArchWall wall, float along, float across ) => wall.PointAt( along ) + wall.Normal * across;
protected override void BuildOptions( ToolSidebarWidget panel ) {
if ( Lent ) {
BuildLent( panel );
return;
}
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Operation", Operation, OperationGlyph, PickOperation );
if ( AsksKind ) {
flow.Step( "Kind", KindName, KindGlyph, PickKind );
}
if ( AsksForm ) {
flow.Step( "Form", FormName, form == ArchBoolForm.Circle ? "circle" : "crop_square", PickForm );
}
if ( AsksReach ) {
flow.Step( "Reach", ReachName, ReachGlyph, PickReach );
}
flow.Step( "Dressing", DressingName, "tune", BuildDressing );
if ( AsksForm && op != ArchBoolOp.Extrude ) {
flow.Step( "Edge run", edged ? edging.Profile : "None", "timeline", BuildEdge );
}
}
void BuildLent( ToolSidebarWidget panel ) {
if ( Owner.Picked is not { Item: not null } picked ) {
return;
}
if ( picked.Item is ArchLadderPart or ArchBalconyPart or ArchExteriorStairPart ) {
Fixtures.MergedOptions( panel );
return;
}
ArchPartUi.Sheet( panel, Owner, picked, Refresh );
}
bool Zoning => op is ArchBoolOp.Damage or ArchBoolOp.Extrude;
bool AsksKind => op is ArchBoolOp.Add or ArchBoolOp.Detail || Zoning;
bool AsksForm => op is ArchBoolOp.Add or ArchBoolOp.Subtract || Zoning;
bool AsksReach => op is ArchBoolOp.Subtract or ArchBoolOp.Damage;
string Operation => op switch {
ArchBoolOp.Add => "Add",
ArchBoolOp.Subtract => "Subtract",
ArchBoolOp.Fixture => "Fixture",
ArchBoolOp.Detail => "Face fitting",
_ => "Zone"
};
string OperationGlyph => op switch {
ArchBoolOp.Add => "add_box",
ArchBoolOp.Subtract => "disabled_by_default",
ArchBoolOp.Fixture => "table_rows",
ArchBoolOp.Detail => "view_column",
_ => "select_all"
};
void PickOperation( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "Add — the shape stands as a solid", "bool_add", "add_box",
op == ArchBoolOp.Add, () => Operating( ArchBoolOp.Add, step ) );
grid.Pick( "Subtract — the shape is taken out of everything it passes through", "bool_subtract", "disabled_by_default",
op == ArchBoolOp.Subtract, () => Operating( ArchBoolOp.Subtract, step ) );
grid.Pick( "Zone — an influence zone: what it covers breaks, stands proud or is bitten back", "bool_zone", "select_all",
Zoning, () => Operating( zone, step ) );
grid.Pick( "Fixtures — ladders, balconies and exterior stairs hung from a wall", "bool_fixtures", "table_rows",
op == ArchBoolOp.Fixture, () => Operating( ArchBoolOp.Fixture, step ) );
grid.Pick( "Face fitting — one block per wall face the box covers, on the side you came from", "bool_detail", "view_column",
op == ArchBoolOp.Detail, () => Operating( ArchBoolOp.Detail, step ) );
}
void Operating( ArchBoolOp chosen, ArchWorkflowStep step ) {
op = chosen;
step.Chose();
}
string KindName => op switch {
ArchBoolOp.Add => kind.ToString(),
ArchBoolOp.Damage => "Damage",
ArchBoolOp.Extrude => sinking.Extrude == ArchExtrudeKind.Indent ? "Recess" : sinking.Extrude.ToString(),
_ => detail.Kind.ToString()
};
string KindGlyph => op switch {
ArchBoolOp.Add => kind switch {
ArchBoolAdd.Slab => "crop_landscape",
ArchBoolAdd.Ramp => "trending_up",
ArchBoolAdd.Pillar => "view_column",
ArchBoolAdd.Beam => "horizontal_rule",
_ => "view_stream"
},
ArchBoolOp.Damage => "broken_image",
ArchBoolOp.Extrude => ArchExtrude.Glyph( sinking.Extrude ),
_ => "view_column"
};
void PickKind( ArchWorkflowStep step ) {
if ( op == ArchBoolOp.Add ) {
PickSolid( step );
return;
}
if ( Zoning ) {
PickZone( step );
return;
}
ArchSidebarLayout.Form( step.Layout, form => ArchWallForms.ModKinds( form, detail.Kind, chosen => {
ArchWallModUi.Adopt( detail, chosen );
step.Chose();
} ) );
}
void PickSolid( ArchWorkflowStep step ) {
using var kinds = ArchIconGrid.In( step.Layout );
kinds.Pick( "Slab — a plinth poured on the ground it covers", "bool_slab", "crop_landscape",
kind == ArchBoolAdd.Slab, () => Adopt( ArchBoolAdd.Slab, step ) );
kinds.Pick( "Ramp — the same slab with a raked deck, climbing the way you drag", "bool_ramp", "trending_up",
kind == ArchBoolAdd.Ramp, () => Adopt( ArchBoolAdd.Ramp, step ) );
kinds.Pick( "Pillar — a column on the dragged footprint, dressed by a pillar type", "bool_pillar", "view_column",
kind == ArchBoolAdd.Pillar, () => Adopt( ArchBoolAdd.Pillar, step ) );
kinds.Pick( "Beam — one member hung under the ceiling", "bool_beam", "horizontal_rule",
kind == ArchBoolAdd.Beam, () => Adopt( ArchBoolAdd.Beam, step ) );
kinds.Pick( "Slats — the shape filled with members", "bool_slats", "view_stream",
kind == ArchBoolAdd.Slats, () => Adopt( ArchBoolAdd.Slats, step ) );
}
void PickZone( ArchWorkflowStep step ) {
using var zones = ArchIconGrid.In( step.Layout );
zones.Pick( "Damage — the parts it covers break, spall or lose panels", "bool_damage", "broken_image",
op == ArchBoolOp.Damage, () => Zoned( ArchBoolOp.Damage, ArchExtrudeKind.None, step ) );
foreach ( var standing in Proud ) {
var chosen = standing;
zones.Pick( ArchExtrude.Label( chosen ), $"extrude_{chosen.ToString().ToLowerInvariant()}", ArchExtrude.Glyph( chosen ),
op == ArchBoolOp.Extrude && sinking.Extrude == chosen, () => Zoned( ArchBoolOp.Extrude, chosen, step ) );
}
}
static readonly ArchExtrudeKind[] Proud =
{
ArchExtrudeKind.Face, ArchExtrudeKind.Quoins, ArchExtrudeKind.Courses, ArchExtrudeKind.Indent, ArchExtrudeKind.Pier
};
void Zoned( ArchBoolOp chosen, ArchExtrudeKind standing, ArchWorkflowStep step ) {
op = chosen;
zone = chosen;
if ( standing == ArchExtrudeKind.None ) {
sinking.Affects = ArchDamage.Targets( sinking.ResolvedDamage );
} else {
sinking.Extrude = standing;
sinking.Affects = ArchExtrude.Targets( standing );
}
step.Chose();
}
void Adopt( ArchBoolAdd chosen, ArchWorkflowStep step ) {
if ( chosen != kind ) {
rise = Rising( chosen );
}
kind = chosen;
step.Chose();
}
float Rising( ArchBoolAdd chosen ) => chosen switch {
ArchBoolAdd.Pillar or ArchBoolAdd.Ramp => Owner.Kit.WallHeight,
ArchBoolAdd.Slab => 24f,
_ => rise
};
string FormName => form == ArchBoolForm.Circle ? $"Circle · {segments} sides" : "Box";
void PickForm( ArchWorkflowStep step ) {
using ( var grid = ArchIconGrid.In( step.Layout ) ) {
grid.Pick( "Box — four corners on the grid", "bool_box", "crop_square", form == ArchBoolForm.Box,
() => { form = ArchBoolForm.Box; step.Chose(); } );
grid.Pick( "Circle — an n-gon inscribed in the drag", "bool_circle", "circle", form == ArchBoolForm.Circle,
() => { form = ArchBoolForm.Circle; Refresh(); } );
}
if ( form != ArchBoolForm.Circle ) {
return;
}
step.Layout.Add( ArchPartUi.Integer( "Sides", segments, ArchFootprint.LeastSegments,
value => segments = Math.Max( ArchFootprint.LeastSegments, value ) ) );
}
string ReachName => aim switch {
ArchBoolReach.Through => "Through",
ArchBoolReach.IntoCeiling => $"Ceiling · {recess:0}",
ArchBoolReach.IntoFloor => $"Floor · {recess:0}",
ArchBoolReach.IntoWall => $"Wall · {opening:0}",
_ => "Where you drag"
};
string ReachGlyph => aim switch {
ArchBoolReach.Through => "vertical_align_top",
ArchBoolReach.IntoCeiling => "border_top",
ArchBoolReach.IntoFloor => "border_bottom",
ArchBoolReach.IntoWall => "door_open",
_ => "ads_click"
};
void PickReach( ArchWorkflowStep step ) {
using ( var grid = ArchIconGrid.In( step.Layout ) ) {
grid.Pick( "Where you drag — the floor looking down, the ceiling looking up at it", "bool_dragged", "ads_click",
aim == ArchBoolReach.Dragged, () => Aiming( ArchBoolReach.Dragged, step ) );
grid.Pick( "Through — out past everything standing over it", "bool_through", "vertical_align_top",
aim == ArchBoolReach.Through, () => Aiming( ArchBoolReach.Through, step ) );
grid.Pick( "Into the ceiling — a coffer, taken up into the ceiling body only", "bool_coffer", "border_top",
aim == ArchBoolReach.IntoCeiling, () => { aim = ArchBoolReach.IntoCeiling; Refresh(); } );
grid.Pick( "Into the floor — a tray, sunk into the slab only", "bool_tray", "border_bottom",
aim == ArchBoolReach.IntoFloor, () => { aim = ArchBoolReach.IntoFloor; Refresh(); } );
grid.Pick( "Into the wall — an opening between its sill and head", "bool_wall", "door_open",
aim == ArchBoolReach.IntoWall, () => { aim = ArchBoolReach.IntoWall; Refresh(); } );
}
if ( aim == ArchBoolReach.IntoWall ) {
step.Layout.Add( ArchPartUi.Number( "Opening height", opening, 96f, value => opening = MathF.Max( 4f, value ) ) );
return;
}
if ( aim is ArchBoolReach.IntoCeiling or ArchBoolReach.IntoFloor ) {
step.Layout.Add( ArchPartUi.Number( "Recess depth", recess, 4f, value => recess = MathF.Max( 1f, value ) ) );
}
}
void Aiming( ArchBoolReach chosen, ArchWorkflowStep step ) {
aim = chosen;
step.Chose();
}
string DressingName => op switch {
ArchBoolOp.Fixture => Fixtures.MergedTitle,
ArchBoolOp.Add => Hangs ? $"{MathF.Max( 1f, timber.Drop ):0} drop" : Ramps ? $"{rise:0} head" : $"{rise:0} rise",
ArchBoolOp.Damage => sinking.ResolvedDamage.ToString(),
ArchBoolOp.Extrude => $"{ArchExtrude.Reach( sinking ):0} deep",
ArchBoolOp.Detail => detail.Side.ToString(),
_ => sinking.Enclosure == CutEnclosure.Walls ? "Walled shaft" : "Open shaft"
};
void BuildDressing( ArchWorkflowStep step ) {
if ( op == ArchBoolOp.Fixture ) {
ArchSidebarLayout.Form( step.Layout, Fixtures.MergedOptions );
return;
}
if ( op == ArchBoolOp.Detail ) {
BuildFitting( step );
return;
}
ArchSidebarLayout.Form( step.Layout, form => {
switch ( op ) {
case ArchBoolOp.Add:
BuildSolid( form );
break;
case ArchBoolOp.Damage:
ArchBoolUi.Damage( form, sinking, Refresh, null );
break;
case ArchBoolOp.Extrude:
ArchBoolUi.Extrude( form, sinking, Refresh, null, kinds: false );
break;
default:
ArchBoolUi.Cut( form, sinking, Refresh, null );
break;
}
} );
}
void BuildSolid( ToolSidebarWidget form ) {
if ( kind == ArchBoolAdd.Pillar ) {
form.AddGroup( "Placement" ).Add( ArchPartUi.Number( "Height", rise, 128f, value => rise = MathF.Max( 4f, value ) ) );
ArchAsks.PillarKinds( form, Owner.OfferedPillars(), column.Type, Dress );
ArchAsks.BuildPillar( form, column, Pillar, Refresh, null );
return;
}
if ( Hangs ) {
if ( kind == ArchBoolAdd.Slats ) {
ArchAsks.PillarKinds( form, Owner.OfferedPillars(), timber.Type,
type => { ArchBeamGen.Dressed( timber, type ); Refresh(); } );
}
ArchBoolUi.Section( form, timber, kind == ArchBoolAdd.Slats, null );
return;
}
form.AddGroup( "Placement" ).Add( ArchPartUi.Number( Ramps ? "Head" : "Rise", rise, 24f, value => rise = MathF.Max( 4f, value ) ) );
if ( Ramps ) {
return;
}
ArchBoolUi.Guard( form, standing, Refresh, null );
ArchBoolUi.Coping( form, standing, Refresh, null );
}
void BuildFitting( ArchWorkflowStep step ) {
ArchSidebarLayout.Form( step.Layout, form => {
ArchWallForms.ModSides( form, detail.Side, true, side => { detail.Side = side; Refresh(); } );
ArchWallForms.ModSizes( form, detail, null, false );
ArchWallForms.ModFill( form, detail, null );
} );
step.Layout.Add( ArchSidebarLayout.Advice( detail.Kind.Plated()
? "Seated at its wall's head like a fascia board — drag the plate's foot and head widgets to move it down the elevation."
: "Placed at its wall's own head — drag the block's head widget to run it past the floor line." ) );
}
void Dress( ArchPillarType type ) {
column = type.Standing( default, 0f, 0f );
Refresh();
}
void BuildEdge( ArchWorkflowStep step ) {
step.Layout.Add( ArchPartUi.Check( "Trim the edge", edged, value => { edged = value; Refresh(); } ) );
if ( !edged ) {
return;
}
step.Layout.Add( Wrapped( "A run filed under the shape, tracing its outline — round on a circle, square on a box." ) );
ArchSidebarLayout.Form( step.Layout, form => ArchPartUi.Trim( form, edging, Owner.Kit, null ) );
}
sealed class Pending {
public ArchWall Wall;
public ArchRoom Room;
public float Height;
public float Floor;
public ArchWallModPart Part;
}
}