editor/AdvancedMappingTools/Geometry/WallEditor.cs

Editor tool for drawing walls along a path. Builds connected or single-piece walls with mitred or round joints, flat, battlement, ruined or capped tops, buttresses, base courses and single-face mode, and hands plain walls to the connection engine.

namespace AdvancedMappingTools;
public enum WallPlacement
{
	[Title( "Single Pieces" )] Pieces,
	[Title( "Connected Path" )] Connected
}

public enum WallMode
{
	Solid,
	[Title( "Single Face" )] SingleFace
}

public enum WallAlignment
{
	Center,
	Inside,
	Outside
}

public enum WallFaceSide
{
	Outside,
	Inside
}

public enum WallTop
{
	Flat,
	Battlements,
	Ruined,
	[Title( "Coping Slab" )] Coping,
	[Title( "Pitched Coping" )] Pitched,
	[Title( "Rounded Cap" )] Rounded,
	[Title( "Stepped Coping" )] Stepped
}

public enum WallJoint
{
	Miter,
	Round,
	None
}

/// <summary>
/// Draw walls as one connected object with corners, curves and closed loops,
/// or as single pieces where every click finishes a wall and the next starts at its end.
/// </summary>
[ToolCategory( "Walls & Floors" )]
[Title( "Wall" ), Icon( "view_week" )]
public sealed partial class WallEditor( PrimitiveTool tool ) : PathEditor( tool )
{
	const float CornerAngle = 25.0f;

	readonly ConnectedWallGenerator _pieces = new();

	Vector3? _lastDirection;

	protected override int MinPoints => 2;
	protected override bool CanClose => Placement == WallPlacement.Connected;
	protected override bool PlacesPiecesInChain => Placement == WallPlacement.Pieces;
	protected override bool PlacesObjects => UsesConnections;

	/// <summary>
	/// Plain single pieces connect through the connection engine. Pieces with a shaped top,
	/// buttresses, a round post or only one face are made the old way, those are not parts yet.
	/// </summary>
	bool Plain => Mode == WallMode.Solid && Joints != WallJoint.Round
		&& TopStyle == WallTop.Flat && ButtressSpacing <= 0.0f && SegmentLength <= 0.0f && GroundOffset == 0.0f;

	bool UsesConnections => Plain && (Placement == WallPlacement.Pieces || ConnectedParts);

	protected override bool SnapsToLines => true;

	ConnectedWallSettings PieceSettings() => new()
	{
		Path = Curve(),
		OpeningGrid = OpeningGrid,
		Openings = OpeningGrid ? OpeningColumns : 0,
		OpeningRows = OpeningRows,
		OpeningRowSpacing = OpeningRowSpacing,
		AutomaticOpeningKind = OpeningKind,
		OpeningWidth = GridOpeningWidth,
		OpeningHeight = GridOpeningHeight,
		SillHeight = GridSill,
		Plinth = BaseCourse > 0,
		PlinthHeight = BaseCourse,
		PlinthProjection = BaseOverhang,
		Height = Height,
		Thickness = Thickness,
		SeparateWallMaterials = SeparateWallMaterials,
		InteriorMaterial = PartTool.Pick( InteriorSurface?.ResourcePath, SurfaceMaterial ),
		ExteriorMaterial = PartTool.Pick( ExteriorSurface?.ResourcePath, SurfaceMaterial ),
		UseOpenings = UseWallOpenings,
		Alignment = Alignment,
		Join = AdvancedCorners ? CornerProfile : Joints == WallJoint.None ? WallJoinStyle.Overlap : WallJoinStyle.Miter,
		Material = PartTool.Pick( null, SurfaceMaterial ),
		WindowMaterial = PartTool.Pick( null, TrimMaterial ),
		GlassMaterial = "materials/glass/glass_a.vmat",
		Variation = 0.0f
	};

	protected override PolygonMesh BuildPreview() => PartService.Plan( _pieces, PieceSettings() ).Flatten( new Transform( Vector3.Zero, Rotation.Identity ) );

	protected override void PlaceObjects()
	{
		var settings = PieceSettings();

		settings.Connected = ConnectFinished;
		PartTool.PlaceWall( _pieces, settings );
	}

	protected override IEnumerable<string> Hints => Placement == WallPlacement.Pieces
		? ["Each click: finish a wall piece", "Esc: end the chain"]
		: [.. base.Hints, "Click the green point: close loop"];

