Editor/Geometry/ArchMeshSweep.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchMeshSweep {
public static void Solid(
ArchMesh canvas,
IReadOnlyList<Vector3[]> rows,
IReadOnlyList<ArchFrame> frames,
float depth,
IReadOnlyList<ArchBrush> tops,
ArchBrush side,
bool closed,
Func<int, int, ArchBrush?> paint = null,
Func<int, float?> soffit = null,
IReadOnlyList<ArchCarveVolume> carves = null ) {
if ( rows.Count < 2 || rows[0].Length < 2 ) {
return;
}
var ribbed = Ribbed( rows, carves );
var grid = ribbed.Rows;
var across = ribbed.Across;
var bases = Bases( grid, depth, soffit );
var columns = ribbed.Columns;
var segments = closed ? grid.Count : grid.Count - 1;
var carved = Carved( grid, carves, closed );
bool Taken( int row, int column ) => carved.Contains( (row, column) );
int Gap( int column ) => ribbed.Gap[column];
for ( var row = 0; row < segments; row++ ) {
var next = (row + 1) % grid.Count;
var along = frames[row].Along;
var station = frames[row].Distance;
var ahead = next == 0 ? station + (grid[next][0] - grid[row][0]).Length : frames[next].Distance;
for ( var column = 0; column < columns; column++ ) {
if ( Taken( row, column ) ) {
Reveal( canvas, grid, bases, Taken, row, next, column, segments, columns, closed, along, station, ahead, side );
continue;
}
canvas.Ribbon( grid[row][column], grid[row][column + 1], grid[next][column + 1], grid[next][column],
along, paint?.Invoke( row, Gap( column ) ) ?? Top( tops, Gap( column ) ),
Cell( across, row, next, column, station, ahead ) );
}
if ( !Taken( row, 0 ) ) {
Flank( canvas, grid, bases, row, next, 0, along, station, ahead, Top( tops, Gap( 0 ) ), false );
}
if ( !Taken( row, columns - 1 ) ) {
Flank( canvas, grid, bases, row, next, columns, along, station, ahead, Top( tops, Gap( columns - 1 ) ), true );
}
Underside( canvas, grid, bases, across, Taken, row, next, columns, along, station, ahead, side );
}
if ( closed ) {
return;
}
Cap( canvas, grid[0], bases[0], Taken, 0, side, false );
Cap( canvas, grid[^1], bases[^1], Taken, segments - 1, side, true );
}
public static HashSet<(int Row, int Column)> Carved( IReadOnlyList<Vector3[]> rows, IReadOnlyList<ArchCarveVolume> volumes, bool closed = false ) {
var carved = new HashSet<(int Row, int Column)>();
if ( volumes is not { Count: > 0 } || rows is not { Count: > 1 } ) {
return carved;
}
var segments = closed ? rows.Count : rows.Count - 1;
for ( var row = 0; row < segments; row++ ) {
var next = (row + 1) % rows.Count;
for ( var column = 0; column + 1 < rows[row].Length; column++ ) {
var middle = (rows[row][column] + rows[row][column + 1] + rows[next][column + 1] + rows[next][column]) * 0.25f;
foreach ( var volume in volumes ) {
if ( Takes( volume, middle ) ) {
carved.Add( (row, column) );
break;
}
}
}
}
return carved;
}
static bool Takes( ArchCarveVolume volume, Vector3 point ) {
return volume.Covers( new Vector2( point.x, point.y ) ) && Banded( volume, point );
}
static bool Banded( ArchCarveVolume volume, Vector3 point ) {
var flat = new Vector2( point.x, point.y );
return point.z > volume.Floor.At( flat ) && point.z < volume.Ceiling.At( flat );
}
static ArchWeave Cell( IReadOnlyList<float[]> across, int row, int next, int column, float station, float ahead ) {
return new ArchWeave {
A = new Vector2( station, across[row][column] ),
B = new Vector2( station, across[row][column + 1] ),
C = new Vector2( ahead, across[next][column + 1] ),
D = new Vector2( ahead, across[next][column] )
};
}
static void Reveal(
ArchMesh canvas,
IReadOnlyList<Vector3[]> rows,
IReadOnlyList<float> bases,
Func<int, int, bool> carved,
int row,
int next,
int column,
int segments,
int columns,
bool closed,
Vector3 along,
float station,
float ahead,
ArchBrush brush ) {
if ( Stands( carved, row, column - 1, segments, columns ) ) {
Flank( canvas, rows, bases, row, next, column, along, station, ahead, brush, true );
}
if ( Stands( carved, row, column + 1, segments, columns ) ) {
Flank( canvas, rows, bases, row, next, column + 1, along, station, ahead, brush, false );
}
if ( Stands( carved, Behind( row, segments, closed ), column, segments, columns ) ) {
Rib( canvas, rows[row], bases[row], column, brush, true );
}
if ( Stands( carved, Ahead( row, segments, closed ), column, segments, columns ) ) {
Rib( canvas, rows[next], bases[next], column, brush, false );
}
}
static int Behind( int row, int segments, bool closed ) => closed ? (row + segments - 1) % segments : row - 1;
static int Ahead( int row, int segments, bool closed ) => closed ? (row + 1) % segments : row + 1;
static bool Stands( Func<int, int, bool> carved, int row, int column, int segments, int columns ) {
return row >= 0 && row < segments && column >= 0 && column < columns && carved?.Invoke( row, column ) != true;
}
static void Underside(
ArchMesh canvas,
IReadOnlyList<Vector3[]> rows,
IReadOnlyList<float> bases,
IReadOnlyList<float[]> across,
Func<int, int, bool> carved,
int row,
int next,
int columns,
Vector3 along,
float station,
float ahead,
ArchBrush brush ) {
var width = across[row][columns];
if ( !Opened( carved, row, columns ) ) {
canvas.Ribbon(
Base( rows[row][columns], bases[row] ),
Base( rows[row][0], bases[row] ),
Base( rows[next][0], bases[next] ),
Base( rows[next][columns], bases[next] ),
along, brush, ArchWeave.Cell( station, ahead, 0f, width ) );
return;
}
for ( var column = 0; column < columns; column++ ) {
if ( carved( row, column ) ) {
continue;
}
canvas.Ribbon(
Base( rows[row][column + 1], bases[row] ),
Base( rows[row][column], bases[row] ),
Base( rows[next][column], bases[next] ),
Base( rows[next][column + 1], bases[next] ),
along, brush, new ArchWeave {
A = new Vector2( station, width - across[row][column + 1] ),
B = new Vector2( station, width - across[row][column] ),
C = new Vector2( ahead, width - across[next][column] ),
D = new Vector2( ahead, width - across[next][column + 1] )
} );
}
}
static bool Opened( Func<int, int, bool> carved, int row, int columns ) {
for ( var column = 0; column < columns; column++ ) {
if ( carved( row, column ) ) {
return true;
}
}
return false;
}
static ArchBrush Top( IReadOnlyList<ArchBrush> tops, int column ) {
return tops.Count == 0 ? default : tops[Math.Min( column, tops.Count - 1 )];
}
static void Flank( ArchMesh canvas, IReadOnlyList<Vector3[]> rows, IReadOnlyList<float> bases, int row, int next, int column, Vector3 along, float station, float ahead, ArchBrush brush, bool right ) {
var nearTop = rows[row][column];
var farTop = rows[next][column];
var nearBase = Base( nearTop, bases[row] );
var farBase = Base( farTop, bases[next] );
var near = nearTop.z - bases[row];
var far = farTop.z - bases[next];
if ( right ) {
canvas.Ribbon( nearTop, nearBase, farBase, farTop, along, brush, new ArchWeave {
A = new Vector2( station, 0f ),
B = new Vector2( station, near ),
C = new Vector2( ahead, far ),
D = new Vector2( ahead, 0f )
} );
return;
}
canvas.Ribbon( nearTop, farTop, farBase, nearBase, along, brush, new ArchWeave {
A = new Vector2( station, 0f ),
B = new Vector2( ahead, 0f ),
C = new Vector2( ahead, far ),
D = new Vector2( station, near )
} );
}
static float[] Developed( IReadOnlyList<Vector3[]> rows ) {
var widest = rows[0];
var reach = 0f;
foreach ( var row in rows ) {
var width = Across( row );
if ( width[^1] <= reach ) continue;
reach = width[^1];
widest = row;
}
return Across( widest );
}
static List<float[]> Arcs( IReadOnlyList<Vector3[]> rows ) {
var arcs = new List<float[]>();
foreach ( var row in rows ) {
arcs.Add( Across( row ) );
}
return arcs;
}
static float[] Across( IReadOnlyList<Vector3> row ) {
var across = new float[row.Count];
for ( var index = 1; index < row.Count; index++ ) {
across[index] = across[index - 1] + (row[index] - row[index - 1]).Length;
}
return across;
}
static void Cap( ArchMesh canvas, IReadOnlyList<Vector3> row, float bottom, Func<int, int, bool> carved, int band, ArchBrush brush, bool end ) {
for ( var column = 0; column < row.Count - 1; column++ ) {
if ( carved( band, column ) ) {
continue;
}
Rib( canvas, row, bottom, column, brush, end );
}
}
static void Rib( ArchMesh canvas, IReadOnlyList<Vector3> row, float bottom, int column, ArchBrush brush, bool forward ) {
var near = row[column];
var far = row[column + 1];
if ( forward ) {
canvas.Quad( near, far, Base( far, bottom ), Base( near, bottom ), brush );
return;
}
canvas.Quad( far, near, Base( near, bottom ), Base( far, bottom ), brush );
}
static List<float> Bases( IReadOnlyList<Vector3[]> rows, float depth, Func<int, float?> soffit ) {
var bases = new List<float>();
for ( var row = 0; row < rows.Count; row++ ) {
var lowest = float.MaxValue;
foreach ( var point in rows[row] ) {
lowest = MathF.Min( lowest, point.z );
}
var named = soffit?.Invoke( row );
bases.Add( named.HasValue
? MathF.Min( named.Value, lowest - 0.5f )
: lowest - MathF.Max( 0.5f, depth ) );
}
return bases;
}
static Vector3 Base( Vector3 point, float bottom ) => point.WithZ( bottom );
// Rings wind right to left — reversed winding inverts the inner surface
public static void Tube(
ArchMesh canvas,
IReadOnlyList<Vector3[]> inner,
IReadOnlyList<Vector3[]> outer,
IReadOnlyList<ArchFrame> frames,
ArchBrush boreFace,
ArchBrush shellFace,
Func<int, int, bool> skip = null ) {
if ( inner.Count < 2 || inner.Count != outer.Count || inner[0].Length < 2 || outer[0].Length != inner[0].Length ) {
return;
}
var developed = Arcs( inner );
var shell = Arcs( outer );
var columns = inner[0].Length - 1;
var segments = inner.Count - 1;
for ( var row = 0; row < segments; row++ ) {
var next = row + 1;
var along = frames[row].Along;
var station = frames[row].Distance;
var ahead = frames[next].Distance;
for ( var column = 0; column < columns; column++ ) {
if ( skip?.Invoke( row, column ) == true ) {
Bridged( canvas, inner, outer, frames, developed, skip, row, next, column, segments, columns, shellFace );
continue;
}
canvas.Ribbon( inner[row][column], inner[row][column + 1], inner[next][column + 1], inner[next][column],
along, boreFace, Cell( developed, row, next, column, station, ahead ) );
canvas.Ribbon( outer[row][column + 1], outer[row][column], outer[next][column], outer[next][column + 1],
along, shellFace, new ArchWeave {
A = new Vector2( station, shell[row][column + 1] ),
B = new Vector2( station, shell[row][column] ),
C = new Vector2( ahead, shell[next][column] ),
D = new Vector2( ahead, shell[next][column + 1] )
} );
}
}
Ring( canvas, inner[0], outer[0], developed[0], shellFace, false, skip, 0 );
Ring( canvas, inner[^1], outer[^1], developed[^1], shellFace, true, skip, segments - 1 );
Seam( canvas, inner, outer, frames, 0, shellFace, skip );
Seam( canvas, inner, outer, frames, columns, shellFace, skip );
}
static void Bridged(
ArchMesh canvas,
IReadOnlyList<Vector3[]> inner,
IReadOnlyList<Vector3[]> outer,
IReadOnlyList<ArchFrame> frames,
IReadOnlyList<float[]> developed,
Func<int, int, bool> skip,
int row,
int next,
int column,
int segments,
int columns,
ArchBrush brush ) {
if ( Stands( skip, row, column - 1, segments, columns ) ) {
Foot( canvas, inner, outer, frames, row, column, brush, false );
}
if ( Stands( skip, row, column + 1, segments, columns ) ) {
Foot( canvas, inner, outer, frames, row, column + 1, brush, true );
}
if ( Stands( skip, row - 1, column, segments, columns ) ) {
Annulus( canvas, inner[row], outer[row], developed[row], column, brush, true );
}
if ( Stands( skip, row + 1, column, segments, columns ) ) {
Annulus( canvas, inner[next], outer[next], developed[next], column, brush, false );
}
}
static void Seam( ArchMesh canvas, IReadOnlyList<Vector3[]> inner, IReadOnlyList<Vector3[]> outer, IReadOnlyList<ArchFrame> frames, int column, ArchBrush brush, Func<int, int, bool> skip ) {
var right = column == 0;
var cell = right ? 0 : column - 1;
for ( var row = 0; row < inner.Count - 1; row++ ) {
if ( skip?.Invoke( row, cell ) == true ) {
continue;
}
Foot( canvas, inner, outer, frames, row, column, brush, right );
}
}
static void Foot(
ArchMesh canvas,
IReadOnlyList<Vector3[]> inner,
IReadOnlyList<Vector3[]> outer,
IReadOnlyList<ArchFrame> frames,
int row,
int column,
ArchBrush brush,
bool right ) {
var next = row + 1;
var station = frames[row].Distance;
var ahead = frames[next].Distance;
var here = (outer[row][column] - inner[row][column]).Length;
var there = (outer[next][column] - inner[next][column]).Length;
if ( right ) {
canvas.Ribbon( outer[row][column], inner[row][column], inner[next][column], outer[next][column],
frames[row].Along, brush, new ArchWeave {
A = new Vector2( station, here ),
B = new Vector2( station, 0f ),
C = new Vector2( ahead, 0f ),
D = new Vector2( ahead, there )
} );
return;
}
canvas.Ribbon( inner[row][column], outer[row][column], outer[next][column], inner[next][column],
frames[row].Along, brush, new ArchWeave {
A = new Vector2( station, 0f ),
B = new Vector2( station, here ),
C = new Vector2( ahead, there ),
D = new Vector2( ahead, 0f )
} );
}
static void Ring(
ArchMesh canvas,
IReadOnlyList<Vector3> inner,
IReadOnlyList<Vector3> outer,
IReadOnlyList<float> developed,
ArchBrush brush,
bool end,
Func<int, int, bool> skip,
int band ) {
for ( var column = 0; column < inner.Count - 1; column++ ) {
if ( skip?.Invoke( band, column ) == true ) {
continue;
}
Annulus( canvas, inner, outer, developed, column, brush, end );
}
}
// UV mapped along the ring — outer edge takes the stretch
static void Annulus(
ArchMesh canvas,
IReadOnlyList<Vector3> inner,
IReadOnlyList<Vector3> outer,
IReadOnlyList<float> developed,
int column,
ArchBrush brush,
bool end ) {
var here = (outer[column] - inner[column]).Length;
var there = (outer[column + 1] - inner[column + 1]).Length;
if ( end ) {
canvas.Ribbon( outer[column], inner[column], inner[column + 1], outer[column + 1], default, brush, new ArchWeave {
A = new Vector2( here, developed[column] ),
B = new Vector2( 0f, developed[column] ),
C = new Vector2( 0f, developed[column + 1] ),
D = new Vector2( there, developed[column + 1] )
} );
return;
}
canvas.Ribbon( inner[column], outer[column], outer[column + 1], inner[column + 1], default, brush, new ArchWeave {
A = new Vector2( 0f, developed[column] ),
B = new Vector2( here, developed[column] ),
C = new Vector2( there, developed[column + 1] ),
D = new Vector2( 0f, developed[column + 1] )
} );
}
public static void Skin(
ArchMesh canvas,
IReadOnlyList<Vector3[]> rows,
IReadOnlyList<ArchFrame> frames,
ArchBrush brush,
bool closed,
IReadOnlyList<ArchCarveVolume> carves = null ) {
if ( rows.Count < 2 || rows[0].Length < 2 ) {
return;
}
var ribbed = Ribbed( rows, carves );
var grid = ribbed.Rows;
var across = ribbed.Across;
var columns = ribbed.Columns;
var segments = closed ? grid.Count : grid.Count - 1;
var carved = Carved( grid, carves, closed );
for ( var row = 0; row < segments; row++ ) {
var next = (row + 1) % grid.Count;
var station = frames[row].Distance;
var ahead = next == 0 ? station + (grid[next][0] - grid[row][0]).Length : frames[next].Distance;
for ( var column = 0; column < columns; column++ ) {
if ( carved.Contains( (row, column) ) ) {
continue;
}
canvas.Ribbon( grid[row][column], grid[row][column + 1], grid[next][column + 1], grid[next][column],
frames[row].Along, brush, Cell( across, row, next, column, station, ahead ) );
}
}
}
}