Mesh generation utilities for architectural geometry. It builds swept solid, tube and skin meshes from rows of 3D stations and framing info, applies brushes/UVs, handles carved cut-volumes, ribs, caps, undersides and seams.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// One row per station, same column count, left to right; the section at a station is built by the caller.
// Every face carries AUTHORED texcoords off the developed surface: a projected mapping cannot tile a strip that bends.
// `paint` re-skins top faces: a crossover is the pavement itself in another material, not a slab laid over it.
// `soffit` names a shared underside outright: a deck's verge starts a kerb-height lower, so depth cannot say it.
// Cuts split the rows first (ArchMeshSweep.Ribs), so no cell straddles a boundary and the test below is exact.
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;
}
// A rib at every cut boundary FIRST: a cell that straddles one can only go whole or stay whole.
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) );
// The section's own gap, so a brush list and a paving callback are never shifted by an inserted rib.
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 ) );
}
// A flank takes its own strip's top brush, or the back of a pavement comes out in kerb stone.
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 );
}
// WHICH CELLS of a swept grid a set of cut volumes takes out - the one answer, read by the road's own strips and
// by a bore's lining. A cell goes when its own middle stands inside a cut, which is exact only on a grid whose
// cells do not straddle the cut's boundary: along the run the cut's corners pin their own stations, and across
// it the ribs are split - by Ribbed for a strip, by ArchTunnelProfile's heads for a lining.
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 );
}
// A cell's four corners each sit where they really are on the developed sheet: with no cut in the run the two
// rows quote one grid and this is ArchWeave.Cell, and where a cut split them the rib is quoted per row, so the
// two cells sharing it still quote it identically and the tiling cannot step.
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] )
};
}
// What a cut leaves behind: every boundary between a cell that went and one that stayed is a face of the hole.
// A boundary against no neighbour at all is the strip's own outside, which was never there to close.
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;
// A neighbour that is there and stayed. Off the end of the grid is not standing material: that boundary is the
// strip's own outside, which was never there to close.
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;
}
// One quad per row across the whole width, which is what a run with nothing taken out of it wants. A row a cut
// reached is split cell by cell instead - the hole has to be open from underneath as well as from above.
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 )
{
// The last column is a section rib whatever a cut inserted, so the run's own width is row-independent.
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;
}
// Quoted back across, like the whole-width quad it replaces, or the soffit's courses mirror where a row splits.
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;
}
// One brush per column gap (kerb band vs pavement); a short list runs out on its last entry.
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 )
} );
}
// ONE grid serves the whole run, taken off the widest row: measured per row, camber and fall drift and the courses wander.
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 );
}
// Every ring measured on itself. A tube has no widest row to take one grid off: its two skins stand on
// different arcs by construction, and on a bend each station's is its own.
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 );
}
}
// One cell of a rib, standing down to the underside: the run's own end, and every face of a hole that runs
// across the strip rather than along it.
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 );
}
// One level underside per station, or the soffit mirrors its own camber; a named soffit still clears the section above.
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 );
// A bore: lining thickness runs along the profile's normal, not downward, so depth cannot say it.
// Rings wind right to left: wound the other way the inner surface comes out inside out.
// `skip` drops a cell from both surfaces and the two skins are bridged round what is left, so an opening in a
// lining shows the thickness it was built with.
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;
}
// Each ring's OWN arc. One arc quoted for every station stretches the courses wherever the section moves,
// and on a bend the outside of the tube travels farther than the inside - the same reason a strip authors
// its texcoords at all. Two cells sharing a rib still quote that rib identically, so nothing steps.
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 )
{
// A lining is TWO skins, so what a cut takes out of it has a thickness to show: every boundary
// between a cell that went and one that stayed is bridged bore face to shell, which is what
// makes an opening in a bore read as a lined mouth rather than a slot cut in paper. A chamber
// standing in the hole then meets a wall rather than an open edge.
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 ) );
// Wound back across, so its weave is quoted back across with it and off the shell's OWN arc:
// the bore cell's weave run the other way mirrors every course on the outside of the lining.
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 );
// The open profile's feet are seams down the run: left as corners they leave an open edge mouth to mouth.
Seam( canvas, inner, outer, frames, 0, shellFace, skip );
Seam( canvas, inner, outer, frames, columns, shellFace, skip );
}
// The faces of an opening in a lining, wound exactly as the profile's own feet and end rings are - so the
// mouth of a cross-passage is the same solid edge the bore already shows where it stops.
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 );
}
}
// A seam dies with the cell above it, or the lining's bottom edge runs straight across the opening.
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 );
}
}
// One station of a seam down the run: the lining's own thickness, standing across the gap between its skins.
// Wound off the side it is on, or a foot faces up into the solid it closes - and the weave is quoted in that
// winding, or the two feet of one lining tile opposite ways.
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;
// Each END its own thickness: a bore whose section moves down the run carries the lining's depth with it, and
// one number quoted to both ends of a tapering foot steps against the station beside it.
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 )
} );
}
// Closes the lining end; buried in a portal headwall wherever there is one.
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 );
}
}
// One cell of that ring, which is also every face of an opening that runs ACROSS the lining rather than
// along it.
//
// ONE arc position per radial edge, quoted at BOTH its ends. The two skins stand on different arcs - the outer is
// the longer, and by the crown it has run some forty inches ahead - so measuring each corner on its own skin
// slides the outer corner along the band and the tile lines come out diagonal, by a different amount in every
// cell. A flat annulus cannot unroll into a rectangle at all, so it is mapped like any other curved band: along
// the ring, square to it, with the outer edge taking the stretch.
static void Annulus(
ArchMesh canvas,
IReadOnlyList<Vector3> inner,
IReadOnlyList<Vector3> outer,
IReadOnlyList<float> developed,
int column,
ArchBrush brush,
bool end )
{
// Each RADIAL EDGE its own thickness. The two rings part company at the invert - the inner springs off the
// road, the outer carries on down past it - so the cell where the profile leaves the foot is a wedge, and
// quoting the foot's depth to its far edge as well steps that edge against the arc facet standing on it.
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] )
} );
}
// A surface with no thickness laid a hair over another one - a marking, a crossing apron. Nothing to close where
// a cut takes a cell: a skin has no body, so the hole is the missing cell itself.
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 ) );
}
}
}
}