	[Property, Group( "Wall" ), WideMode, Description( "Single Pieces makes one object per click, Connected Path makes one object for the whole line." )]
	public WallPlacement Placement { get; set; } = WallPlacement.Connected;

	[Property, Group( "Wall" ), Title( "Use Connections" ), WideMode, Description( "Builds plain walls through the connection engine, also a connected path. Fancy tops, buttresses and single faces stay on the old path." )]
	public bool ConnectedParts { get; set; } = true;

	[Property, Group( "Wall" ), Title( "Joints" ), WideMode, Description( "How single pieces meet at a corner." )]
	public WallJoint Joints { get; set; } = WallJoint.Miter;

	[Property, Group( "Wall" ), WideMode]
	public WallMode Mode { get; set; } = WallMode.Solid;

	[Property, Group( "Wall" ), WideMode, Description( "Where the wall sits relative to the path. Inside is left of the drawing direction, or inside a closed loop." )]
	public WallAlignment Alignment { get; set; } = WallAlignment.Center;

	[Property, ShowIf( nameof( Mode ), WallMode.SingleFace ), Group( "Wall" ), Title( "Face Side" ), WideMode, Description( "Which way a single face looks." )]
	public WallFaceSide FaceSide { get; set; } = WallFaceSide.Outside;

	[Property, Group( "Wall" ), Range( 1, 4096, slider: false ), WideMode]
	public float Height { get; set; } = 128.0f;

	[Property, Group( "Wall" ), Range( 0.25f, 512, slider: false ), WideMode]
	public float Thickness { get; set; } = 8.0f;

	[Property, Advanced, Group( "Wall" ), Title( "Ground Offset" ), Range( -1024, 1024, slider: false ), WideMode]
	public float GroundOffset { get; set; } = 0.0f;

	[Property, Advanced, Group( "Wall" ), WideMode]
	public bool Top { get; set; } = true;

	[Property, Advanced, Group( "Wall" ), WideMode]
	public bool Bottom { get; set; } = true;

	[Property, Advanced, Group( "Wall" ), Title( "End Caps" ), WideMode, Description( "Closes the two ends of an open wall." )]
	public bool EndCaps { get; set; } = true;

	[Property, Advanced, Group( "Wall" ), Title( "Segment Length" ), Range( 0, 1024, slider: false ), WideMode, Description( "Cuts long stretches into faces of this length. Zero keeps them whole." )]
	public float SegmentLength { get; set; } = 0.0f;

	[Property, Group( "Top Detail" ), Title( "Top Style" ), WideMode]
	public WallTop TopStyle { get; set; } = WallTop.Flat;

	[Property, ShowIf( nameof( TopStyle ), WallTop.Battlements ), Group( "Top Detail" ), Title( "Merlon Width" ), Range( 2, 256, slider: false ), WideMode]
	public float MerlonWidth { get; set; } = 16.0f;

	[Property, ShowIf( nameof( TopStyle ), WallTop.Battlements ), Group( "Top Detail" ), Title( "Merlon Height" ), Range( 1, 256, slider: false ), WideMode]
	public float MerlonHeight { get; set; } = 16.0f;

	[Property, ShowIf( nameof( TopStyle ), WallTop.Ruined ), Group( "Top Detail" ), Title( "Damage" ), Range( 0, 1 ), WideMode, Description( "How much of a ruined wall is broken away." )]
	public float Damage { get; set; } = 0.4f;

	[Property, ShowIf( nameof( TopStyle ), WallTop.Ruined ), Group( "Top Detail" ), Title( "Break Size" ), Range( 2, 256, slider: false ), WideMode]
	public float BreakSize { get; set; } = 16.0f;

	[Property, ShowIf( nameof( TopStyle ), WallTop.Ruined ), Group( "Top Detail" ), Range( 0, 9999, slider: false ), WideMode]
	public int Seed { get; set; } = 1;

	[Property, Group( "Top Detail" ), Title( "Cap Height" ), Range( 1, 128, slider: false ), WideMode, Description( "Height of a coping or cap on the wall top." )]
	public float CapHeight { get; set; } = 6.0f;

	[Property, Group( "Top Detail" ), Title( "Cap Overhang" ), Range( 0, 64, slider: false ), WideMode, Description( "How far the cap reaches past both faces of the wall." )]
	public float CapOverhang { get; set; } = 2.0f;

