Editor UI code for architectural boolean tools. It builds sidebar controls for shaping boolean operations, platforms, ramps, beams, damage/cut presets and which building parts are affected, wiring UI elements to model properties and change/refresh callbacks.
using System;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// Read by the Boolean Modifiers subtool and by Select, off the same records - a draft and a placed shape
// carry identical controls because they ARE the same records. Neither a platform nor a cut had a property
// sheet before this: selecting one showed an empty shelf.
public static class ArchBoolUi
{
static bool AffectsExpanded
{
get => EditorCookie.Get( "arch.bool.affects.expanded", false );
set => EditorCookie.Set( "arch.bool.affects.expanded", value );
}
public static void Shape( ToolSidebarWidget panel, ArchBoolForm form, int segments, Action<ArchBoolForm> reform, Action<int> resegment )
{
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Shape" ) ) )
{
grid.Pick( "Box — four corners on the grid", "bool_box", "crop_square", form == ArchBoolForm.Box,
() => reform( ArchBoolForm.Box ) );
grid.Pick( "Circle — an n-gon inscribed in the drag", "bool_circle", "circle", form == ArchBoolForm.Circle,
() => reform( ArchBoolForm.Circle ) );
}
if ( form != ArchBoolForm.Circle )
{
return;
}
var sides = panel.AddGroup( "Circle" );
sides.Add( ArchPartUi.Integer( "Sides", segments, ArchFootprint.LeastSegments,
value => resegment( Math.Max( ArchFootprint.LeastSegments, value ) ) ) );
}
// A tick that GATES the fields under it has to rebuild the sheet as well as commit, or the fields it reveals
// never arrive and the ones it hides stay put. Handing these only `changed` is why ticking the edge dressing
// did nothing you could see, and why the coping numbers could not be reached at all.
static Action Both( Action changed, Action refresh )
{
return () =>
{
changed?.Invoke();
refresh?.Invoke();
};
}
public static void Platform( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed )
{
var slab = panel.AddGroup( "Solid" );
slab.Add( ArchPartUi.Number( platform.Ramp ? "Head" : "Rise", platform.Rise, 24f,
value => platform.TopHeight = platform.GradeHeight + MathF.Max( 4f, value ), changed ) );
slab.Add( ArchPartUi.Check( "Ramp the deck", platform.Ramp, value => platform.Ramp = value, Both( changed, refresh ) ) );
if ( platform.Ramp )
{
Ramp( panel, platform, "rise", changed );
return;
}
var guard = panel.AddGroup( "Guardrail" );
guard.Add( ArchPartUi.Check( "Guard rail round the edge", platform.Guardrail, value => platform.Guardrail = value, Both( changed, refresh ) ) );
if ( platform.Guardrail )
{
guard.Add( ArchPartUi.Number( "Height", platform.GuardHeight, 42f,
value => platform.GuardHeight = MathF.Max( 12f, value ), changed ) );
}
var coping = panel.AddGroup( "Coping" );
coping.Add( ArchPartUi.Check( "Coping band", platform.Coping, value => platform.Coping = value, Both( changed, refresh ) ) );
if ( !platform.Coping )
{
return;
}
coping.Add( ArchPartUi.Number( "Height", platform.CopingHeight, 4f, value => platform.CopingHeight = value, changed ) );
coping.Add( ArchPartUi.Number( "Width", platform.CopingWidth, 8f, value => platform.CopingWidth = value, changed ) );
coping.Add( ArchPartUi.Number( "Oversail", platform.CopingOversail, 1.5f, value => platform.CopingOversail = value, changed ) );
coping.Add( ArchPartUi.Check( "Round the inside edges too", platform.CopingInside, value => platform.CopingInside = value, changed ) );
}
// Which way it climbs and how far it drops getting to its foot. The gradient is never authored - it falls
// out of the run the footprint has - so a ramp dragged longer is a gentler one with no second edit. Shared
// by the platform that rakes its top and the cut that rakes the floor it leaves: same three numbers, one form.
// A coping is not offered: a run is level, and a raked deck has no one height to walk a band round.
public static void Ramp( ToolSidebarWidget panel, IArchRamped part, string shelf, Action changed )
{
var ramp = panel.AddGroup( "Ramp" );
ramp.Add( ArchPartUi.Number( "Climb angle", part.RampYaw, 0f,
value => part.RampYaw = ArchShapeHandles.Facing( value ), changed ) );
ramp.Add( ArchPartUi.Number( "Fall", part.RampFall, 0f,
value => part.RampFall = MathF.Max( 0f, value ), changed ) );
ramp.Add( ArchPartUi.Wrapped( $"Zero fall is the whole {shelf}, so the foot meets the bottom of the band. Type one to leave a level shelf." ) );
}
// One member or a field of them, and the field's own numbers. The drop is measured DOWN from the plane
// it hangs on, so a deeper beam grows into the room rather than up through the ceiling.
public static void Beam( ToolSidebarWidget panel, ArchBeamPart beam, Action refresh, Action changed )
{
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Members" ) ) )
{
grid.Pick( "One member — exactly the shape that was dragged", "beam_one", "horizontal_rule", beam.Members == BeamMembers.One,
() => { beam.Members = BeamMembers.One; changed?.Invoke(); refresh?.Invoke(); } );
grid.Pick( "Filled — that same shape repeated as slats", "beam_field", "view_stream", beam.Members == BeamMembers.Field,
() => { beam.Members = BeamMembers.Field; changed?.Invoke(); refresh?.Invoke(); } );
}
Section( panel, beam, beam.Members == BeamMembers.Field, changed );
}
// Read by the placement tool too, where the Members picker would only repeat the kind already chosen.
public static void Section( ToolSidebarWidget panel, ArchBeamPart beam, bool field, Action changed )
{
var section = panel.AddGroup( "Section" );
section.Add( ArchPartUi.Number( "Drop", beam.Drop, 12f, value => beam.Drop = MathF.Max( 1f, value ), changed ) );
if ( !field )
{
return;
}
section.Add( ArchPartUi.Number( "Member width", beam.MemberWidth, 6f, value => beam.MemberWidth = MathF.Max( 1f, value ), changed ) );
section.Add( ArchPartUi.Number( "Gap", beam.MemberGap, 10f, value => beam.MemberGap = MathF.Max( 0f, value ), changed ) );
section.Add( ArchPartUi.Number( "Angle", beam.Yaw, 0f, value => beam.Yaw = value, changed ) );
section.Add( ArchPartUi.Integer( "Sides", beam.Sides, 0, value => beam.Sides = Math.Max( 0, value ), changed ) );
}
public static void Damage( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed )
{
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Damage type" ) ) )
{
Damage( grid, cut, ArchDamageKind.Masonry, "Masonry — staggered exposed bricks at wall and pillar corners", "damage_masonry", "view_module", refresh, changed );
Damage( grid, cut, ArchDamageKind.SurfaceSpall, "Surface spall — a shallow chipped region", "damage_spall", "texture", refresh, changed );
Damage( grid, cut, ArchDamageKind.MissingPanels, "Missing panels — selected ceiling cells are absent", "damage_missing_panels", "grid_off", refresh, changed );
Damage( grid, cut, ArchDamageKind.DisplacedPanels, "Displaced panels — selected ceiling cells hang dropped and tilted", "damage_displaced_panels", "view_quilt", refresh, changed );
Damage( grid, cut, ArchDamageKind.MixedPanels, "Mixed panels — selected ceiling cells are missing or displaced", "damage_mixed_panels", "dashboard", refresh, changed );
}
var pattern = panel.AddGroup( "Pattern" );
if ( cut.ResolvedDamage == ArchDamageKind.Masonry )
{
MasonryPresets( pattern, cut, refresh, changed );
ArchSidebarLayout.Form( pattern, nested =>
ArchSidebarSection.Disclosure( nested, "Damage.masonry.advanced", "Advanced", true, advanced =>
{
advanced.Add( ArchPartUi.Number( "Course coverage", cut.DamageAmount, 1f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );
advanced.Add( ArchPartUi.Number( "Brick width", cut.DamageCellWidth, 16f, value => cut.DamageCellWidth = MathF.Max( 2f, value ), changed ) );
advanced.Add( ArchPartUi.Number( "Course height", cut.DamageCellLength, 8f, value => cut.DamageCellLength = MathF.Max( 2f, value ), changed ) );
advanced.Add( ArchPartUi.Number( "Reveal depth", cut.DamageDepth, 6f, value => cut.DamageDepth = MathF.Max( 0.25f, value ), changed ) );
advanced.Add( ArchPartUi.Check( "Carve wall finish", cut.MasonryCarves, value => cut.MasonryCarves = value, changed ) );
} ) );
}
else if ( ArchDamage.Panels( cut.ResolvedDamage ) )
{
pattern.Add( ArchPartUi.Number( "Amount", cut.DamageAmount, 0.55f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );
pattern.Add( ArchPartUi.Number( "Panel width", cut.DamageCellWidth, 24f, value => cut.DamageCellWidth = MathF.Max( 4f, value ), changed ) );
pattern.Add( ArchPartUi.Number( "Panel length", cut.DamageCellLength, 48f, value => cut.DamageCellLength = MathF.Max( 4f, value ), changed ) );
if ( cut.ResolvedDamage is ArchDamageKind.DisplacedPanels or ArchDamageKind.MixedPanels )
{
pattern.Add( ArchPartUi.Number( "Drop", cut.DamageDrop, 4f, value => cut.DamageDrop = MathF.Max( 0f, value ), changed ) );
pattern.Add( ArchPartUi.Number( "Tilt", cut.DamageTilt, 12f, value => cut.DamageTilt = MathF.Max( 0f, value ), changed ) );
}
}
else
{
pattern.Add( ArchPartUi.Number( "Amount", cut.DamageAmount, 0.55f, value => cut.DamageAmount = Math.Clamp( value, 0f, 1f ), changed ) );
pattern.Add( ArchPartUi.Number( "Damage depth", cut.DamageDepth, 4f, value => cut.DamageDepth = MathF.Max( 0.5f, value ), changed ) );
}
Affects( panel, cut, refresh, changed, ArchDamage.Targets( cut.ResolvedDamage ) );
}
static void Damage( ArchIconGrid grid, ArchCutPart cut, ArchDamageKind kind, string tooltip, string slug, string glyph, Action refresh, Action changed )
{
grid.Pick( tooltip, slug, glyph, cut.ResolvedDamage == kind, () =>
{
cut.Damage = kind;
cut.BreakEdges = kind == ArchDamageKind.Masonry;
cut.Affects = ArchDamage.Targets( kind );
cut.DamageAmount = kind == ArchDamageKind.Masonry ? 1f : cut.DamageAmount;
cut.DamageCellWidth = kind == ArchDamageKind.Masonry ? 16f : cut.DamageCellWidth;
cut.DamageCellLength = kind == ArchDamageKind.Masonry ? 8f : cut.DamageCellLength;
cut.DamageDepth = kind == ArchDamageKind.Masonry ? 6f : cut.DamageDepth;
cut.MasonryPattern = kind == ArchDamageKind.Masonry ? ArchMasonryPattern.ExposedEdge : cut.MasonryPattern;
cut.MasonryCarves = kind == ArchDamageKind.Masonry ? false : cut.MasonryCarves;
changed?.Invoke();
refresh?.Invoke();
} );
}
static void MasonryPresets( Layout pattern, ArchCutPart cut, Action refresh, Action changed )
{
using var presets = ArchIconGrid.In( pattern );
presets.Pick( "Bricks - simple one-brick boxes extruded from an intact corner", "masonry_bricks", "view_in_ar",
MasonryMatches( cut, ArchMasonryPattern.ExposedEdge, 16f, 8f, 6f, false ), () => ApplyMasonry( cut, ArchMasonryPattern.ExposedEdge, 16f, 8f, 6f, false, refresh, changed ) );
presets.Pick( "Edge — two bricks, a header and intact courses at a broken wall edge", "masonry_edge", "view_module",
MasonryMatches( cut, ArchMasonryPattern.ExposedEdge, 12f, 4f, 1.25f, true ), () => ApplyMasonry( cut, ArchMasonryPattern.ExposedEdge, 12f, 4f, 1.25f, true, refresh, changed ) );
presets.Pick( "Worn — a fuller broken bond with fewer intact courses", "masonry_worn", "texture",
MasonryMatches( cut, ArchMasonryPattern.WornCorner, 12f, 4f, 1.75f, true ), () => ApplyMasonry( cut, ArchMasonryPattern.WornCorner, 12f, 4f, 1.75f, true, refresh, changed ) );
presets.Pick( "Pier — dense alternating long and header courses around square pillars", "masonry_pier", "view_column",
MasonryMatches( cut, ArchMasonryPattern.BrickPier, 12f, 4f, 0.75f, false ), () => ApplyMasonry( cut, ArchMasonryPattern.BrickPier, 12f, 4f, 0.75f, false, refresh, changed ) );
}
static void ApplyMasonry( ArchCutPart cut, ArchMasonryPattern pattern, float width, float course, float depth, bool carves, Action refresh, Action changed )
{
cut.MasonryPattern = pattern;
cut.DamageAmount = 1f;
cut.DamageCellWidth = width;
cut.DamageCellLength = course;
cut.DamageDepth = depth;
cut.MasonryCarves = carves;
changed?.Invoke();
refresh?.Invoke();
}
static bool MasonryMatches( ArchCutPart cut, ArchMasonryPattern pattern, float width, float course, float depth, bool carves )
{
return cut.MasonryPattern == pattern
&& cut.MasonryCarves == carves
&& MathF.Abs( cut.DamageAmount - 1f ) < 0.001f
&& MathF.Abs( cut.DamageCellWidth - width ) < 0.001f
&& MathF.Abs( cut.DamageCellLength - course ) < 0.001f
&& MathF.Abs( cut.DamageDepth - depth ) < 0.001f;
}
// A cut that only carves builds nothing, so what it WEARS is the whole form: the walls it carries past
// the solid it started in, and whatever caps it where it comes out.
public static void Cut( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed )
{
Affects( panel, cut, refresh, changed );
var floor = panel.AddGroup( "Floor" );
floor.Add( ArchPartUi.Check( "Ramp the floor it leaves", cut.Ramp, value => cut.Ramp = value, Both( changed, refresh ) ) );
if ( cut.Ramp )
{
Ramp( panel, cut, "depth", changed );
}
var edge = panel.AddGroup( "Opening edge" );
edge.Add( ArchPartUi.Check( "Dress the cut edge", cut.Edge, value => cut.Edge = value, Both( changed, refresh ) ) );
if ( cut.Edge )
{
edge.Add( ArchPartUi.Number( "Edge width", cut.EdgeWidth, 0f,
value => cut.EdgeWidth = MathF.Max( 0f, value ), changed ) );
}
edge.Add( ArchPartUi.Check( "Break the cut edge", cut.BreakEdges, value => cut.BreakEdges = value, changed ) );
edge.Add( ArchPartUi.Check( "Rail the stair edges it opens", cut.GuardsOpenedEdges,
value => cut.GuardsOpenedEdges = value, changed ) );
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Enclosure" ) ) )
{
grid.Pick( "Open — the shaft is the absence in what it passed through", "cut_open", "crop_free",
cut.Enclosure == CutEnclosure.None, () => { cut.Enclosure = CutEnclosure.None; changed?.Invoke(); refresh?.Invoke(); } );
grid.Pick( "Walled — it carries its own walls up past the solid it started in", "cut_walled", "crop_square",
cut.Enclosure == CutEnclosure.Walls, () => { cut.Enclosure = CutEnclosure.Walls; changed?.Invoke(); refresh?.Invoke(); } );
}
if ( cut.Enclosure == CutEnclosure.Walls )
{
var shell = panel.AddGroup( "Walls" );
shell.Add( ArchPartUi.Number( "Thickness", cut.WallThickness, 8f, value => cut.WallThickness = value, changed ) );
}
using ( var grid = ArchIconGrid.In( panel.AddGroup( "Head" ) ) )
{
grid.Pick( "Open to the sky", "cut_head_none", "crop_free",
cut.Head == CutHead.None, () => { cut.Head = CutHead.None; changed?.Invoke(); refresh?.Invoke(); } );
grid.Pick( "Capped — a lid over the top", "cut_head_cap", "horizontal_rule",
cut.Head == CutHead.Cap, () => { cut.Head = CutHead.Cap; changed?.Invoke(); refresh?.Invoke(); } );
grid.Pick( "Coped — a band round the mouth, finished like an outside edge", "cut_head_coping", "border_top",
cut.Head == CutHead.Coping, () => { cut.Head = CutHead.Coping; changed?.Invoke(); refresh?.Invoke(); } );
grid.Pick( "Housing — a little walled box with its own roof", "cut_head_housing", "home",
cut.Head == CutHead.Housing, () => { cut.Head = CutHead.Housing; changed?.Invoke(); refresh?.Invoke(); } );
}
if ( cut.Head == CutHead.Coping )
{
var coping = panel.AddGroup( "Coping" );
coping.Add( ArchPartUi.Number( "Height", cut.CopingHeight, 4f, value => cut.CopingHeight = value, changed ) );
coping.Add( ArchPartUi.Number( "Oversail", cut.CopingOversail, 1.5f, value => cut.CopingOversail = value, changed ) );
coping.Add( ArchPartUi.Number( "Width", cut.WallThickness, 8f, value => cut.WallThickness = value, changed ) );
return;
}
if ( cut.Head != CutHead.Housing )
{
return;
}
var housing = panel.AddGroup( "Housing" );
housing.Add( ArchPartUi.Number( "Height", cut.HeadHeight, 96f, value => cut.HeadHeight = value, changed ) );
housing.Add( ArchPartUi.Check( "Roof over it", cut.HeadRoof, value => cut.HeadRoof = value, changed ) );
}
static void Affects( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed, ArchCutAffects allowed = ArchCutAffects.All )
{
var open = AffectsExpanded;
var enabled = Enum.GetValues<ArchCutAffects>()
.Count( target => target is not (ArchCutAffects.None or ArchCutAffects.All) && (allowed & target) == target && ArchCut.Affects( cut, target ) );
var targets = Enum.GetValues<ArchCutAffects>().Count( target => target is not (ArchCutAffects.None or ArchCutAffects.All) && (allowed & target) == target );
var toggle = new Button( $"Affects ({enabled}/{targets})", open ? "expand_less" : "expand_more" )
{
Clicked = () => { AffectsExpanded = !open; refresh?.Invoke(); }
};
panel.Layout.Add( toggle );
if ( !open )
{
return;
}
var affects = panel.AddGroup( "Affects" );
Affect( affects, cut, allowed, "Floors", ArchCutAffects.Floors, changed );
Affect( affects, cut, allowed, "Ceilings", ArchCutAffects.Ceilings, changed );
Affect( affects, cut, allowed, "Foundations", ArchCutAffects.Foundations, changed );
Affect( affects, cut, allowed, "Walls and corners", ArchCutAffects.Walls, changed );
Affect( affects, cut, allowed, "Wall fittings", ArchCutAffects.WallFittings, changed );
Affect( affects, cut, allowed, "Windows and doors", ArchCutAffects.Windows, changed );
Affect( affects, cut, allowed, "Roofs", ArchCutAffects.Roofs, changed );
Affect( affects, cut, allowed, "Platforms", ArchCutAffects.Platforms, changed );
Affect( affects, cut, allowed, "Pillars", ArchCutAffects.Pillars, changed );
Affect( affects, cut, allowed, "Beams", ArchCutAffects.Beams, changed );
Affect( affects, cut, allowed, "Stairs", ArchCutAffects.Stairs, changed );
Affect( affects, cut, allowed, "Trims", ArchCutAffects.Trims, changed );
Affect( affects, cut, allowed, "Gutters", ArchCutAffects.Gutters, changed );
Affect( affects, cut, allowed, "Road strips", ArchCutAffects.Roadway, changed );
Affect( affects, cut, allowed, "Tunnel lining", ArchCutAffects.Lining, changed );
}
static void Affect( Layout group, ArchCutPart cut, ArchCutAffects allowed, string label, ArchCutAffects target, Action changed )
{
if ( (allowed & target) == target )
{
Affect( group, cut, label, target, changed );
}
}
static void Affect( Layout group, ArchCutPart cut, string label, ArchCutAffects target, Action changed )
{
group.Add( ArchPartUi.Check( label, ArchCut.Affects( cut, target ), enabled =>
{
cut.Affects = enabled ? cut.Affects | target : cut.Affects & ~target;
}, changed ) );
}
}