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 ) );
	}
}