Editor/Output/ArchMesh.Faces.cs

Editor-side partial of ArchMesh handling face creation and texture/geometry parameterization. It emits faces into a HalfEdgeMesh, computes normals with Newell, chooses texture axes, sets material/texture parameters, folds data into a content hash, handles authored UVs from weaves, and caches material sheet sizes and material keys.

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

namespace Sunless.Architecture;

public sealed partial class ArchMesh
{
	// Vertices() handed its handles to whichever canvas is open, so the face has to be built on the same one.
	void Face( VertexHandle[] handles, ArchBrush brush, Vector3 tangent, ArchWeave? weave = null )
	{
		if ( open is { } into )
		{
			into.Face( handles, brush, tangent, weave );

			return;
		}

		if ( handles is null || handles.Length < 3 )
		{
			return;
		}

		var corners = Collapsed( handles );

		if ( corners is null )
		{
			return;
		}

		var face = mesh.AddFace( corners );

		if ( !face.IsValid )
		{
			return;
		}

		mesh.SetFaceMaterial( face, brush.Material ?? ArchBrush.Missing );

		// Axes from the normal in projection space - the local normal stretches rotated pieces.
		var normal = projection.Rotation * Normal( corners );
		var along = tangent.IsNearZeroLength ? tangent : projection.Rotation * tangent;

		Axes( normal, along, out var axisU, out var axisV );

		var scale = brush.TexelScale > 0f ? brush.TexelScale : TexelScale;

		mesh.SetFaceTextureParameters( face, new Vector4( axisU, brush.Shift.x ), new Vector4( axisV, brush.Shift.y ), scale );

		Woven( face, handles, corners, brush, weave, scale );

		Folded( corners, brush, axisU, axisV, scale );

		emitted++;

		IsEmpty = false;
		finished = false;
	}

	// Corners as INDICES, because their positions were folded where they were made.
	void Folded( VertexHandle[] corners, ArchBrush brush, Vector3 axisU, Vector3 axisV, float scale )
	{
		content = ArchHash.Fold( content, corners.Length );

		foreach ( var corner in corners )
		{
			content = ArchHash.Fold( content, corner.Index );
		}

		content = ArchHash.Fold( content, Keyed( brush.Material ) );
		content = ArchHash.Fold( content, scale );
		content = ArchHash.Fold( content, brush.Shift );
		content = ArchHash.Fold( content, axisU );
		content = ArchHash.Fold( content, axisV );
	}

	static readonly Dictionary<Material, ulong> brushKeys = new();

	// By NAME, not by reference: reloading a kit rebuilds the material cache, and a canvas that came back
	// identical has to key identically or every part in the plan reads as changed.
	static ulong Keyed( Material material )
	{
		if ( material is null )
		{
			return 0ul;
		}

		if ( brushKeys.TryGetValue( material, out var held ) )
		{
			return held;
		}

		return brushKeys[material] = ArchHash.Of( material.Name );
	}

	// On a bend the outside outruns the centreline, so the strip AUTHORS texcoords from the developed surface.
	// Projected parameters stay set - the mapping tool edits with them, the engine falls back on them.
	void Woven( FaceHandle face, VertexHandle[] handles, VertexHandle[] corners, ArchBrush brush, ArchWeave? weave, float scale )
	{
		if ( weave is not { } woven || handles.Length is not (3 or 4) || !mesh.GetFaceVerticesConnectedToFace( face, out var edges ) )
		{
			return;
		}

		var sheet = Sheet( brush.Material );
		var vertices = mesh.GetFaceVertices( face );

		for ( var index = 0; index < edges.Length && index < vertices.Length; index++ )
		{
			var corner = System.Array.IndexOf( handles, vertices[index] );

			if ( corner < 0 )
			{
				continue;
			}

			var developed = woven.Corner( corner ) / scale;
			var coord = new Vector2( developed.x / sheet.x, developed.y / sheet.y );

			mesh.SetTextureCoord( edges[index], coord );

			content = ArchHash.Fold( content, coord );
		}

		// Claimed, because Finish recomputes coordinates from the projected parameters for everything that did NOT
		// author its own - which is most of the generator. Left to run over the whole mesh it threw this weave away
		// again the moment the canvas was finished, and no strip in the game ever tiled.
		authored.Add( face );
	}

	// The size the engine measures brush-mapped faces against - density stays the texel scale's business.
	static Vector2 Sheet( Material material )
	{
		if ( material is null )
		{
			return 512f;
		}

		if ( sheets.TryGetValue( material, out var size ) )
		{
			return size;
		}

		size = 512f;

		if ( material.FirstTexture is { } texture )
		{
			size = texture.Size;

			var width = material.Attributes.GetInt( "WorldMappingWidth" );
			var height = material.Attributes.GetInt( "WorldMappingHeight" );

			if ( width > 0 ) size.x = width / 0.25f;
			if ( height > 0 ) size.y = height / 0.25f;
		}

		sheets[material] = size;

		return size;
	}