	bool HasCap => TopStyle is WallTop.Coping or WallTop.Pitched or WallTop.Rounded or WallTop.Stepped;

	[Property, Group( "Buttresses" ), Title( "Buttress Spacing" ), Range( 0, 2048, slider: false ), WideMode, Description( "Supports on the outer side, this far apart. Zero turns them off." )]
	public float ButtressSpacing { get; set; } = 0.0f;

	[Property, HideIf( nameof( ButtressSpacing ), 0.0f ), Group( "Buttresses" ), Title( "Buttress Width" ), Range( 1, 256, slider: false ), WideMode]
	public float ButtressWidth { get; set; } = 16.0f;

	[Property, HideIf( nameof( ButtressSpacing ), 0.0f ), Group( "Buttresses" ), Title( "Buttress Depth" ), Range( 1, 256, slider: false ), WideMode]
	public float ButtressDepth { get; set; } = 16.0f;

	[Property, Group( "Base" ), Title( "Base Course" ), Range( 0, 256, slider: false ), WideMode, Description( "A projecting plinth at the foot of the wall. Zero turns it off." )]
	public float BaseCourse { get; set; } = 0.0f;

	[Property, HideIf( nameof( BaseCourse ), 0.0f ), Group( "Base" ), Title( "Base Overhang" ), Range( 0, 64, slider: false ), WideMode]
	public float BaseOverhang { get; set; } = 4.0f;

	protected override bool IsShapeValid => SamplePath().Count >= 2;

	protected override void OnBuilt( MeshComponent component )
	{
		var path = SamplePath();

		if ( path.Count >= 2 )
			_lastDirection = (path[^1] - path[^2]).WithZ( 0.0f ).Normal;
	}

	public override void OnCancel()
	{
		_lastDirection = null;
		base.OnCancel();
	}

	protected override void BuildMesh( MeshKit kit )
	{
		var path = SamplePath();
		if ( SegmentLength > 0.0f )
			path = PathFrames.Resample( path, Closed, MathF.Max( SegmentLength, 8.0f ) );
		if ( TopStyle == WallTop.Ruined )
			path = PathFrames.Resample( path, Closed, MathF.Max( BreakSize, 2.0f ) );
		// SamplePath already contains the placement height; applying it to Frame again doubles it.
		// Inside and outside follow the loop, whichever way round it was drawn.
		var side = Closed && SignedArea( path ) < 0.0f ? -1.0f : 1.0f;
		if ( Mode == WallMode.SingleFace )
		{
			BuildSingleFace( kit, path, side );
			return;
		}

		var left = Alignment switch
		{
			WallAlignment.Inside => Thickness,
			WallAlignment.Outside => 0.0f,
			_ => Thickness * 0.5f
		} * side;
		var right = left - Thickness * side;
		var outer = OffsetPath( path, Closed, left );
		var inner = OffsetPath( path, Closed, right );
		var bottom = Vector3.Up * GroundOffset;
		var heights = TopHeights( path.Count );
		var segments = Closed ? path.Count : path.Count - 1;
		Vector3 Up( int index ) => Vector3.Up * (GroundOffset + heights[index]);
		// Flip so faces always wind outward whichever side "outer" ended up on.
		if ( side < 0.0f )
			(outer, inner) = (inner, outer);
		for ( var i = 0; i < segments; i++ )
		{
			var j = (i + 1) % path.Count;
			kit.Material = SeparateWallMaterials ? (InteriorSurface ?? SurfaceMaterial) : SurfaceMaterial;
			kit.Face( outer[j] + bottom, outer[i] + bottom, outer[i] + Up( i ), outer[j] + Up( j ) );
			kit.Material = SeparateWallMaterials ? (ExteriorSurface ?? SurfaceMaterial) : SurfaceMaterial;
			kit.Face( inner[i] + bottom, inner[j] + bottom, inner[j] + Up( j ), inner[i] + Up( i ) );
			kit.Material = TrimMaterial;
			if ( Top )
				kit.Face( inner[i] + Up( i ), inner[j] + Up( j ), outer[j] + Up( j ), outer[i] + Up( i ) );
			if ( Bottom )
				kit.Face( inner[i] + bottom, outer[i] + bottom, outer[j] + bottom, inner[j] + bottom );
		}

		if ( !Closed && EndCaps )
		{
			kit.Material = TrimMaterial;
			kit.Face( outer[0] + bottom, inner[0] + bottom, inner[0] + Up( 0 ), outer[0] + Up( 0 ) );
			kit.Face( inner[^1] + bottom, outer[^1] + bottom, outer[^1] + Up( path.Count - 1 ), inner[^1] + Up( path.Count - 1 ) );
		}

		var merlonTop = GroundOffset + MathF.Max( Height, 1.0f ) + (TopStyle == WallTop.Battlements ? MerlonHeight : 0.0f);
		if ( TopStyle == WallTop.Battlements )
			BuildBattlements( kit, path, outer, inner );
		if ( HasCap )
			BuildCap( kit, path, left, right, heights );
		if ( ButtressSpacing > 0.0f )
			BuildButtresses( kit, path, side, right );
		if ( BaseCourse > 0.0f )
		{
			var courseTop = GroundOffset + BaseCourse;
			var over = MathF.Max( BaseOverhang, 0.0f );
			var plinthOuter = OffsetPath( path, Closed, MathF.Max( left, right ) + over );
			var plinthInner = OffsetPath( path, Closed, MathF.Min( left, right ) - over );
			for ( var i = 0; i < segments; i++ )
			{
				var j = (i + 1) % path.Count;
				kit.Material = SurfaceMaterial;
				kit.Face( plinthOuter[j] + bottom, plinthOuter[i] + bottom, plinthOuter[i] + Vector3.Up * courseTop, plinthOuter[j] + Vector3.Up * courseTop );
				kit.Face( plinthInner[i] + bottom, plinthInner[j] + bottom, plinthInner[j] + Vector3.Up * courseTop, plinthInner[i] + Vector3.Up * courseTop );
				kit.Material = TrimMaterial;
				kit.Face( plinthInner[i] + Vector3.Up * courseTop, plinthInner[j] + Vector3.Up * courseTop, plinthOuter[j] + Vector3.Up * courseTop, plinthOuter[i] + Vector3.Up * courseTop );
			}
		}

		BuildJoint( kit, path, left, right, merlonTop );
	}

