Editor/Bool/ArchBoolUi.cs
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 );
}
// 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;
}
Guard( panel, platform, refresh, changed );
Coping( panel, platform, refresh, changed );
}
// The two groups the placement tool asks for as well, where the rise is the seed's own and the rake is the
// kind already chosen - so it reads these rather than the whole sheet, and a change lands in one place.
public static void Guard( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action changed ) {
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 ) {
return;
}
guard.Add( ArchPartUi.Number( "Height", platform.GuardHeight, 42f,
value => platform.GuardHeight = MathF.Max( 12f, value ), changed ) );
ArchBarrierUi.GuardStyles( guard, platform.GuardStyle, style => {
platform.GuardStyle = style;
changed?.Invoke();
} );
}
public static void Coping( ToolSidebarWidget panel, ArchPlatformPart platform, Action refresh, Action 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 ) );
}
// The proud sibling of the damage sheet. There is no Affects group: an extrude zone stands work on a wall face
// and a wall face is the only host that can answer it, so offering the mask would be offering a way to break it.
// The kind row is the caller's where the caller already asked for it: the placement tool answers it in its own
// step, and two grids of one choice light independently.
public static void Extrude( ToolSidebarWidget panel, ArchCutPart cut, Action refresh, Action changed, bool kinds = true ) {
if ( kinds ) {
using var grid = ArchIconGrid.In( panel.AddGroup( "Extrude" ) );
Extrude( grid, cut, ArchExtrudeKind.Face, refresh, changed );
Extrude( grid, cut, ArchExtrudeKind.Quoins, refresh, changed );
Extrude( grid, cut, ArchExtrudeKind.Courses, refresh, changed );
Extrude( grid, cut, ArchExtrudeKind.Indent, refresh, changed );
Extrude( grid, cut, ArchExtrudeKind.Pier, refresh, changed );
}
var recessing = cut.Extrude == ArchExtrudeKind.Indent;
var standing = ArchExtrude.Stands( cut.Extrude );
var block = panel.AddGroup( recessing ? "Recess" : standing ? "Pier" : "Block" );
block.Add( ArchPartUi.Number( recessing ? "Depth" : "Proud", cut.ExtrudeDepth, 4f,
value => cut.ExtrudeDepth = MathF.Max( ArchExtrude.LeastDepth, value ), changed ) );
// A recess has no far side to offer: on both faces it would be a hole, which is Subtract's job.
if ( !recessing ) {
block.Add( ArchPartUi.Check( "Both faces", cut.ExtrudeBothFaces, value => cut.ExtrudeBothFaces = value, changed ) );
}
if ( standing ) {
using ( var founding = ArchIconGrid.In( panel.AddGroup( "Footing" ) ) ) {
Footing( founding, cut, null, "Type's own — whatever the pillar type authored", "footing_type", "block", refresh, changed );
Footing( founding, cut, PillarFooting.None, "None — the shaft runs straight into what it stands on", "footing_none", "remove", refresh, changed );
Footing( founding, cut, PillarFooting.Square, "Square — a plain pad, buried", "footing_square", "crop_square", refresh, changed );
Footing( founding, cut, PillarFooting.Bevelled, "Bevelled — a splayed pad, buried", "footing_bevelled", "change_history", refresh, changed );
Footing( founding, cut, PillarFooting.Stepped, "Stepped — two courses stepping out, buried", "footing_stepped", "stairs", refresh, changed );
}
// Everything else about the column - footing, plinth, banded shaft, capital - is the TYPE's, so this is
// the only other choice the zone makes about it.
ArchAsks.PillarKinds( panel, ArchAsks.PillarTypes(), cut.ExtrudePillarType, type => {
cut.ExtrudePillarType = type.Name;
changed?.Invoke();
refresh?.Invoke();
} );
return;
}
if ( ArchExtrude.Coursed( cut.Extrude ) ) {
block.Add( ArchPartUi.Number( "Course height", cut.ExtrudeCourse, 16f, value => cut.ExtrudeCourse = MathF.Max( 0f, value ), changed ) );
block.Add( ArchPartUi.Number( "Joint", cut.ExtrudeJoint, 0f, value => cut.ExtrudeJoint = MathF.Max( 0f, value ), changed ) );
}
}
static void Footing( ArchIconGrid grid, ArchCutPart cut, PillarFooting? founding, string tooltip, string slug, string glyph, Action refresh, Action changed ) {
grid.Pick( tooltip, slug, glyph, cut.ExtrudeFooting == founding, () => {
cut.ExtrudeFooting = founding;
changed?.Invoke();
refresh?.Invoke();
} );
}
static void Extrude( ArchIconGrid grid, ArchCutPart cut, ArchExtrudeKind kind, Action refresh, Action changed ) {
grid.Pick( ArchExtrude.Label( kind ), $"extrude_{kind.ToString().ToLowerInvariant()}", ArchExtrude.Glyph( kind ), cut.Extrude == kind, () => {
cut.Extrude = kind;
cut.Affects = ArchExtrude.Targets( kind );
changed?.Invoke();
refresh?.Invoke();
} );
}
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(); } );
}
var walled = cut.Enclosure == CutEnclosure.Walls;
if ( walled ) {
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 ) );
// The band's width IS the shell's thickness, so a walled shaft has already been asked for it - two
// controls on one field is the panel telling you they are two numbers.
if ( !walled ) {
coping.Add( ArchPartUi.Number( "Band 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 ) );
}
}