	// Collapsed at ONE end is still a triangle and must be emitted - refusing it holes the strip.
	// At both it is a line; a corner repeating out of sequence is a bowtie - neither has a face.
	static VertexHandle[] Collapsed( VertexHandle[] handles )
	{
		var corners = new List<VertexHandle>( handles.Length );

		for ( var index = 0; index < handles.Length; index++ )
		{
			if ( !handles[index].Equals( handles[(index + handles.Length - 1) % handles.Length] ) )
			{
				corners.Add( handles[index] );
			}
		}

		return corners.Count >= 3 && !Repeats( corners ) ? corners.ToArray() : null;
	}

	static bool Repeats( IReadOnlyList<VertexHandle> handles )
	{
		for ( var index = 0; index < handles.Count; index++ )
		{
			for ( var other = index + 1; other < handles.Count; other++ )
			{
				if ( handles[index].Equals( handles[other] ) )
				{
					return true;
				}
			}
		}

		return false;
	}

	public static Vector3 Newell( IReadOnlyList<Vector3> points )
	{
		var normal = Vector3.Zero;

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

			normal += new Vector3(
				(a.y - b.y) * (a.z + b.z),
				(a.z - b.z) * (a.x + b.x),
				(a.x - b.x) * (a.y + b.y) );
		}

		return normal.IsNearZeroLength ? Vector3.Up : normal.Normal;
	}

	static Vector3 Centre( IReadOnlyList<Vector3> points )
	{
		var total = Vector3.Zero;

		foreach ( var point in points )
		{
			total += point;
		}

		return total / points.Count;
	}

	static List<Vector3> Flipped( IReadOnlyList<Vector3> points )
	{
		var reversed = new List<Vector3>( points );
		reversed.Reverse();

		return reversed;
	}

	// Newell over EVERY corner, not a cross product of the first three: a carved n-gon can open with three
	// collinear corners - a clip crossing that lands on one, or a stranded corner conformed into an edge -
	// and their cross product is direction noise the texture axes are then chosen from. That is a face whose
	// tiling smears down the slope and whose texture parameters read back NaN.
	Vector3 Normal( IReadOnlyList<VertexHandle> handles )
	{
		var corners = new List<Vector3>( handles.Count );

		foreach ( var handle in handles )
		{
			corners.Add( mesh.GetVertexPosition( handle ) );
		}

		return Newell( corners );
	}

	static void Axes( Vector3 normal, Vector3 tangent, out Vector3 axisU, out Vector3 axisV )
	{
		if ( !tangent.IsNearZeroLength )
		{
			// Projected INTO the face's own plane, never taken raw. A tangent leaning out of that plane advances u
			// barely at all across the face, so the courses stretch out to nothing in that direction - which is what a
			// piece's run does on every face it stands EDGE ON to, and a frame carried round a bend or over a grade is
			// a degree or two out of square with its own sides anyway.
			axisU = (tangent - normal * Vector3.Dot( normal, tangent )).Normal;
			axisV = Vector3.Cross( normal, axisU ).Normal;

			if ( !axisU.IsNearZeroLength && !axisV.IsNearZeroLength )
			{
				return;
			}

			// Nothing of the run survived the projection - the face stands square across it, the end of a block whose
			// other faces it maps. That face's own level line is still the piece's direction; the world's axes are not,
			// and a piece that said which way it runs must never be mapped to them.
			axisU = Vector3.Cross( normal, Vector3.Up ).Normal;
			axisV = Vector3.Cross( normal, axisU ).Normal;

			if ( !axisU.IsNearZeroLength && !axisV.IsNearZeroLength )
			{
				return;
			}
		}

		var absolute = new Vector3( MathF.Abs( normal.x ), MathF.Abs( normal.y ), MathF.Abs( normal.z ) );

		// Pitched axes: u along the eave, v up the slope - world x/y would skew every course.
		if ( absolute.z > 0.05f && absolute.z < 0.995f )
		{
			var level = Vector3.Cross( normal, Vector3.Up ).Normal;
			var pitch = Vector3.Cross( normal, level ).Normal;

			if ( !level.IsNearZeroLength && !pitch.IsNearZeroLength )
			{
				axisU = level;
				axisV = pitch;
				return;
			}
		}

		if ( absolute.z >= absolute.x && absolute.z >= absolute.y )
		{
			axisU = Vector3.Forward;
			axisV = -Vector3.Left;
			return;
		}

		if ( absolute.x >= absolute.y )
		{
			axisU = Vector3.Left;
			axisV = -Vector3.Up;
			return;
		}

		axisU = Vector3.Forward;
		axisV = -Vector3.Up;
	}
}