	/// <summary>
	/// Closes the outer corner between this piece and the one before it. The square ends
	/// of both pieces meet on the shared point, leaving a wedge on the outside of the turn.
	/// </summary>
	void BuildJoint( MeshKit kit, List<Vector3> path, float left, float right, float top )
	{
		if ( Placement != WallPlacement.Pieces || Joints == WallJoint.None ) return;
		if ( _lastDirection is not Vector3 last || path.Count < 2 ) return;

		var direction = (path[1] - path[0]).WithZ( 0.0f ).Normal;
		var turn = last.x * direction.y - last.y * direction.x;

		if ( MathF.Abs( turn ) < 0.01f ) return;

		var point = path[0];
		kit.Material = SurfaceMaterial;

		if ( Joints == WallJoint.Round )
		{
			kit.Cylinder( point.WithZ( point.z ), Thickness * 0.5f, Thickness * 0.5f, GroundOffset, top, 16 );
			return;
		}

		var distance = turn > 0.0f ? -right : left;
		if ( distance <= 0.0f ) return;

		var sign = turn > 0.0f ? -1.0f : 1.0f;
		var before = new Vector3( -last.y, last.x, 0.0f ) * (distance * sign);
		var after = new Vector3( -direction.y, direction.x, 0.0f ) * (distance * sign);

		kit.Prism( [point, point + before, point + after], point.z + GroundOffset, point.z + top );
	}

