Editor/Output/ArchMesh.cs
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;
// Taken once per canvas rather than per face: the hook is held, but the null check and the volatile read are not
// free at the rate faces are emitted.
readonly IArchFaceMapper mapper = ArchFaceMappers.Standing();
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 );
// FOLDED, because every texcoord is computed through it. A building put on a plinth moves not one local corner,
// so nothing else in this key would notice that its courses had all just shifted by the lift.
content = ArchHash.Fold( content, projection.Position );
content = ArchHash.Fold( content, projection.Rotation.Forward );
}
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 = 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, and folded through the GRAIN so float noise cannot move the key. The position itself is left
// where the generator resolved it: a canvas stands in its own wall's frame, so snapping a derived
// coordinate to the rung HERE moves one wall's corner and not its neighbour's wherever the two are
// not square to each other, and no station a join hands them can survive both snaps.
content = ArchHash.Fold( ArchHash.Fold( ArchHash.Fold( content, (ulong)key.Item1 ), (ulong)key.Item2 ), (ulong)key.Item3 );
}
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
};
}