editor/AdvancedMappingTools/Core/MeshKit.cs
namespace AdvancedMappingTools;
public enum ArchStyle
{
	Flat,
	[Title( "Round / Roman" )] Round,
	[Title( "Pointed / Gothic" )] Pointed,
	Segmental,
	Tudor,
	Lancet,
	Horseshoe,
	Elliptical,
	Ogee,
	[Title( "Three Centered" )] ThreeCentered
}

/// <summary>
/// Collects faces into a PolygonMesh. Faces are given in local space through <see cref="Frame"/>,
/// wound counter clockwise when seen from outside. Vertices are shared inside one part.
/// In a frame +X runs along the shape, +Y to its left and +Z up.
/// The mesh is made in <see cref="Finish"/>: two parts that meet on exactly the same face become
/// one body there, so a shape put together from pieces has no walls left inside it.
/// </summary>
public sealed class MeshKit
{
	const float WeldPrecision = 100.0f;
	const float WarpLimit = 0.02f;

	sealed record PendingFace( int[] Ring, Vector3[] Points, Material Material, int Part, int Assembly );

	readonly List<PendingFace> _faces = [];

	readonly List<(PolygonMesh Mesh, Transform Frame)> _imports = [];

	/// <summary>Include a finished mesh without re-welding its cut rims or independent solids.</summary>
	public void ImportMesh( PolygonMesh mesh, Transform frame )
	{
		if ( mesh is not null )
			_imports.Add( (mesh, frame) );
	}
	readonly Dictionary<(int, int, int), int> _ids = [];
	int _part;
	int _assembly;
	int _assemblies;
	int _assemblyDepth;
	bool _finished;

	public PolygonMesh Mesh { get; } = new();
	public Material Material { get; set; }
	public Transform Frame { get; set; } = new( Vector3.Zero, Rotation.Identity );
	public int FaceCount { get; private set; }

	/// <summary>
	/// Faces the engine refused. Known after <see cref="Finish"/>.
	/// </summary>
	public int FailedFaces { get; private set; }

	/// <summary>
	/// Faces that could not be connected to their neighbours, where a shape crosses itself.
	/// Known after <see cref="Finish"/>.
	/// </summary>
	public int DetachedFaces { get; private set; }

	public const float DefaultSmoothing = 40.0f;

	const float DentLimit = 0.03f;
	const float PlaneDirections = 500.0f;
	const float PlaneThickness = 0.04f;
	const float FlushLift = 0.06f;
	const float LowArchCenter = 0.25f;
	const float SmallestCap = 0.001f;

	/// <summary>
	/// Faces meeting at less than this many degrees shade as one surface.
	/// </summary>
	public float SmoothingAngle { get; set; } = DefaultSmoothing;

	/// <summary>
	/// Moves parts laid flush on each other apart by a hair, so their faces do not flicker.
	/// </summary>
	public bool LiftFlushFaces { get; set; } = true;

	/// <summary>
	/// Start a new shape that shares no vertices with what was added before.
	/// </summary>
	public void BeginPart() => _part++;

	/// <summary>
	/// Parts added from here to <see cref="EndAssembly"/> are fitted to each other on purpose, like the
	/// bars, beads and glass of a window. They touch along edges without lying on each other, so
	/// none of them is lifted off another. Against everything else they behave as before.
	/// </summary>
	public void BeginAssembly()
	{
		// An assembly inside another one, such as a window in a dormer, belongs to the outer one.
		if ( _assemblyDepth++ == 0 )
			_assembly = ++_assemblies;
	}

	public void EndAssembly()
	{
		if ( _assemblyDepth > 0 && --_assemblyDepth == 0 )
			_assembly = 0;
	}

	/// <summary>
	/// The finished mesh, or nothing if no face was added.
	/// </summary>
	public PolygonMesh Finish()
	{
		if ( FaceCount == 0 && _imports.Count == 0 )
			return null;
		if ( !_finished )
		{
			_finished = true;
			if ( FaceCount > 0 )
				AddFaces();
			foreach ( var(mesh, frame)in _imports )
				Mesh.MergeMesh( mesh, frame, out _, out _, out _ );
		}

		Mesh.SetSmoothingAngle( SmoothingAngle );
		Mesh.TextureAlignToGrid( Transform.Zero );
		return Mesh;
	}

	public void Face( params Vector3[] points )
	{
		var ring = new List<int>( points.Length );
		var world = new List<Vector3>( points.Length );

		foreach ( var point in points )
		{
			var position = Frame.PointToWorld( point );
			var key = ((int)MathF.Round( position.x * WeldPrecision ),
				(int)MathF.Round( position.y * WeldPrecision ),
				(int)MathF.Round( position.z * WeldPrecision ));

			if ( !_ids.TryGetValue( key, out var id ) )
				_ids[key] = id = _ids.Count;

			if ( ring.Count > 0 && ring[^1] == id ) continue;

			ring.Add( id );
			world.Add( position );
		}

		if ( ring.Count > 1 && ring[^1] == ring[0] )
		{
			ring.RemoveAt( ring.Count - 1 );
			world.RemoveAt( world.Count - 1 );
		}

		if ( ring.Count < 3 ) return;

		_faces.Add( new PendingFace( [.. ring], [.. world], Material, _part, _assembly ) );
		FaceCount++;
	}

	/// <summary>
	/// True when two rings with the same corners run the same way round.
	/// </summary>
	static bool SameDirection( int[] a, int[] b )
	{
		var start = Array.IndexOf( b, a[0] );

		return b[(start + 1) % b.Length] == a[1];
	}

