Editor/Carve/ArchDamage.cs

Editor-side static utility for computing architectural damage and carving geometry. It resolves damage targets, generates carve volumes and meshes for masonry, panels, pillars and wall corners, computes masonry courses, noise-based variations, clipping and convex hulls used when rendering damaged building parts in the editor.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchDamage
{
	public enum MasonryCourse
	{
		None,
		Header,
		Double
	}

	const int SelectionSalt = 311;
	public const int StateSalt = 617;
	const int TiltSalt = 947;

	public static ArchCutAffects Targets( ArchDamageKind kind )
	{
		return kind switch
		{
			ArchDamageKind.Masonry => ArchCutAffects.Walls | ArchCutAffects.Pillars | ArchCutAffects.Trims,
			ArchDamageKind.SurfaceSpall => ArchCutAffects.Ceilings,
			ArchDamageKind.MissingPanels or ArchDamageKind.DisplacedPanels or ArchDamageKind.MixedPanels => ArchCutAffects.Ceilings,
			_ => ArchCutAffects.None
		};
	}

	public static IEnumerable<ArchCarveVolume> Resolve( ArchCutPart cut, ArchKit kit )
	{
		foreach ( var leg in cut.Legs() )
		{
			if ( Panels( cut.ResolvedDamage ) )
			{
				foreach ( var panel in PanelVolumes( cut, leg ) )
				{
					yield return panel;
				}

				continue;
			}

			var volume = ArchCut.Bite( cut, leg );

			if ( cut.ResolvedDamage == ArchDamageKind.Masonry )
			{
				var jitter = MathF.Max( kit?.BreakJitter ?? 0f, MathF.Max( 4f, cut.DamageCellWidth * 0.5f ) );
				volume = volume.Breaking( new ArchCarveBreak { Seed = Seed( cut ), Jitter = jitter } );
			}

			yield return volume;
		}
	}

	public static bool Panels( ArchDamageKind kind )
	{
		return kind is ArchDamageKind.MissingPanels or ArchDamageKind.DisplacedPanels or ArchDamageKind.MixedPanels;
	}

	public static bool Displaced( ArchCutPart cut, ArchCarveVolume volume )
	{
		if ( cut.ResolvedDamage == ArchDamageKind.DisplacedPanels )
		{
			return true;
		}

		if ( cut.ResolvedDamage != ArchDamageKind.MixedPanels )
		{
			return false;
		}

		return Noise( cut, volume, StateSalt ) >= 0.5f;
	}

	public static bool BakeToTarget( ArchPlan plan, ArchKit kit, ArchCutPart cut )
	{
		if ( plan?.OwnerOf( cut ) is not { } building || cut.ResolvedDamage != ArchDamageKind.Masonry )
		{
			return false;
		}

		var edge = MathF.Max( 2f, cut.DamageCellWidth );
		var half = edge * 0.5f;
		var targets = new List<(Vector2 Point, List<Vector2> Loop, float Bottom, float Top)>();

		foreach ( var room in building.Rooms.Where( room => room.Floor == cut.Level ) )
		{
			foreach ( var wall in room.Walls )
			{
				var top = room.BaseHeight + ArchWallSection.Height( wall, room, kit );

				foreach ( var point in new[] { wall.Start, wall.End } )
				{
					targets.Add( (point,
						ArchFootprint.Rect( point - new Vector2( half, half ), point + new Vector2( half, half ) ),
						room.BaseHeight, top) );
				}
			}

			foreach ( var pillar in room.Pillars.Where( pillar => pillar.Placement != PillarPlacement.Pilaster ) )
			{
				foreach ( var position in ArchAsks.PillarPositions( pillar ) )
				{
					var point = new Vector2( position.x, position.y );
					targets.Add( (point,
						ArchFootprint.Rect( point - pillar.Half, point + pillar.Half ),
						position.z, position.z + ArchAsks.PillarHeight( pillar, room, kit, plan )) );
				}
			}
		}

		var baked = false;

		foreach ( var segment in cut.Legs().ToList() )
		{
			var outline = segment.Outline();
			var centre = outline.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / outline.Count;
			var target = targets
				.Where( entry => ArchFootprint.Overlaps( outline, entry.Loop ) )
				.OrderBy( entry => (entry.Point - centre).LengthSquared )
				.FirstOrDefault();

			if ( target.Loop is null )
			{
				continue;
			}

			segment.Start = target.Loop[0];
			segment.End = target.Loop[2];
			segment.Width = MathF.Max( target.Loop.Max( point => point.x ) - target.Loop.Min( point => point.x ),
				target.Loop.Max( point => point.y ) - target.Loop.Min( point => point.y ) );
			segment.BaseHeight = target.Bottom;
			segment.TopHeight = target.Top;
			segment.Loop = target.Loop;
			baked = true;
		}

		if ( baked )
		{
			cut.Profile = CutProfile.Poly;
		}

		return baked;
	}

	public static void WallCorners(
		ArchMesh canvas,
		ArchPlan plan,
		ArchKit kit,
		ArchBuilding building,
		ArchRoom room,
		ArchWall wall,
		float edgeFrom,
		float edgeTo,
		float height,
		float thickness,
		ArchStyle style,
		ArchConnectionService connections = null )
	{
		connections ??= new ArchConnectionService( building, kit );

		foreach ( var (cut, volume) in ArchCut.Damage( plan, room.Floor, kit, room.BaseHeight, room.BaseHeight + height, building.Id, ArchCutAffects.Walls, wall.Id ) )
		{
			if ( cut.ResolvedDamage != ArchDamageKind.Masonry )
			{
				continue;
			}

			var brush = style.Brush( ArchSurface.WallBase,
				new[] { cut.Palette, wall.Palette, room.Palette, building.Palette } );

			foreach ( var (point, along, direction) in new[]
			{
				(wall.Start, edgeFrom, -1f),
				(wall.End, edgeTo, 1f)
			} )
			{
				var adjacent = room.Walls
					.Where( candidate => candidate.Id != wall.Id && candidate.Length > 1f )
					.Where( candidate => (candidate.Start - point).Length < 0.1f || (candidate.End - point).Length < 0.1f )
					.Where( candidate => MathF.Abs( Vector2.Dot( candidate.Direction, wall.Direction ) ) < 0.99f )
					.OrderBy( candidate => candidate.Id )
					.FirstOrDefault();

				if ( adjacent is not null && wall.Id > adjacent.Id )
				{
					continue;
				}

				var reach = MathF.Max( 2f, cut.DamageCellWidth );
				var corner = ArchFootprint.Rect( point - new Vector2( reach, reach ), point + new Vector2( reach, reach ) );

				if ( !ArchFootprint.Overlaps( volume.Footprint, corner ) )
				{
					continue;
				}

				var bottom = Math.Clamp( volume.Floor.At( point ) - room.BaseHeight, 0f, height );
				var top = Math.Clamp( volume.Ceiling.At( point ) - room.BaseHeight, 0f, height );

				if ( adjacent is null )
				{
					BrickCorner( canvas, cut, wall.Id, along, direction, bottom, top, thickness, brush );
					continue;
				}

				var adjacentThickness = ArchWallSection.Thickness( adjacent, kit );
				var adjacentJoin = connections.Wall( adjacent, room ).WithThickness( adjacentThickness ).Create();
				var adjacentAtStart = (adjacent.Start - point).Length < 0.1f;
				var adjacentAlong = adjacentAtStart ? adjacentJoin.StartExtend : adjacentJoin.EndExtend;
				var adjacentDirection = adjacentAtStart ? -1f : 1f;

				JoinedBrickCorner( canvas, cut, wall, along, direction, thickness,
					adjacent, adjacentAlong, adjacentDirection, adjacentThickness, bottom, top, brush );
			}
		}
	}

	public static void PillarCorners(
		ArchMesh canvas,
		ArchPlan plan,
		ArchKit kit,
		ArchBuilding building,
		ArchRoom room,
		ArchPillarPart part,
		Vector3 origin,
		float height,
		ArchStyle style )
	{
		if ( part.Sides > 4 )
		{
			return;
		}

		var half = part.Half;
		var footprint = ArchFootprint.Rect( new Vector2( origin.x, origin.y ) - half, new Vector2( origin.x, origin.y ) + half );

		foreach ( var (cut, volume) in ArchCut.Damage( plan, room.Floor, kit, origin.z, origin.z + height, building.Id, ArchCutAffects.Pillars, part.Id ) )
		{
			if ( cut.ResolvedDamage != ArchDamageKind.Masonry || !ArchFootprint.Overlaps( volume.Footprint, footprint ) )
			{
				continue;
			}

			var brush = style.Brush( ArchSurface.WallBase,
				new[] { cut.Palette, part.Palette, room.Palette, building.Palette } );

			var bottom = Math.Clamp( volume.Floor.At( new Vector2( origin.x, origin.y ) ), origin.z, origin.z + height );
			var top = Math.Clamp( volume.Ceiling.At( new Vector2( origin.x, origin.y ) ), origin.z, origin.z + height );
			var width = MathF.Max( 2f, cut.DamageCellWidth );
			var course = MathF.Max( 2f, cut.DamageCellLength );
			var proud = MathF.Max( 0.5f, cut.DamageDepth );
			var socket = MathF.Min( width * 0.25f, MathF.Min( half.x, half.y ) * 0.5f );
			var joint = MathF.Min( 0.375f, course * 0.1f );
			var first = (int)MathF.Floor( bottom / course );
			var last = (int)MathF.Ceiling( top / course );

			for ( var z = first; z < last; z++ )
			{
				var state = CourseAt( cut, z, 1f );

				if ( state == MasonryCourse.None || !KeepsCourse( cut, part.Id, z ) )
				{
					continue;
				}

				var low = MathF.Max( bottom, z * course ) + joint;
				var high = MathF.Min( top, (z + 1) * course ) - joint;
				var reachNoise = BrickVariation( cut, part.Id, z );
				var stateScale = state == MasonryCourse.Double ? 1f : 0.45f;
				var reachX = MathF.Min( half.x, width * stateScale * (0.75f + reachNoise * 0.25f) );
				var reachY = MathF.Min( half.y, width * stateScale * (0.75f + reachNoise * 0.25f) );
				var runsAlongX = (z & 1) == 0;

				foreach ( var (corner, signX, signY) in new[]
				{
					(new Vector2( origin.x - half.x, origin.y - half.y ), -1f, -1f),
					(new Vector2( origin.x + half.x, origin.y - half.y ), 1f, -1f),
					(new Vector2( origin.x + half.x, origin.y + half.y ), 1f, 1f),
					(new Vector2( origin.x - half.x, origin.y + half.y ), -1f, 1f)
				} )
				{
					if ( !volume.Covers( corner ) )
					{
						continue;
					}

					if ( runsAlongX )
					{
						canvas.Box(
							new Vector3( corner.x - signX * reachX, corner.y - signY * socket, low ),
							new Vector3( corner.x + signX * socket, corner.y + signY * proud, high ),
							brush,
							tangent: Vector3.Forward );

						continue;
					}

					canvas.Box(
						new Vector3( corner.x - signX * socket, corner.y - signY * reachY, low ),
						new Vector3( corner.x + signX * proud, corner.y + signY * socket, high ),
						brush,
						tangent: Vector3.Forward );
				}
			}
		}
	}

	public static IEnumerable<(List<Vector3> Points, bool Closed)> Surviving(
		ArchPlan plan,
		ArchKit kit,
		int level,
		int hostId,
		IReadOnlyList<Vector3> path )
	{
		var runs = new List<List<Vector3>>();

		for ( var index = 0; index + 1 < (path?.Count ?? 0); index++ )
		{
			var from = path[index];
			var to = path[index + 1];
			var flatFrom = new Vector2( from.x, from.y );
			var flatTo = new Vector2( to.x, to.y );
			var blocked = ArchCut.Damage( plan, level, kit, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), hostId, ArchCutAffects.Trims )
				.Where( found => found.Cut.ResolvedDamage == ArchDamageKind.Masonry )
				.SelectMany( found => ArchFootprint.Inside( found.Volume.Footprint, flatFrom, flatTo ) )
				.ToList();
			var marks = blocked.SelectMany( range => new[] { Math.Clamp( range.From, 0f, 1f ), Math.Clamp( range.To, 0f, 1f ) } )
				.Append( 0f )
				.Append( 1f )
				.Distinct()
				.OrderBy( value => value )
				.ToList();

			for ( var mark = 0; mark + 1 < marks.Count; mark++ )
			{
				var start = marks[mark];
				var finish = marks[mark + 1];
				var middle = (start + finish) * 0.5f;

				if ( finish - start < 0.001f || blocked.Any( range => middle > range.From && middle < range.To ) )
				{
					continue;
				}

				var first = Vector3.Lerp( from, to, start );
				var last = Vector3.Lerp( from, to, finish );
				var run = runs.LastOrDefault();

				if ( run is null || (run[^1] - first).Length > 0.05f )
				{
					run = new List<Vector3> { first };
					runs.Add( run );
				}

				run.Add( last );
			}
		}

		foreach ( var run in runs.Where( run => run is { Count: >= 2 } ) )
		{
			var closed = ArchRunPath.IsClosed( run );

			yield return (closed ? run.Take( run.Count - 1 ).ToList() : run, closed);
		}
	}

	public static void DisplacedPanel( ArchMesh canvas, ArchCutPart cut, ArchCarveVolume volume, float soffit, ArchBrush brush )
	{
		var loop = volume.Footprint.ToList();

		if ( loop.Count < 3 )
		{
			return;
		}

		var centre = loop.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / loop.Count;
		var pitch = (Noise( cut, volume, TiltSalt ) * 2f - 1f) * MathF.Max( 0f, cut.DamageTilt );
		var roll = (Noise( cut, volume, TiltSalt + 1 ) * 2f - 1f) * MathF.Max( 0f, cut.DamageTilt );
		var rotation = Rotation.From( pitch, 0f, roll );
		var top = soffit - MathF.Max( 0.5f, cut.DamageDrop );
		var thickness = MathF.Max( 0.35f, MathF.Min( 1f, cut.DamageDrop * 0.2f ) );
		var lower = new List<Vector3>();
		var upper = new List<Vector3>();

		foreach ( var point in loop )
		{
			var local = new Vector3( point.x - centre.x, point.y - centre.y, 0f );
			var turned = rotation * local;

			lower.Add( new Vector3( centre.x + turned.x, centre.y + turned.y, top + turned.z - thickness ) );
			upper.Add( new Vector3( centre.x + turned.x, centre.y + turned.y, top + turned.z ) );
		}

		canvas.Prism( lower, upper, brush );
	}

	static void BrickCorner( ArchMesh canvas, ArchCutPart cut, int wallId, float along, float direction, float bottom, float top, float thickness, ArchBrush brush )
	{
		var width = MathF.Max( 2f, cut.DamageCellWidth );
		var course = MathF.Max( 2f, cut.DamageCellLength );
		var half = thickness * 0.5f;
		var recess = MathF.Min( MathF.Max( 0.25f, cut.DamageDepth ), MathF.Max( 0.25f, thickness - 0.5f ) );
		var socket = MathF.Min( 1f, course * 0.2f );
		var joint = MathF.Min( 0.375f, course * 0.1f );
		var extrusion = MathF.Min( 1.5f, MathF.Max( 0.5f, recess * 0.25f ) );
		var back = MathF.Min( 0f, -half + recess );
		var first = (int)MathF.Floor( bottom / course );
		var last = (int)MathF.Ceiling( top / course );

		for ( var z = first; z < last; z++ )
		{
			var state = CourseAt( cut, z, direction );

			if ( state == MasonryCourse.None || !KeepsCourse( cut, wallId, z ) )
			{
				continue;
			}

			var low = MathF.Max( bottom, z * course ) + joint;
			var high = MathF.Min( top, (z + 1) * course ) - joint;
			var into = direction < 0f ? 1f : -1f;
			var variation = BrickVariation( cut, wallId, z );
			var lengthScale = 0.88f + variation * 0.16f;
			var courseSocket = socket * (0.8f + variation * 0.4f);
			var front = -half - extrusion * (0.8f + variation * 0.35f);

			if ( state == MasonryCourse.Header )
			{
				var header = MathF.Max( course * 1.25f, width * 0.35f ) * lengthScale;

				Brick( canvas, along, into, -courseSocket, header, front, back, low, high, brush );

				continue;
			}

			var primaryLength = width * lengthScale;

			Brick( canvas, along, into, -courseSocket, primaryLength, front, back, low, high, brush );
		}
	}

	static void JoinedBrickCorner(
		ArchMesh canvas,
		ArchCutPart cut,
		ArchWall owner,
		float ownerAlong,
		float ownerDirection,
		float ownerThickness,
		ArchWall adjacent,
		float adjacentAlong,
		float adjacentDirection,
		float adjacentThickness,
		float bottom,
		float top,
		ArchBrush brush )
	{
		var course = MathF.Max( 2f, cut.DamageCellLength );
		var joint = MathF.Min( 0.375f, course * 0.1f );
		var first = (int)MathF.Floor( bottom / course );
		var last = (int)MathF.Ceiling( top / course );
		var hostId = Math.Min( owner.Id, adjacent.Id );

		for ( var z = first; z < last; z++ )
		{
			if ( !KeepsCourse( cut, hostId, z ) )
			{
				continue;
			}

			var low = MathF.Max( bottom, z * course ) + joint;
			var high = MathF.Min( top, (z + 1) * course ) - joint;
			var loops = new List<IReadOnlyList<Vector2>>();

			AddJoinedBrickLoop( loops, cut, owner, owner, ownerAlong, ownerDirection, ownerThickness, z );
			AddJoinedBrickLoop( loops, cut, owner, adjacent, adjacentAlong, adjacentDirection, adjacentThickness, z );

			var loop = ConvexHull( ArchFootprint.Union( loops ).SelectMany( region => region ) );

			if ( loop.Count >= 3 )
			{
				CornerPrism( canvas, loop, low, high, brush );
			}
		}
	}

	static void CornerPrism( ArchMesh canvas, IReadOnlyList<Vector2> loop, float low, float high, ArchBrush brush )
	{
		var bottom = loop.Select( point => new Vector3( point.x, point.y, low ) ).ToList();
		var top = loop.Select( point => new Vector3( point.x, point.y, high ) ).ToList();

		if ( loop.Count != 6 )
		{
			canvas.Prism( bottom, top, brush, tangent: Vector3.Forward );
			return;
		}

		using var solid = canvas.Solid( ArchSolid.Hull( bottom.Concat( top ).ToList() ) );

		for ( var index = 0; index < loop.Count; index++ )
		{
			var next = (index + 1) % loop.Count;
			canvas.Ribbon( bottom[index], bottom[next], top[next], top[index], Vector3.Forward, brush );
		}

		canvas.Ribbon( bottom[3], bottom[2], bottom[1], bottom[0], Vector3.Forward, brush );
		canvas.Ribbon( bottom[5], bottom[4], bottom[3], bottom[0], Vector3.Forward, brush );
		canvas.Ribbon( top[0], top[1], top[2], top[3], Vector3.Forward, brush );
		canvas.Ribbon( top[0], top[3], top[4], top[5], Vector3.Forward, brush );
	}

	static List<Vector2> ConvexHull( IEnumerable<Vector2> points )
	{
		var ordered = points.Distinct().OrderBy( point => point.x ).ThenBy( point => point.y ).ToList();

		if ( ordered.Count <= 3 )
		{
			return ordered;
		}

		var lower = new List<Vector2>();

		foreach ( var point in ordered )
		{
			while ( lower.Count >= 2 && Turn( lower[^2], lower[^1], point ) <= 0f )
			{
				lower.RemoveAt( lower.Count - 1 );
			}

			lower.Add( point );
		}

		var upper = new List<Vector2>();

		for ( var index = ordered.Count - 1; index >= 0; index-- )
		{
			var point = ordered[index];

			while ( upper.Count >= 2 && Turn( upper[^2], upper[^1], point ) <= 0f )
			{
				upper.RemoveAt( upper.Count - 1 );
			}

			upper.Add( point );
		}

		lower.RemoveAt( lower.Count - 1 );
		upper.RemoveAt( upper.Count - 1 );
		lower.AddRange( upper );

		return lower;
	}

	static float Turn( Vector2 from, Vector2 through, Vector2 to )
	{
		return (through.x - from.x) * (to.y - from.y) - (through.y - from.y) * (to.x - from.x);
	}

	static void AddJoinedBrickLoop(
		List<IReadOnlyList<Vector2>> loops,
		ArchCutPart cut,
		ArchWall owner,
		ArchWall leg,
		float along,
		float direction,
		float thickness,
		int course )
	{
		var state = CourseAt( cut, course, direction );

		if ( state == MasonryCourse.None )
		{
			return;
		}

		var width = MathF.Max( 2f, cut.DamageCellWidth );
		var courseHeight = MathF.Max( 2f, cut.DamageCellLength );
		var half = thickness * 0.5f;
		var recess = MathF.Min( MathF.Max( 0.25f, cut.DamageDepth ), MathF.Max( 0.25f, thickness - 0.5f ) );
		var extrusion = MathF.Min( 1.5f, MathF.Max( 0.5f, recess * 0.25f ) );
		var variation = BrickVariation( cut, leg.Id, course );
		var lengthScale = 0.88f + variation * 0.16f;
		var socket = MathF.Min( 1f, courseHeight * 0.2f ) * (0.8f + variation * 0.4f);
		var length = state == MasonryCourse.Header
			? MathF.Max( courseHeight * 1.25f, width * 0.35f ) * lengthScale
			: width * lengthScale;
		var front = -half - extrusion * (0.8f + variation * 0.35f);
		var back = MathF.Min( 0f, -half + recess );
		var anchor = leg.Start + leg.Direction * along;
		var into = leg.Direction * (direction < 0f ? 1f : -1f);
		var world = new[]
		{
			anchor - into * socket + leg.Normal * front,
			anchor + into * length + leg.Normal * front,
			anchor + into * length + leg.Normal * back,
			anchor - into * socket + leg.Normal * back
		};

		loops.Add( world.Select( point =>
		{
			var delta = point - owner.Start;
			return new Vector2( Vector2.Dot( delta, owner.Direction ), Vector2.Dot( delta, owner.Normal ) );
		} ).ToList() );
	}

	static void Brick( ArchMesh canvas, float edge, float into, float from, float to, float front, float back, float low, float high, ArchBrush brush )
	{
		canvas.Box(
			new Vector3( edge + into * from, front, low ),
			new Vector3( edge + into * to, back, high ),
			brush,
			tangent: Vector3.Forward );
	}

	public static MasonryCourse CourseAt( ArchCutPart cut, int course, float direction )
	{
		var oppositeFace = direction > 0f;
		var phase = cut.MasonryPattern switch
		{
			ArchMasonryPattern.BrickPier => ((course % 2) + 2) % 2,
			ArchMasonryPattern.WornCorner => ((course % 4) + 4) % 4,
			_ => ((course % 6) + 6) % 6
		};

		return cut.MasonryPattern switch
		{
			ArchMasonryPattern.BrickPier => Interlocked( phase == 0, oppositeFace ),
			ArchMasonryPattern.WornCorner => phase == 3 ? MasonryCourse.None : Interlocked( phase % 2 == 0, oppositeFace ),
			_ => phase is 2 or 4 ? MasonryCourse.None : Interlocked( phase is 0 or 3, oppositeFace )
		};
	}

	static float BrickVariation( ArchCutPart cut, int hostId, int course )
	{
		return ArchBarrierShape.Noise( Seed( cut ), hostId ^ course * 83492791, StateSalt ) < 0.5f ? 0.25f : 0.75f;
	}

	static MasonryCourse Interlocked( bool stretcherOnFirstFace, bool oppositeFace )
	{
		return stretcherOnFirstFace != oppositeFace ? MasonryCourse.Double : MasonryCourse.Header;
	}

	public static bool KeepsCourse( ArchCutPart cut, int hostId, int course )
	{
		return ArchBarrierShape.Noise( Seed( cut ), hostId ^ course * 19349663, SelectionSalt ) <= Math.Clamp( cut.DamageAmount, 0f, 1f );
	}

	static IEnumerable<ArchCarveVolume> PanelVolumes( ArchCutPart cut, ArchCutSegment leg )
	{
		var footprint = leg.Outline();
		var width = MathF.Max( 4f, cut.DamageCellWidth );
		var length = MathF.Max( 4f, cut.DamageCellLength );
		var amount = Math.Clamp( cut.DamageAmount, 0f, 1f );

		ArchFootprint.Bounds( footprint, out var min, out var max );

		var fromX = (int)MathF.Floor( min.x / width );
		var toX = (int)MathF.Ceiling( max.x / width );
		var fromY = (int)MathF.Floor( min.y / length );
		var toY = (int)MathF.Ceiling( max.y / length );

		for ( var x = fromX; x < toX; x++ )
		{
			for ( var y = fromY; y < toY; y++ )
			{
				var cellMin = new Vector2( MathF.Max( min.x, x * width ), MathF.Max( min.y, y * length ) );
				var cellMax = new Vector2( MathF.Min( max.x, (x + 1) * width ), MathF.Min( max.y, (y + 1) * length ) );
				var cell = Clip( ArchFootprint.Rect( cellMin, cellMax ), footprint );

				if ( cell.Count < 3 || MathF.Abs( ArchFootprint.SignedArea( cell ) ) < 4f )
				{
					continue;
				}

				var index = x * 73856093 ^ y * 19349663;

				if ( ArchBarrierShape.Noise( Seed( cut ), index, SelectionSalt ) > amount )
				{
					continue;
				}

				yield return ArchCarveVolume.Over( cell, leg.BaseHeight, leg.TopHeight );
			}
		}
	}

	static List<Vector2> Clip( IReadOnlyList<Vector2> subject, IReadOnlyList<Vector2> boundary )
	{
		var clipped = subject.ToList();
		var winding = ArchFootprint.SignedArea( boundary ) >= 0f ? 1f : -1f;

		for ( var edge = 0; edge < boundary.Count && clipped.Count > 0; edge++ )
		{
			var from = boundary[edge];
			var to = boundary[(edge + 1) % boundary.Count];
			var input = clipped;
			clipped = new List<Vector2>();
			var previous = input[^1];
			var previousInside = Side( from, to, previous ) * winding >= -0.01f;

			foreach ( var current in input )
			{
				var currentInside = Side( from, to, current ) * winding >= -0.01f;

				if ( currentInside != previousInside )
				{
					clipped.Add( Intersection( previous, current, from, to ) );
				}

				if ( currentInside )
				{
					clipped.Add( current );
				}

				previous = current;
				previousInside = currentInside;
			}
		}

		return ArchFootprint.Wind( clipped );
	}

	static Vector2 Intersection( Vector2 a, Vector2 b, Vector2 c, Vector2 d )
	{
		var ab = b - a;
		var cd = d - c;
		var divisor = ab.x * cd.y - ab.y * cd.x;

		if ( MathF.Abs( divisor ) < 0.0001f )
		{
			return b;
		}

		var offset = c - a;
		var along = (offset.x * cd.y - offset.y * cd.x) / divisor;

		return a + ab * along;
	}

	static float Side( Vector2 a, Vector2 b, Vector2 point )
	{
		return (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x);
	}

	static float Noise( ArchCutPart cut, ArchCarveVolume volume, int salt )
	{
		var centre = volume.Footprint.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / volume.Footprint.Count;
		var index = (int)MathF.Round( centre.x * 17f ) ^ (int)MathF.Round( centre.y * 31f );

		return ArchBarrierShape.Noise( Seed( cut ), index, salt );
	}

	public static int Seed( ArchCutPart cut )
	{
		return cut.Id == 0 ? 1 : cut.Id;
	}
}