Editor/Output/ArchMesh.cs

Editor-side ArchMesh and related types. ArchMesh builds and manages a HalfEdge mesh, tracks welded vertices, solids and authored faces, computes content hash, and provides utilities for vertex sharing and UV projection; also declares BoxFaces, ArchBrush and ArchWeave helper types for materials, texel scaling and developed-surface coordinates.

Native Interop
using System;
using System.Collections.Generic;
using HalfEdgeMesh;
using Sandbox;

namespace Sunless.Architecture;

public sealed partial class ArchMesh
{
	public const float TexelScale = 0.3076f;

	static readonly Dictionary<Material, Vector2> sheets = new();

	readonly PolygonMesh mesh = new();
	readonly Transform projection;

	Dictionary<(long, long, long), VertexHandle> welds;

	// What this canvas HOLDS, folded as it is emitted, so a part whose geometry did not move is never finished,
	// contact-tested or handed to the engine again. The geometry's own key, not a guess about the inputs that made
	// it - which is the only kind that cannot go stale.
	ulong content = ArchHash.Seed;

	// Cleared by any emit: a canvas added to after being finished has to be tidied again.
	bool finished;

	// The convex pieces this canvas is MADE of, recorded as they are emitted by whatever knew it was emitting a
	// solid. Physics reads them instead of the triangles.
	readonly List<ArchSolid> solids = new();

	// Every face emitted, and how many of them a solid claimed. Physics may only take the solids INSTEAD of the mesh
	// when the two counts agree - a single loose face means some of the piece is described by nothing, and a shape
	// missing there is a player falling through it.
	int emitted;
	int described;

	// The faces that wrote their OWN texcoords off a developed surface. Everything else is mapped by projecting its
	// plane, and Finish computes those from the parameters - so the two have to be told apart.
	readonly HashSet<FaceHandle> authored = new();

	int solidFrom = -1;

	public ulong Content => content;

	public IReadOnlyList<ArchSolid> Solids => solids;

	public bool Describes => emitted > 0 && described == emitted;

	// Open while something is emitting the faces of ONE convex piece. Nested scopes are the outer one's business -
	// a Beam is a Prism, and counting both would claim its faces twice.
	public IDisposable Solid( ArchSolid solid )
	{
		if ( open is { } into )
		{
			return into.Solid( solid );
		}

		return solidFrom >= 0 ? null : new SolidScope( this, solid );
	}

	readonly struct SolidScope : IDisposable
	{
		readonly ArchMesh canvas;
		readonly ArchSolid solid;

		public SolidScope( ArchMesh canvas, ArchSolid solid )
		{
			this.canvas = canvas;
			this.solid = solid;

			canvas.solidFrom = canvas.emitted;
		}

		public void Dispose()
		{
			var drawn = canvas.emitted - canvas.solidFrom;

			canvas.solidFrom = -1;

			if ( drawn <= 0 )
			{
				return;
			}

			canvas.solids.Add( solid );
			canvas.described += drawn;
		}
	}

	public ArchMesh( Transform projection )
	{
		this.projection = projection;
		mesh.SetTransform( projection );
	}

	public Transform Projection => projection;

	// Splits a piece onto its own canvas; both share the projection the UVs are measured in.
	public ArchMesh Sibling() => new( projection );

	// Per piece, never globally: welding two solids that merely touch puts four faces on one edge.
	public ArchMesh Welded()
	{
		welds ??= new Dictionary<(long, long, long), VertexHandle>();

		return this;
	}

	// One solid at a time - welding the whole piece puts four faces on a beam's shared edge.
	public IDisposable Welding()
	{
		if ( open is { } into )
		{
			return into.Welding();
		}

		var held = welds;

		welds = new Dictionary<(long, long, long), VertexHandle>();

		return new WeldScope( this, held );
	}

	readonly struct WeldScope( ArchMesh canvas, Dictionary<(long, long, long), VertexHandle> restore ) : IDisposable
	{
		public void Dispose()
		{
			canvas.welds = restore;
		}
	}

	// Own corners always share, so Collapsed sees them; welding ACROSS faces stays opt-in.
	VertexHandle[] Vertices( params Vector3[] positions )
	{
		if ( open is { } into )
		{
			return into.Vertices( positions );
		}

		var shared = welds ?? new Dictionary<(long, long, long), VertexHandle>();
		var handles = new VertexHandle[positions.Length];

		for ( var index = 0; index < positions.Length; index++ )
		{
			var position = ArchGridService.Fine( positions[index] );
			var key = Grain( position );

			if ( !shared.TryGetValue( key, out var handle ) )
			{
				handle = mesh.AddVertices( position )[0];
				shared[key] = handle;

				// Folded where it is MADE, so a reused corner costs nothing and only the face records that it
				// took it. Positions are already on the finest rung, so an identical plan folds identically.
				content = ArchHash.Fold( content, position );
			}

			handles[index] = handle;
		}

		return handles;
	}

	// A hundredth of an inch, the grain ArchFootprint and ArchAudit key shared corners by.
	static (long, long, long) Grain( Vector3 point )
	{
		return ((long)MathF.Round( point.x * 100f ), (long)MathF.Round( point.y * 100f ), (long)MathF.Round( point.z * 100f ));
	}

	public bool IsEmpty { get; private set; } = true;
}

[System.Flags]
public enum BoxFaces
{
	None = 0,
	Top = 1,
	Bottom = 2,
	Front = 4,
	Back = 8,
	Left = 16,
	Right = 32,
	All = Top | Bottom | Front | Back | Left | Right
}

public readonly struct ArchBrush
{
	static Material missing;

	public static Material Missing => missing ??= Material.Create( "arch_missing", "shaders/complex.shader" );

	public Material Material { get; init; }
	public float TexelScale { get; init; }
	// World projection puts every same-height window on the same grain - shift breaks the stamped copy.
	public Vector2 Shift { get; init; }

	public ArchBrush Shifted( Vector2 shift ) => new() { Material = Material, TexelScale = TexelScale, Shift = Shift + shift };
}

// Developed-surface corner positions in quad order - shared ribs quote identically, so the sheet cannot step.
public readonly struct ArchWeave
{
	public Vector2 A { get; init; }
	public Vector2 B { get; init; }
	public Vector2 C { get; init; }
	public Vector2 D { get; init; }

	public static ArchWeave Cell( float along, float ahead, float across, float over )
	{
		return new ArchWeave
		{
			A = new Vector2( along, across ),
			B = new Vector2( along, over ),
			C = new Vector2( ahead, over ),
			D = new Vector2( ahead, across )
		};
	}

	public Vector2 Corner( int index ) => index switch
	{
		0 => A,
		1 => B,
		2 => C,
		_ => D
	};
}