	void AddFaces()
	{
		var parts = new Dictionary<int, int>();

		int Find( int x )
		{
			if ( !parts.TryGetValue( x, out var parent ) ) return parts[x] = x;

			while ( parent != x )
			{
				x = parent;
				parent = parts[x];
			}

			return x;
		}

		var dropped = new bool[_faces.Count];

		foreach ( var group in Enumerable.Range( 0, _faces.Count ).GroupBy( i => string.Join( ' ', _faces[i].Ring.OrderBy( x => x ) ) ).Where( x => x.Count() > 1 ) )
		{
			var kept = new List<int>();

			foreach ( var index in group )
			{
				var face = _faces[index];
				// Inside one part such faces mean a shape folded back on itself, which is not for this to hide.
				var same = kept.FindIndex( k => _faces[k].Part != face.Part && SameDirection( _faces[k].Ring, face.Ring ) );
				var opposite = kept.FindIndex( k => _faces[k].Part != face.Part && !SameDirection( _faces[k].Ring, face.Ring ) );

				if ( same >= 0 )
				{
					dropped[index] = true;
				}
				else if ( opposite >= 0 )
				{
					// Back to back: the wall between two parts. Both faces go and the parts become one body.
					dropped[index] = true;
					dropped[kept[opposite]] = true;
					parts[Find( face.Part )] = Find( _faces[kept[opposite]].Part );
					kept.RemoveAt( opposite );
				}
				else
				{
					kept.Add( index );
				}
			}
		}

		var lifted = LiftFlushFaces ? FlushFaces( dropped, Find ) : [];
		var vertices = new Dictionary<(int, int), HalfEdgeMesh.VertexHandle>();

		for ( var i = 0; i < _faces.Count; i++ )
		{
			if ( dropped[i] ) continue;

			var pending = _faces[i];
			var part = Find( pending.Part );
			var handles = new HalfEdgeMesh.VertexHandle[pending.Ring.Length];

			for ( var v = 0; v < handles.Length; v++ )
			{
				var corner = (part, pending.Ring[v]);

				if ( !vertices.TryGetValue( corner, out var handle ) )
					vertices[corner] = handle = Mesh.AddVertex( pending.Points[v] + lifted.GetValueOrDefault( corner ) );

				handles[v] = handle;
			}

			var face = Mesh.AddFace( handles );

			// A shape that crosses itself reuses an edge the mesh already has twice.
			// Such a face stands on its own vertices instead of leaving a hole, and says so.
			if ( !face.IsValid )
			{
				face = Mesh.AddFace( Mesh.AddVertices( pending.Points ) );
				DetachedFaces++;
			}

			if ( !face.IsValid )
			{
				FailedFaces++;
				continue;
			}

			Mesh.SetFaceMaterial( face, pending.Material );
		}
	}

	/// <summary>
	/// Trim laid flush on a wall shows a face in the same place as the wall, and two faces in one
	/// place flicker. Whatever was added later moves out of that plane by a hair, so it stands
	/// in front. Too little to see or to matter, and the last thing that decides the picture.
	/// </summary>
	Dictionary<(int Part, int Point), Vector3> FlushFaces( bool[] dropped, Func<int, int> find )
	{
		var planes = new Dictionary<(int, int, int, int), List<int>>();
		var directions = new Vector3[_faces.Count];

		for ( var i = 0; i < _faces.Count; i++ )
		{
			if ( dropped[i] ) continue;

			var points = _faces[i].Points;
			var sum = Vector3.Zero;

			for ( var k = 1; k + 1 < points.Length; k++ )
				sum += Vector3.Cross( points[k] - points[0], points[k + 1] - points[0] );

			if ( sum.Length < 1e-4f ) continue;

			var normal = sum.Normal;
			var key = ((int)MathF.Round( normal.x * PlaneDirections ), (int)MathF.Round( normal.y * PlaneDirections ), (int)MathF.Round( normal.z * PlaneDirections ), (int)MathF.Round( normal.Dot( points[0] ) / PlaneThickness ));

			directions[i] = normal;

			if ( !planes.TryGetValue( key, out var list ) ) planes[key] = list = [];

			list.Add( i );
		}

		var result = new Dictionary<(int Part, int Point), Vector3>();

		foreach ( var group in planes.Values.Where( x => x.Count > 1 ) )
		{
			if ( group.Select( i => find( _faces[i].Part ) ).Distinct().Count() < 2 ) continue;

			// Layers on layers: each face stands one step in front of the highest one it covers.
			var layers = new Dictionary<int, int>();

			foreach ( var i in group.OrderBy( x => find( _faces[x].Part ) ) )
			{
				var part = find( _faces[i].Part );
				var rank = 0;

				foreach ( var (other, layer) in layers )
				{
					if ( find( _faces[other].Part ) != part && layer >= rank && !Fitted( _faces[i], _faces[other] ) && Overlap( _faces[i].Points, _faces[other].Points, directions[i] ) )
						rank = layer + 1;
				}

				layers[i] = rank;

				if ( rank == 0 ) continue;

				foreach ( var point in _faces[i].Ring )
				{
					var corner = (part, point);
					var lift = directions[i] * (FlushLift * rank);

					if ( result.GetValueOrDefault( corner ).Dot( directions[i] ) < lift.Dot( directions[i] ) )
						result[corner] = result.GetValueOrDefault( corner ) + lift;
				}
			}
		}

		return result;
	}

	static bool Fitted( PendingFace a, PendingFace b ) => a.Assembly != 0 && a.Assembly == b.Assembly;

