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.
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
};
}