	/// <summary>
	/// A cap along the top of the wall, one closed body per segment. Its section runs across the wall
	/// from the inner (0) to the outer (1) edge of the cap and upward, between the mitred corners, so
	/// pieces meet at every corner without a gap.
	/// </summary>
	void BuildCap( MeshKit kit, List<Vector3> path, float left, float right, float[] heights )
	{
		var over = Math.Clamp( CapOverhang, 0.0f, 64.0f );
		var height = Math.Clamp( CapHeight, 1.0f, 128.0f );
		var a = OffsetPath( path, Closed, MathF.Min( left, right ) - over );
		var b = OffsetPath( path, Closed, MathF.Max( left, right ) + over );
		var segments = Closed ? path.Count : path.Count - 1;
		var sections = new List<(float Across, float Up)[]>();

		switch ( TopStyle )
		{
			case WallTop.Pitched:
				sections.Add( [(0, 0), (1, 0), (1, height * .25f), (.5f, height), (0, height * .25f)] );
				break;
			case WallTop.Rounded:
			{
				var arc = new List<(float, float)> { (0, 0), (1, 0) };
				for ( var k = 1; k < 8; k++ )
				{
					var t = MathF.PI * k / 8;
					arc.Add( (.5f + .5f * MathF.Cos( t ), height * MathF.Sin( t )) );
				}

				sections.Add( [.. arc] );
				break;
			}
			case WallTop.Stepped:
			{
				var inset = over > 0 ? Math.Clamp( over / (MathF.Abs( left - right ) + over * 2 ), 0.05f, .4f ) : .15f;
				sections.Add( [(0, 0), (1, 0), (1, height * .4f), (0, height * .4f)] );
				sections.Add( [(inset, height * .4f), (1 - inset, height * .4f), (1 - inset, height), (inset, height)] );
				break;
			}
			default:
				sections.Add( [(0, 0), (1, 0), (1, height), (0, height)] );
				break;
		}

		kit.Material = TrimMaterial;
		for ( var i = 0; i < segments; i++ )
		{
			var j = (i + 1) % path.Count;
			Vector3 At( int index, (float Across, float Up) p ) =>
				Vector3.Lerp( a[index], b[index], p.Across ) + Vector3.Up * (GroundOffset + heights[index] + p.Up);

			foreach ( var section in sections )
			{
				var near = section.Select( p => At( i, p ) ).ToArray();
				var far = section.Select( p => At( j, p ) ).ToArray();
				ConvexBody( kit, near, far );
			}
		}
	}

	/// <summary>
	/// A closed convex body between two matching outlines. Every face is turned away from the middle
	/// of the body, so it comes out right whichever way the outlines run.
	/// </summary>
	static void ConvexBody( MeshKit kit, Vector3[] near, Vector3[] far )
	{
		var middle = (near.Aggregate( Vector3.Zero, ( s, p ) => s + p ) + far.Aggregate( Vector3.Zero, ( s, p ) => s + p )) / (near.Length + far.Length);
		void Face( params Vector3[] ring )
		{
			var normal = Vector3.Zero;
			for ( var k = 0; k < ring.Length; k++ )
				normal += Vector3.Cross( ring[k], ring[(k + 1) % ring.Length] );
			var center = ring.Aggregate( Vector3.Zero, ( s, p ) => s + p ) / ring.Length;
			if ( normal.Dot( center - middle ) < 0 )
				Array.Reverse( ring );
			kit.Face( ring );
		}

		// Its own part: the end faces of two pieces meet exactly at a corner and must not be welded.
		kit.BeginPart();
		Face( [.. near] );
		Face( [.. far] );
		for ( var k = 0; k < near.Length; k++ )
		{
			var n = (k + 1) % near.Length;
			Face( near[k], near[n], far[n], far[k] );
		}
	}

	float[] TopHeights( int count )
	{
		var heights = new float[count];
		var height = MathF.Max( Height, 1.0f );
		var random = new Random( Seed );

		for ( var i = 0; i < count; i++ )
		{
			heights[i] = TopStyle == WallTop.Ruined
				? height * (1.0f - Damage * ((float)random.NextDouble() * 0.7f + (i % 2) * 0.3f))
				: height;
		}

		return heights;
	}

	/// <summary>
	/// Merlons sit directly on the edges of the wall, so they follow curves and two
	/// merlons meeting at a mitred corner share its edge. Every stretch between corners
	/// starts and ends on a merlon.
	/// </summary>
	void BuildBattlements( MeshKit kit, List<Vector3> path, Vector3[] outer, Vector3[] inner )
	{
		var count = path.Count;
		var width = MathF.Max( MerlonWidth, 2.0f );
		var low = path[0].z + GroundOffset + MathF.Max( Height, 1.0f );
		var high = low + MerlonHeight;

		kit.Material = TrimMaterial;

		bool IsCorner( int index )
		{
			if ( !Closed && (index == 0 || index == count - 1) ) return true;

			var a = (path[index] - path[(index + count - 1) % count]).WithZ( 0.0f ).Normal;
			var b = (path[(index + 1) % count] - path[index]).WithZ( 0.0f ).Normal;

			return a.Dot( b ) < MathF.Cos( CornerAngle.DegreeToRadian() );
		}

		var corners = Enumerable.Range( 0, count ).Where( IsCorner ).ToList();
		if ( corners.Count == 0 ) corners.Add( 0 );
		if ( Closed ) corners.Add( corners[0] + count );

		for ( var c = 0; c < corners.Count - 1; c++ )
		{
			var stretch = Enumerable.Range( corners[c], corners[c + 1] - corners[c] + 1 ).Select( i => i % count ).ToList();
			var lengths = new List<float> { 0.0f };

			for ( var i = 0; i < stretch.Count - 1; i++ )
				lengths.Add( lengths[^1] + path[stretch[i]].Distance( path[stretch[i + 1]] ) );

			var total = lengths[^1];
			var merlons = (int)MathF.Max( 1.0f, MathF.Floor( (total + width) / (width * 2.0f) ) );
			var gap = merlons > 1 ? (total - width * merlons) / (merlons - 1) : 0.0f;

			for ( var m = 0; m < merlons; m++ )
			{
				var from = merlons > 1 ? (width + gap) * m : MathF.Max( (total - width) * 0.5f, 0.0f );
				var to = MathF.Min( from + width, total );

				EmitMerlon( kit, stretch, lengths, outer, inner, from, to, low, high );
			}
		}
	}

