Editor/Output/ArchMesh.Faces.cs
using System;
using System.Collections.Generic;
using HalfEdgeMesh;
using Sandbox;
namespace Sunless.Architecture;
public sealed partial class ArchMesh {
void Face( VertexHandle[] handles, ArchBrush brush, Vector3 tangent, ArchWeave? weave = null, Vector3? anchor = null ) {
if ( open is { } into ) {
into.Face( handles, brush, tangent, weave, anchor );
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 );
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;
var shift = Anchored( brush.Shift, normal, axisU, axisV, anchor, scale );
mapper.Map( mesh, face, new Vector4( axisU, shift.x ), new Vector4( axisV, shift.y ), scale );
Woven( face, handles, corners, brush, weave, scale );
Folded( corners, brush, axisU, axisV, scale );
emitted++;
IsEmpty = false;
finished = false;
}
// Shifts UV mapping so a raked run agrees with the level one at the anchor point
Vector2 Anchored( Vector2 shift, Vector3 normal, Vector3 axisU, Vector3 axisV, Vector3? anchor, float scale ) {
if ( anchor is not { } datum ) {
return shift;
}
Axes( normal, Vector3.Zero, out var levelU, out var levelV );
var point = projection.PointToWorld( datum );
return shift + new Vector2(
Vector3.Dot( levelU - axisU, point ) / scale,
Vector3.Dot( levelV - axisV, point ) / scale );
}
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 reference — a reloaded kit must key identically
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 );
}
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 — Finish would overwrite these with projected coordinates otherwise
authored.Add( face );
}
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; at both it's degenerate
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 all corners — a cross product of just three can hit collinear points after carving
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 );
}
// Flips each axis independently to match the level answer — prevents mirror on half the orientations
static void Squared( Vector3 normal, ref Vector3 axisU, ref Vector3 axisV ) {
Axes( normal, Vector3.Zero, out var levelU, out var levelV );
if ( Vector3.Dot( axisU, levelU ) < 0f ) {
axisU = -axisU;
}
if ( Vector3.Dot( axisV, levelV ) < 0f ) {
axisV = -axisV;
}
}
static void Axes( Vector3 normal, Vector3 tangent, out Vector3 axisU, out Vector3 axisV ) {
if ( !tangent.IsNearZeroLength ) {
// Projected into the face plane — a raw tangent stretches to nothing on edge-on faces
axisU = (tangent - normal * Vector3.Dot( normal, tangent )).Normal;
axisV = Vector3.Cross( normal, axisU ).Normal;
if ( !axisU.IsNearZeroLength && !axisV.IsNearZeroLength ) {
Squared( normal, ref axisU, ref axisV );
return;
}
axisU = Vector3.Cross( normal, Vector3.Up ).Normal;
axisV = Vector3.Cross( normal, axisU ).Normal;
if ( !axisU.IsNearZeroLength && !axisV.IsNearZeroLength ) {
Squared( normal, ref axisU, ref axisV );
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
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;
}
}