	/// <summary>
	/// True when two flat faces in one plane cover some of the same area.
	/// </summary>
	static bool Overlap( Vector3[] a, Vector3[] b, Vector3 normal )
	{
		var u = Vector3.Cross( normal, MathF.Abs( normal.x ) < 0.7f ? Vector3.Forward : Vector3.Left ).Normal;
		var v = Vector3.Cross( normal, u ).Normal;

		Vector2[] Flat( Vector3[] points ) => [.. points.Select( p => new Vector2( p.Dot( u ), p.Dot( v ) ) )];

		var first = Flat( a );
		var second = Flat( b );

		static bool Inside( Vector2[] ring, Vector2 point )
		{
			var inside = false;

			for ( var i = 0; i < ring.Length; i++ )
			{
				var p = ring[i];
				var q = ring[(i + 1) % ring.Length];

				if ( p.y > point.y != q.y > point.y && point.x < p.x + (point.y - p.y) / (q.y - p.y) * (q.x - p.x) )
					inside = !inside;
			}

			return inside;
		}

		static bool Cross( Vector2 p, Vector2 q, Vector2 r, Vector2 s )
		{
			var d1 = (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
			var d2 = (q.x - p.x) * (s.y - p.y) - (q.y - p.y) * (s.x - p.x);
			var d3 = (s.x - r.x) * (p.y - r.y) - (s.y - r.y) * (p.x - r.x);
			var d4 = (s.x - r.x) * (q.y - r.y) - (s.y - r.y) * (q.x - r.x);

			return d1 * d2 < -1e-3f && d3 * d4 < -1e-3f;
		}

		// The middle of an edge or a face stands in for faces that only share their corners.
		static Vector2 Middle( Vector2[] ring ) => ring.Aggregate( Vector2.Zero, ( sum, x ) => sum + x ) / ring.Length;

		if ( Inside( first, Middle( second ) ) || Inside( second, Middle( first ) ) ) return true;

		for ( var i = 0; i < first.Length; i++ )
		{
			if ( Inside( second, (first[i] + first[(i + 1) % first.Length]) * 0.5f ) ) return true;

			for ( var k = 0; k < second.Length; k++ )
			{
				if ( Cross( first[i], first[(i + 1) % first.Length], second[k], second[(k + 1) % second.Length] ) ) return true;
			}
		}

		return false;
	}

	public void FacingFront( float x0, float x1, float y, float z0, float z1 ) =>
		Face( new( x0, y, z0 ), new( x1, y, z0 ), new( x1, y, z1 ), new( x0, y, z1 ) );

	public void FacingBack( float x0, float x1, float y, float z0, float z1 ) =>
		Face( new( x1, y, z0 ), new( x0, y, z0 ), new( x0, y, z1 ), new( x1, y, z1 ) );

	public void FacingUp( float x0, float x1, float y0, float y1, float z ) =>
		Face( new( x0, y0, z ), new( x1, y0, z ), new( x1, y1, z ), new( x0, y1, z ) );

	public void FacingDown( float x0, float x1, float y0, float y1, float z ) =>
		Face( new( x0, y0, z ), new( x0, y1, z ), new( x1, y1, z ), new( x1, y0, z ) );

	public void FacingForward( float x, float y0, float y1, float z0, float z1 ) =>
		Face( new( x, y0, z0 ), new( x, y1, z0 ), new( x, y1, z1 ), new( x, y0, z1 ) );

	public void FacingRearward( float x, float y0, float y1, float z0, float z1 ) =>
		Face( new( x, y1, z0 ), new( x, y0, z0 ), new( x, y0, z1 ), new( x, y1, z1 ) );

	/// <summary>
	/// A closed box as a part of its own.
	/// </summary>
	public void Box( Vector3 mins, Vector3 maxs )
	{
		if ( maxs.x - mins.x <= 0.0f || maxs.y - mins.y <= 0.0f || maxs.z - mins.z <= 0.0f ) return;

		BeginPart();
		FacingFront( mins.x, maxs.x, mins.y, mins.z, maxs.z );
		FacingBack( mins.x, maxs.x, maxs.y, mins.z, maxs.z );
		FacingUp( mins.x, maxs.x, mins.y, maxs.y, maxs.z );
		FacingDown( mins.x, maxs.x, mins.y, maxs.y, mins.z );
		FacingForward( maxs.x, mins.y, maxs.y, mins.z, maxs.z );
		FacingRearward( mins.x, mins.y, maxs.y, mins.z, maxs.z );
	}

	/// <summary>
	/// A closed body between a bottom and a top ring of four corners, counter clockwise from above.
	/// </summary>
	public void Hex( Vector3 a, Vector3 b, Vector3 c, Vector3 d, Vector3 e, Vector3 f, Vector3 g, Vector3 h )
	{
		BeginPart();
		Face( d, c, b, a );
		Face( e, f, g, h );
		Face( a, b, f, e );
		Face( b, c, g, f );
		Face( c, d, h, g );
		Face( d, a, e, h );
	}

	/// <summary>
	/// A square bar between two points, kept upright around its length.
	/// </summary>
	public void Beam( Vector3 from, Vector3 to, float width, float height )
	{
		var delta = to - from;
		if ( delta.Length < 0.01f ) return;

		var up = MathF.Abs( delta.Normal.z ) > 0.99f ? Vector3.Forward : Vector3.Up;
		var frame = Frame;

		Frame = frame.ToWorld( new Transform( from, Rotation.LookAt( delta.Normal, up ) ) );
		Box( new Vector3( 0.0f, -width * 0.5f, -height * 0.5f ), new Vector3( delta.Length, width * 0.5f, height * 0.5f ) );
		Frame = frame;
	}

	/// <summary>
	/// A shape drawn in the XZ plane, counter clockwise seen from -Y, extruded along Y.
	/// </summary>
	public void Profile( IReadOnlyList<Vector2> shape, float y0, float y1 )
	{
		BeginPart();
		Face( [.. shape.Select( p => new Vector3( p.x, y0, p.y ) )] );
		Face( [.. shape.Reverse().Select( p => new Vector3( p.x, y1, p.y ) )] );

		for ( var i = 0; i < shape.Count; i++ )
		{
			var a = shape[i];
			var b = shape[(i + 1) % shape.Count];

			Face( new( a.x, y0, a.y ), new( a.x, y1, a.y ), new( b.x, y1, b.y ), new( b.x, y0, b.y ) );
		}
	}

	/// <summary>
	/// A cylinder, or a cone when the radii differ. The part ends in a point at radius zero.
	/// </summary>
	public void Cylinder( Vector3 center, float radius0, float radius1, float z0, float z1, int sides )
	{
		var low = Circle( center, radius0, z0, sides );
		var high = Circle( center, radius1, z1, sides );

		BeginPart();
		Face( [.. low.Reverse()] );
		Face( high );

		for ( var i = 0; i < sides; i++ )
		{
			var j = (i + 1) % sides;
			Face( low[i], low[j], high[j], high[i] );
		}
	}

	/// <summary>
	/// A hollow tube, closed at top and bottom.
	/// </summary>
	public void Ring( Vector3 center, float outer, float inner, float z0, float z1, int sides )
	{
		var outerLow = Circle( center, outer, z0, sides );
		var outerHigh = Circle( center, outer, z1, sides );
		var innerLow = Circle( center, inner, z0, sides );
		var innerHigh = Circle( center, inner, z1, sides );

		BeginPart();

		for ( var i = 0; i < sides; i++ )
		{
			var j = (i + 1) % sides;

			Face( outerLow[i], outerLow[j], outerHigh[j], outerHigh[i] );
			Face( innerLow[j], innerLow[i], innerHigh[i], innerHigh[j] );
			Face( innerHigh[i], outerHigh[i], outerHigh[j], innerHigh[j] );
			Face( innerLow[j], outerLow[j], outerLow[i], innerLow[i] );
		}
	}

	/// <summary>
	/// A turned shape from a list of (radius, height) going upward. A radius of zero closes an end.
	/// </summary>
	public void Lathe( Vector3 center, IReadOnlyList<Vector2> profile, int sides )
	{
		BeginPart();

		for ( var r = 0; r < profile.Count - 1; r++ )
		{
			var low = Circle( center, profile[r].x, profile[r].y, sides );
			var high = Circle( center, profile[r + 1].x, profile[r + 1].y, sides );

			for ( var i = 0; i < sides; i++ )
			{
				var j = (i + 1) % sides;
				Face( low[i], low[j], high[j], high[i] );
			}
		}
	}

	public static Vector3[] Circle( Vector3 center, float radius, float z, int sides )
	{
		var points = new Vector3[sides];

		for ( var i = 0; i < sides; i++ )
		{
			var angle = MathF.PI * 2.0f * i / sides;
			points[i] = center + new Vector3( MathF.Cos( angle ) * radius, MathF.Sin( angle ) * radius, z );
		}

		return points;
	}

	/// <summary>
	/// Skin rings that each follow the same path into a bar with this cross section.
	/// One ring per profile point, counter clockwise in the path's left/up plane.
	/// With <paramref name="materials"/> every side of the profile gets its own material.
	/// </summary>
	public void Sweep( IReadOnlyList<Vector3[]> rings, bool closed, bool closedProfile = true, IReadOnlyList<Material> materials = null )
	{
		var count = rings[0].Length;
		var steps = closed ? count : count - 1;
		var sides = closedProfile ? rings.Count : rings.Count - 1;

		BeginPart();

		for ( var r = 0; r < sides; r++ )
		{
			var p = rings[r];
			var q = rings[(r + 1) % rings.Count];

			if ( materials is not null ) Material = materials[r];

			for ( var i = 0; i < steps; i++ )
			{
				var j = (i + 1) % count;
				SweepQuad( p[i], q[i], q[j], p[j] );
			}
		}

		if ( closed || !closedProfile ) return;

		// A band that runs out to nothing ends in a line: its sides meet there, a cap would have no area.
		if ( HasArea( rings.Select( x => x[0] ) ) ) Face( [.. rings.Reverse().Select( x => x[0] )] );
		if ( HasArea( rings.Select( x => x[^1] ) ) ) Face( [.. rings.Select( x => x[^1] )] );
	}

	static bool HasArea( IEnumerable<Vector3> ring )
	{
		var points = ring.ToArray();
		var normal = Vector3.Zero;

		for ( var i = 0; i < points.Length; i++ )
			normal += Vector3.Cross( points[i], points[(i + 1) % points.Length] );

		return normal.Length > SmallestCap;
	}

	/// <summary>
	/// A side of a shell, which can be any shape. A corner that points inward or lies on a straight
	/// line has to end the diagonal, any other one folds the quad over itself or lays a
	/// triangle flat on that line.
	/// </summary>
	void ShellQuad( Vector3 a, Vector3 b, Vector3 c, Vector3 d )
	{
		var facing = Vector3.Cross( c - a, d - b );

		if ( Dented( a, b, c, facing ) || Dented( c, d, a, facing ) )
		{
			Face( a, b, d );
			Face( b, c, d );
		}
		else if ( Dented( d, a, b, facing ) || Dented( b, c, d, facing ) )
		{
			Face( a, b, c );
			Face( a, c, d );
		}
		else
		{
			SweepQuad( a, b, c, d );
		}
	}

	static bool Dented( Vector3 from, Vector3 point, Vector3 to, Vector3 facing )
	{
		var first = point - from;
		var second = to - point;

		if ( first.Length < 0.001f || second.Length < 0.001f ) return false;

		var turn = Vector3.Cross( first.Normal, second.Normal ).Dot( facing.Normal );

		// A sharp tip runs back on itself as well, but it stays a corner.
		return first.Dot( second ) > 0.0f ? turn < DentLimit : turn < -DentLimit;
	}

	/// <summary>
	/// One side of a sweep. On a climbing turn these quads are warped and a warped face folds
	/// along a diagonal. Top and bottom of a thin body have to fold along the same line,
	/// or they pass through each other, so such a quad is cut here and not left to chance.
	/// </summary>
	void SweepQuad( Vector3 a, Vector3 b, Vector3 c, Vector3 d )
	{
		var normal = Vector3.Cross( b - a, d - a );

		if ( normal.Length < 1e-5f || MathF.Abs( (c - a).Dot( normal.Normal ) ) < WarpLimit )
		{
			Face( a, b, c, d );
			return;
		}

		if ( (b - a).Dot( Vector3.Cross( Vector3.Up, d - a ) ) > 0.0f )
		{
			Face( a, b, c );
			Face( a, c, d );
		}
		else
		{
			Face( a, b, d );
			Face( b, c, d );
		}
	}

	/// <summary>
	/// A closed body between two grids of points with the same size, the upper one looking up.
	/// Going along the first index and then the second one has to turn counter clockwise seen
	/// from above. Corners of a grid may lie on top of each other, a side then ends in a point.
	/// </summary>
	public void Shell( Vector3[,] upper, Vector3[,] lower )
	{
		var columns = upper.GetLength( 0 ) - 1;
		var rows = upper.GetLength( 1 ) - 1;

		BeginPart();

		for ( var i = 0; i < columns; i++ )
		{
			for ( var j = 0; j < rows; j++ )
			{
				ShellQuad( upper[i, j], upper[i + 1, j], upper[i + 1, j + 1], upper[i, j + 1] );
				ShellQuad( lower[i, j + 1], lower[i + 1, j + 1], lower[i + 1, j], lower[i, j] );
			}

			ShellQuad( lower[i, 0], lower[i + 1, 0], upper[i + 1, 0], upper[i, 0] );
			ShellQuad( lower[i + 1, rows], lower[i, rows], upper[i, rows], upper[i + 1, rows] );
		}

		for ( var j = 0; j < rows; j++ )
		{
			ShellQuad( lower[0, j + 1], lower[0, j], upper[0, j], upper[0, j + 1] );
			ShellQuad( lower[columns, j], lower[columns, j + 1], upper[columns, j + 1], upper[columns, j] );
		}
	}

	/// <summary>
	/// A closed bar between two lines of points, with its inner and outer surface as wide as
	/// <paramref name="depth"/>. The stretches between the lines can be any shape.
	/// </summary>
	public void Fan( IReadOnlyList<Vector3> near, IReadOnlyList<Vector3> far, IReadOnlyList<Vector3> nearBack, IReadOnlyList<Vector3> farBack )
	{
		var upper = new Vector3[near.Count, 2];
		var lower = new Vector3[near.Count, 2];

		for ( var i = 0; i < near.Count; i++ )
		{
			upper[i, 0] = farBack[i];
			upper[i, 1] = far[i];
			lower[i, 0] = nearBack[i];
			lower[i, 1] = near[i];
		}

		Shell( upper, lower );
	}

	/// <summary>
	/// How high an arch of this style stands over an opening of this width.
	/// </summary>
	public static float ArchRise( ArchStyle style, float width ) => style switch
	{
		ArchStyle.Round => width * 0.5f,
		ArchStyle.Pointed => width * 0.866f,
		ArchStyle.Segmental => width * 0.2f,
		ArchStyle.Tudor => width * 0.35f,
		ArchStyle.Lancet => width * 1.25f,
		ArchStyle.Horseshoe => width * 0.785f,
		ArchStyle.Elliptical => width * 0.3f,
		ArchStyle.Ogee => width * 0.8f,
		ArchStyle.ThreeCentered => width * 0.33f,
		_ => 0.0f
	};

	/// <summary>
	/// A prism from a closed outline, whichever way round it runs. Outline heights are ignored.
	/// </summary>
	public void Prism( IReadOnlyList<Vector3> outline, float z0, float z1 )
	{
		var area = RingArea( outline );

		if ( MathF.Abs( area ) < 0.001f ) return;

		var ring = area > 0.0f ? outline : outline.Reverse().ToArray();

		BeginPart();
		Face( [.. ring.Reverse().Select( p => p.WithZ( z0 ) )] );
		Face( [.. ring.Select( p => p.WithZ( z1 ) )] );

		for ( var i = 0; i < ring.Count; i++ )
		{
			var a = ring[i];
			var b = ring[(i + 1) % ring.Count];
			Face( a.WithZ( z0 ), b.WithZ( z0 ), b.WithZ( z1 ), a.WithZ( z1 ) );
		}
	}

	/// <summary>
	/// A slice of a ring or disc between two angles in degrees. An inner radius of zero makes a wedge.
	/// </summary>
	public void Arc( Vector3 center, float inner, float outer, float from, float to, float z0, float z1, int segments ) =>
		Prism( ArcOutline( center, inner, outer, from, to, segments ), z0, z1 );

	/// <summary>
	/// The outline of a slice of a ring or disc, in the order of its outer edge and back along the inner one.
	/// </summary>
	public static List<Vector3> ArcOutline( Vector3 center, float inner, float outer, float from, float to, int segments )
	{
		Vector3 At( float degrees, float radius )
		{
			var angle = degrees.DegreeToRadian();
			return center + new Vector3( MathF.Cos( angle ) * radius, MathF.Sin( angle ) * radius, 0.0f );
		}

		var ring = new List<Vector3>();

		for ( var i = 0; i <= segments; i++ )
			ring.Add( At( from + (to - from) * i / segments, outer ) );

		if ( inner <= 0.0f )
		{
			ring.Add( center );
		}
		else
		{
			for ( var i = segments; i >= 0; i-- )
				ring.Add( At( from + (to - from) * i / segments, inner ) );
		}

		return ring;
	}

	/// <summary>
	/// A curved band between two curves with the same number of points, as one closed body.
	/// The curves run from left to right, shaped like the <see cref="ArchCurve"/> of an arch.
	/// </summary>
	public void Band( IReadOnlyList<Vector2> inner, IReadOnlyList<Vector2> outer, float y0, float y1 ) =>
		Profile( Separate( [.. inner, .. outer.Reverse()] ), y0, y1 );

	/// <summary>
	/// An outline without the points that lie on top of their neighbour.
	/// </summary>
	public static List<Vector2> Separate( IEnumerable<Vector2> outline )
	{
		var result = new List<Vector2>();

		foreach ( var point in outline )
		{
			if ( result.Count == 0 || (result[^1] - point).Length > 0.001f ) result.Add( point );
		}

		while ( result.Count > 1 && (result[^1] - result[0]).Length <= 0.001f )
			result.RemoveAt( result.Count - 1 );

		return result;
	}

	/// <summary>
	/// One closed body from neighbouring footprints of different heights, like solid stairs
	/// seen from above. Footprints that meet must share their corners, heights are absolute.
	/// Only the parts of a side that can be seen get a face, nothing is left inside.
	/// </summary>
	public void SteppedSolid( IReadOnlyList<(IReadOnlyList<Vector3> Outline, float Top)> steps, float bottom, Material top, Material side )
	{
		static (int, int) Key( Vector3 p ) => ((int)MathF.Round( p.x * WeldPrecision ), (int)MathF.Round( p.y * WeldPrecision ));

		var rings = steps.Select( x => (IReadOnlyList<Vector3>)(RingArea( x.Outline ) > 0.0f ? x.Outline : x.Outline.Reverse().ToArray()) ).ToArray();
		var edges = new Dictionary<((int, int), (int, int)), int>();
		var stops = new Dictionary<(int, int), SortedSet<float>>();

		for ( var s = 0; s < rings.Length; s++ )
		{
			var ring = rings[s];

			for ( var i = 0; i < ring.Count; i++ )
			{
				var key = Key( ring[i] );

				edges[(key, Key( ring[(i + 1) % ring.Count] ))] = s;

				if ( !stops.TryGetValue( key, out var heights ) )
					stops[key] = heights = [];

				heights.Add( steps[s].Top );
			}
		}

		BeginPart();

		for ( var s = 0; s < rings.Length; s++ )
		{
			var ring = rings[s];
			var height = steps[s].Top;

			Material = side;
			Face( [.. ring.Reverse().Select( p => p.WithZ( bottom ) )] );

			Material = top;
			Face( [.. ring.Select( p => p.WithZ( height ) )] );

			Material = side;

			for ( var i = 0; i < ring.Count; i++ )
			{
				var p = ring[i];
				var q = ring[(i + 1) % ring.Count];
				var low = edges.TryGetValue( (Key( q ), Key( p )), out var other ) ? steps[other].Top : bottom;

				if ( low >= height ) continue;

				var loop = new List<Vector3> { p.WithZ( low ), q.WithZ( low ) };

				loop.AddRange( stops[Key( q )].Where( z => z > low && z < height ).Select( q.WithZ ) );
				loop.Add( q.WithZ( height ) );
				loop.Add( p.WithZ( height ) );
				loop.AddRange( stops[Key( p )].Where( z => z > low && z < height ).Reverse().Select( p.WithZ ) );

				Face( [.. loop] );
			}
		}
	}

	static float RingArea( IReadOnlyList<Vector3> ring )
	{
		var area = 0.0f;

		for ( var i = 0; i < ring.Count; i++ )
		{
			var a = ring[i];
			var b = ring[(i + 1) % ring.Count];
			area += a.x * b.y - b.x * a.y;
		}

		return area;
	}

	/// <summary>
	/// The curve of an arch from its left to its right spring point, as (x, z). Every curve is
	/// symmetric and has an even number of pieces, so its middle point is the crown.
	/// A flat style gives just the two spring points.
	/// </summary>
	public static Vector2[] ArchCurve( float x0, float x1, float spring, float rise, ArchStyle style, int segments )
	{
		if ( style == ArchStyle.Flat || rise <= 0.0f )
			return [new( x0, spring ), new( x1, spring )];

		segments = Math.Clamp( segments, 2, 64 ) / 2 * 2;

		var half = (x1 - x0) * 0.5f;
		var middle = (x0 + x1) * 0.5f;
		var points = new Vector2[segments + 1];
		var crown = segments / 2;

		for ( var i = 0; i <= crown; i++ )
		{
			var side = ArchSide( style, half, rise, i / (float)crown );

			points[i] = new Vector2( middle + side.x, spring + side.y );
			points[segments - i] = new Vector2( middle - side.x, spring + side.y );
		}

		points[0] = new Vector2( x0, spring );
		points[segments] = new Vector2( x1, spring );

		return points;
	}

	/// <summary>
	/// An arch through points of its left half, written as across,up pairs split by semicolons.
	/// Both run from 0 to 1, between the spring point and the crown. The line is smoothed,
	/// and the result is null when the text holds no usable points.
	/// </summary>
	public static Vector2[] ArchCurve( float x0, float x1, float spring, float rise, string profile, int segments )
	{
		var written = ParsePoints( profile );

		if ( written.Count == 0 ) return null;
		if ( written[0] != Vector2.Zero ) written.Insert( 0, Vector2.Zero );
		if ( written[^1] != Vector2.One ) written.Add( Vector2.One );

		segments = Math.Clamp( segments, 2, 64 ) / 2 * 2;

		var half = (x1 - x0) * 0.5f;
		var path = PathCurve.FromPoints( written.Select( p => new Vector3( p.x * half, 0.0f, p.y * rise ) ), written.Count > 2 ? PathInterpolation.CatmullRom : PathInterpolation.Linear );
		var line = new List<Vector2>();

		for ( var s = 0; s < path.SegmentCount; s++ )
		{
			for ( var i = 0; i < 24; i++ )
			{
				var point = path.Evaluate( s, i / 24.0f );
				line.Add( new Vector2( point.x, point.z ) );
			}
		}

		line.Add( new Vector2( half, rise ) );

		var lengths = new float[line.Count];

		for ( var i = 1; i < line.Count; i++ )
			lengths[i] = lengths[i - 1] + (line[i] - line[i - 1]).Length;

		var crown = segments / 2;
		var points = new Vector2[segments + 1];
		var cursor = 1;

		for ( var i = 0; i <= crown; i++ )
		{
			var wanted = lengths[^1] * i / crown;

			while ( cursor < line.Count - 1 && lengths[cursor] < wanted ) cursor++;

			var span = lengths[cursor] - lengths[cursor - 1];
			var along = span > 0.0f ? (wanted - lengths[cursor - 1]) / span : 0.0f;
			var side = line[cursor - 1] + (line[cursor] - line[cursor - 1]) * Math.Clamp( along, 0.0f, 1.0f );

			points[i] = new Vector2( x0 + side.x, spring + side.y );
			points[segments - i] = new Vector2( x1 - side.x, spring + side.y );
		}

		points[0] = new Vector2( x0, spring );
		points[segments] = new Vector2( x1, spring );

		return points;
	}

	/// <summary>
	/// Points written as x,y pairs split by semicolons. What cannot be read is left out.
	/// </summary>
	public static List<Vector2> ParsePoints( string text )
	{
		var result = new List<Vector2>();

		foreach ( var pair in (text ?? "").Split( ';', StringSplitOptions.RemoveEmptyEntries ) )
		{
			var values = pair.Split( ',' );

			if ( values.Length != 2 ) continue;

			if ( float.TryParse( values[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var x )
				&& float.TryParse( values[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var y ) )
			{
				result.Add( new Vector2( x, y ) );
			}
		}

		return result;
	}

	/// <summary>
	/// The left half of an arch around its middle, from the spring point at t = 0 to the crown
	/// at t = 1. All of these are built from true arcs, so a thickness can follow them.
	/// </summary>
	static Vector2 ArchSide( ArchStyle style, float half, float rise, float t )
	{
		switch ( style )
		{
			case ArchStyle.Segmental:
			{
				// A slice of a large circle through both spring points and the crown.
				var radius = (half * half + rise * rise) / (2.0f * rise);
				var angle = MathF.Atan2( half, radius - rise ) * (1.0f - t);

				return new Vector2( -MathF.Sin( angle ) * radius, rise - radius + MathF.Cos( angle ) * radius );
			}

			case ArchStyle.Pointed:
			case ArchStyle.Lancet:
			{
				// Two circles meeting in the tip. Each centre lies on the spring line, further
				// out for a sharper arch: on the opposite spring point for an equilateral one.
				// A low arch needs its centres below that line, or the circles meet in a dip.
				var center = MathF.Max( (rise * rise - half * half) / (2.0f * half), half * LowArchCenter );
				var drop = MathF.Max( (half * half + 2.0f * half * center - rise * rise) / (2.0f * rise), 0.0f );
				var radius = MathF.Sqrt( (half + center) * (half + center) + drop * drop );
				var start = MathF.Atan2( drop, -(half + center) );
				var angle = start + (MathF.Atan2( rise + drop, -center ) - start) * t;

				return new Vector2( center + MathF.Cos( angle ) * radius, MathF.Sin( angle ) * radius - drop );
			}

			case ArchStyle.Tudor:
				return TudorSide( half, rise, t );

			case ArchStyle.ThreeCentered:
				return ThreeCenteredSide( half, rise, t );

			case ArchStyle.Ogee:
			{
				// One curve out of the spring point, turning over into a tip that points up.
				var u = 1.0f - t;

				return new Vector2( -half * (u * u * u + 3.0f * u * u * t + 3.0f * u * t * t * 0.33f), rise * (3.0f * u * u * t * 0.62f + 3.0f * u * t * t * 0.42f + t * t * t) );
			}

			case ArchStyle.Horseshoe:
			{
				// A circle that carries on below its centre, so the opening closes in at the bottom.
				var below = 25.0f.DegreeToRadian();
				var radius = half / MathF.Cos( below );
				var center = half * MathF.Tan( below );
				var angle = MathF.PI + below - (MathF.PI * 0.5f + below) * t;

				return new Vector2( MathF.Cos( angle ) * radius, (center + MathF.Sin( angle ) * radius) * rise / (center + radius) );
			}

			default:
			{
				var angle = MathF.PI * (1.0f - t * 0.5f);

				return new Vector2( MathF.Cos( angle ) * half, MathF.Sin( angle ) * rise );
			}
		}
	}

	/// <summary>
	/// Four centres: a small arc out of the spring point, then a large flat one up to a blunt tip.
	/// </summary>
	static Vector2 TudorSide( float half, float rise, float t )
	{
		var small = half * 0.45f;
		var center = new Vector2( -half + small, 0.0f );
		var tip = new Vector2( 0.0f, rise );
		var offset = tip - center;

		// The small arc turns until the large one can reach the tip from there.
		for ( var degrees = 70.0f; degrees >= 15.0f; degrees -= 5.0f )
		{
			var turn = degrees.DegreeToRadian();
			var outward = new Vector2( -MathF.Cos( turn ), MathF.Sin( turn ) );
			var room = small - offset.Dot( outward );

			if ( room < half * 0.02f ) continue;

			var extra = (offset.LengthSquared - small * small) / (2.0f * room);
			var large = small + extra;
			var far = center - outward * extra;
			var start = MathF.Atan2( outward.y, outward.x );
			var end = MathF.Atan2( tip.y - far.y, tip.x - far.x );

			if ( t < 0.4f )
			{
				var angle = MathF.PI + (start - MathF.PI) * (t / 0.4f);
				return center + new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * small;
			}

			var along = start + (end - start) * ((t - 0.4f) / 0.6f);

			return far + new Vector2( MathF.Cos( along ), MathF.Sin( along ) ) * large;
		}

		return ArchSide( ArchStyle.Pointed, half, rise, t );
	}

	/// <summary>
	/// Three centres: tight arcs at the spring points and one wide arc over the top, a flat
	/// arch that still starts upright. Only an arch lower than half its span can be made so.
	/// </summary>
	static Vector2 ThreeCenteredSide( float half, float rise, float t )
	{
		if ( rise >= half * 0.98f ) return ArchSide( ArchStyle.Elliptical, half, rise, t );

		var small = rise * 0.7f;
		var drop = ((half - small) * (half - small) - (rise - small) * (rise - small)) / (2.0f * (rise - small));
		var large = rise + drop;
		var center = new Vector2( -half + small, 0.0f );
		var turn = MathF.Atan2( drop, -(half - small) );

		if ( t < 0.4f )
		{
			var angle = MathF.PI + (turn - MathF.PI) * (t / 0.4f);
			return center + new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * small;
		}

		var along = turn + (MathF.PI * 0.5f - turn) * ((t - 0.4f) / 0.6f);

		return new Vector2( MathF.Cos( along ) * large, MathF.Sin( along ) * large - drop );
	}

	/// <summary>
	/// A curve moved outward along its own normals, the same distance everywhere. A negative
	/// distance moves it inward. A tip stays a tip, its point moves further to keep the distance.
	/// A curve runs from left to right, which puts outside on its left.
	/// </summary>
	public static Vector2[] OffsetCurve( IReadOnlyList<Vector2> curve, float distance )
	{
		var result = new Vector2[curve.Count];

		for ( var i = 0; i < curve.Count; i++ )
		{
			var before = i > 0 ? (curve[i] - curve[i - 1]).Normal : (curve[1] - curve[0]).Normal;
			var after = i < curve.Count - 1 ? (curve[i + 1] - curve[i]).Normal : before;
			var normalBefore = new Vector2( -before.y, before.x );
			var normalAfter = new Vector2( -after.y, after.x );
			var mitre = (normalBefore + normalAfter).Normal;

			result[i] = curve[i] + mitre * (distance / MathF.Max( mitre.Dot( normalAfter ), 0.25f ));
		}

		ClearCrossings( result );

		return result;
	}

	/// <summary>
	/// A curve moved inward runs into itself at a tip and on a tight turn. Everything between two
	/// stretches that cross is moved to the crossing, so the curve keeps its number of points.
	/// </summary>
	static void ClearCrossings( Vector2[] curve )
	{
		for ( var i = 0; i + 1 < curve.Length; i++ )
		{
			for ( var j = curve.Length - 2; j > i + 1; j-- )
			{
				var d0 = curve[i + 1] - curve[i];
				var d1 = curve[j + 1] - curve[j];
				var cross = d0.x * d1.y - d0.y * d1.x;

				if ( MathF.Abs( cross ) < 1e-6f ) continue;

				var offset = curve[j] - curve[i];
				var u = (offset.x * d1.y - offset.y * d1.x) / cross;
				var v = (offset.x * d0.y - offset.y * d0.x) / cross;

				if ( u < 0.0f || u > 1.0f || v < 0.0f || v > 1.0f ) continue;

				var point = curve[i] + d0 * u;

				for ( var k = i + 1; k <= j; k++ )
					curve[k] = point;

				i = j;
				break;
			}
		}
	}

	/// <summary>
	/// Frame boxes around a rectangular opening, on the inside of its edges.
	/// </summary>
	public void OpeningFrame( float x0, float x1, float sill, float head, float width, float depth, bool withSill )
	{
		if ( width <= 0.0f ) return;

		var low = withSill ? MathF.Min( sill + width, head - width ) : sill;

		foreach ( var (from, to) in new[] { (x0, x0 + width), (x1 - width, x1) } )
		{
			Box( new Vector3( from, -depth, sill ), new Vector3( to, depth, low ) );
			Box( new Vector3( from, -depth, low ), new Vector3( to, depth, head - width ) );
			Box( new Vector3( from, -depth, head - width ), new Vector3( to, depth, head ) );
		}

		Box( new Vector3( x0 + width, -depth, head - width ), new Vector3( x1 - width, depth, head ) );

		if ( withSill )
			Box( new Vector3( x0 + width, -depth, sill ), new Vector3( x1 - width, depth, low ) );
	}

	/// <summary>
	/// Frame around an arched opening: two jambs and one curved band.
	/// </summary>
	public void ArchFrame( float x0, float x1, float sill, float spring, float rise, ArchStyle style, int segments, float width, float depth, bool withSill )
	{
		if ( width <= 0.0f ) return;

		var low = withSill ? MathF.Min( sill + width, spring ) : sill;

		foreach ( var (from, to) in new[] { (x0, x0 + width), (x1 - width, x1) } )
		{
			Box( new Vector3( from, -depth, sill ), new Vector3( to, depth, low ) );
			Box( new Vector3( from, -depth, low ), new Vector3( to, depth, spring ) );
		}

		if ( withSill )
			Box( new Vector3( x0 + width, -depth, sill ), new Vector3( x1 - width, depth, low ) );

		var outer = ArchCurve( x0, x1, spring, rise, style, segments );
		var inner = OffsetCurve( outer, -width );

		// The band stands on the two jambs.
		outer[0] = new Vector2( x0, spring );
		outer[^1] = new Vector2( x1, spring );
		inner[0] = new Vector2( x0 + width, spring );
		inner[^1] = new Vector2( x1 - width, spring );

		Band( inner, outer, -depth, depth );
	}

	/// <summary>
	/// A straight wall with arched openings. Everything above the spring line is one outline
	/// running along the arches, so a curve may reach back over itself like a horseshoe does.
	/// Below it stand the piers. Each piece shares its corners with the next one.
	/// </summary>
	public void WallWithArches( float length, float thickness, float height,
		IReadOnlyList<(float Start, float End)> openings, float sill, float spring, float rise, ArchStyle style, int segments )
	{
		var y0 = -thickness * 0.5f;
		var y1 = thickness * 0.5f;

		var rows = new List<float> { 0.0f };
		if ( sill > 0.0f ) rows.Add( sill );
		rows.Add( spring );

		void Pier( float x0, float x1 )
		{
			if ( x1 - x0 <= 0.0f ) return;

			for ( var r = 0; r < rows.Count - 1; r++ )
			{
				FacingFront( x0, x1, y0, rows[r], rows[r + 1] );
				FacingBack( x0, x1, y1, rows[r], rows[r + 1] );
			}

			FacingDown( x0, x1, y0, y1, 0.0f );
		}

		BeginPart();

		var outline = new List<Vector2> { new( 0.0f, spring ) };
		var cursor = 0.0f;

		foreach ( var (start, end) in openings )
		{
			Pier( cursor, start );

			FacingForward( start, y0, y1, sill, spring );
			FacingRearward( end, y0, y1, sill, spring );

			if ( sill > 0.0f )
			{
				FacingFront( start, end, y0, 0.0f, sill );
				FacingBack( start, end, y1, 0.0f, sill );
				FacingUp( start, end, y0, y1, sill );
				FacingDown( start, end, y0, y1, 0.0f );
			}

			foreach ( var point in ArchCurve( start, end, spring, rise, style, segments ) )
			{
				if ( outline[^1] != point ) outline.Add( point );
			}

			cursor = end;
		}

		Pier( cursor, length );

		if ( outline[^1].x < length ) outline.Add( new Vector2( length, spring ) );

		outline.Add( new Vector2( length, height ) );
		outline.Add( new Vector2( 0.0f, height ) );

		Face( [.. outline.Select( p => new Vector3( p.x, y0, p.y ) )] );
		Face( [.. outline.AsEnumerable().Reverse().Select( p => new Vector3( p.x, y1, p.y ) )] );

		for ( var i = 0; i < outline.Count; i++ )
		{
			var a = outline[i];
			var b = outline[(i + 1) % outline.Count];

			// The stretches along the spring line stand on the piers.
			if ( a.y == spring && b.y == spring ) continue;

			Face( new( a.x, y0, a.y ), new( a.x, y1, a.y ), new( b.x, y1, b.y ), new( b.x, y0, b.y ) );
		}

		for ( var r = 0; r < rows.Count - 1; r++ )
		{
			FacingRearward( 0.0f, y0, y1, rows[r], rows[r + 1] );
			FacingForward( length, y0, y1, rows[r], rows[r + 1] );
		}
	}

	/// <summary>
	/// A straight wall from x = 0 to <paramref name="length"/> with rectangular openings that
	/// all run from <paramref name="sill"/> to <paramref name="head"/>. Laid out on a grid so
	/// every face shares its corners with its neighbours.
	/// </summary>
	public void WallWithOpenings( float length, float thickness, float height,
		IReadOnlyList<(float Start, float End)> openings, float sill, float head )
	{
		var y0 = -thickness * 0.5f;
		var y1 = thickness * 0.5f;

		var columns = new List<float> { 0.0f };
		foreach ( var opening in openings )
		{
			columns.Add( opening.Start );
			columns.Add( opening.End );
		}
		columns.Add( length );

		var rows = new List<float> { 0.0f };
		if ( sill > 0.0f ) rows.Add( sill );
		rows.Add( head );
		if ( head < height ) rows.Add( height );

		var openRow = rows.IndexOf( head ) - 1;

		bool IsOpening( int column ) => column % 2 == 1;

		BeginPart();

		for ( var c = 0; c < columns.Count - 1; c++ )
		{
			var x0 = columns[c];
			var x1 = columns[c + 1];

			for ( var r = 0; r < rows.Count - 1; r++ )
			{
				if ( IsOpening( c ) && r == openRow ) continue;

				FacingFront( x0, x1, y0, rows[r], rows[r + 1] );
				FacingBack( x0, x1, y1, rows[r], rows[r + 1] );
			}

			if ( !IsOpening( c ) || head < height )
				FacingUp( x0, x1, y0, y1, height );

			if ( !IsOpening( c ) || sill > 0.0f )
				FacingDown( x0, x1, y0, y1, 0.0f );

			if ( !IsOpening( c ) ) continue;

			if ( sill > 0.0f ) FacingUp( x0, x1, y0, y1, sill );
			if ( head < height ) FacingDown( x0, x1, y0, y1, head );

			FacingForward( x0, y0, y1, sill, head );
			FacingRearward( x1, y0, y1, sill, head );
		}

		for ( var r = 0; r < rows.Count - 1; r++ )
		{
			FacingRearward( 0.0f, y0, y1, rows[r], rows[r + 1] );
			FacingForward( length, y0, y1, rows[r], rows[r + 1] );
		}
	}
}