	static void EmitMerlon( MeshKit kit, List<int> stretch, List<float> lengths, Vector3[] outer, Vector3[] inner, float from, float to, float low, float high )
	{
		(Vector3 Outer, Vector3 Inner) At( float distance )
		{
			for ( var i = 0; i < stretch.Count - 1; i++ )
			{
				if ( distance > lengths[i + 1] && i < stretch.Count - 2 ) continue;

				var part = lengths[i + 1] - lengths[i];
				var t = part > 0.0f ? (distance - lengths[i]) / part : 0.0f;

				return (Vector3.Lerp( outer[stretch[i]], outer[stretch[i + 1]], t ), Vector3.Lerp( inner[stretch[i]], inner[stretch[i + 1]], t ));
			}

			return (outer[stretch[^1]], inner[stretch[^1]]);
		}

		var stops = new List<float> { from };
		stops.AddRange( lengths.Where( x => x > from + 0.01f && x < to - 0.01f ) );
		stops.Add( to );

		for ( var i = 0; i < stops.Count - 1; i++ )
		{
			var a = At( stops[i] );
			var b = At( stops[i + 1] );

			kit.Prism( [a.Outer, b.Outer, b.Inner, a.Inner], low, high );
		}
	}

	void BuildButtresses( MeshKit kit, List<Vector3> path, float side, float right )
	{
		var half = ButtressWidth * 0.5f;
		var height = MathF.Max( Height, 1.0f ) * 0.85f;

		kit.Material = TrimMaterial;

		foreach ( var (position, direction) in WalkPath( path, Closed, MathF.Max( ButtressSpacing, 8.0f ) ) )
		{
			var along = direction.WithZ( 0.0f ).Normal;
			var outward = new Vector3( along.y, -along.x, 0.0f ) * side;
			var start = position - outward * (right * side);

			var low = start - along * half;
			var high = start + along * half;
			var lowOut = low + outward * ButtressDepth;
			var highOut = high + outward * ButtressDepth;
			var up = Vector3.Up * height;

			// Which way round depends on which side of the wall the outside is.
			if ( side > 0.0f ) kit.Hex( high, low, lowOut, highOut, high + up, low + up, lowOut + up, highOut + up );
			else kit.Hex( low, high, highOut, lowOut, low + up, high + up, highOut + up, lowOut + up );
		}
	}

	void BuildSingleFace( MeshKit kit, List<Vector3> path, float side )
	{
		if ( SeparateWallMaterials )
			kit.Material = (FaceSide == WallFaceSide.Inside ? InteriorSurface : ExteriorSurface) ?? SurfaceMaterial;
		var flip = (FaceSide == WallFaceSide.Inside) == (side > 0.0f);
		var segments = Closed ? path.Count : path.Count - 1;
		var down = Vector3.Up * GroundOffset;
		var heights = TopHeights( path.Count );
		for ( var i = 0; i < segments; i++ )
		{
			var j = (i + 1) % path.Count;
			var a = path[i];
			var b = path[j];
			var aUp = Vector3.Up * (GroundOffset + heights[i]);
			var bUp = Vector3.Up * (GroundOffset + heights[j]);
			if ( flip )
				kit.Face( b + down, a + down, a + aUp, b + bUp );
			else
				kit.Face( a + down, b + down, b + bUp, a + aUp );
		}
	}
}