Editor/Geometry/ArchMeshSweep.Ribs.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// A swept strip with a rib standing wherever a cut's boundary crosses it, and the developed grid to match.
sealed class ArchMeshSweepRibs {
public List<Vector3[]> Rows { get; init; } = new();
// Per row: a rib the SECTION put there quotes the one developed grid, a rib a CUT put there quotes its own
// proportional place between the two it stands between - the same number in every row while the cut's edge
// runs parallel to the road, raking with the edge where it does not.
public List<float[]> Across { get; init; } = new();
// Which SECTION gap each gap of the split grid came from. The brush list and the paving callback are both
// indexed by the section's own columns, so a rib inserted between two of them would shift every brush after it.
public int[] Gap { get; init; } = Array.Empty<int>();
public int Columns => Rows.Count == 0 ? 0 : Rows[0].Length - 1;
}
// A section's ribs are where the ROAD wanted them; a cut's edge falls where it was dragged. Between the two the
// cut used to lose - a cell went whole or stayed whole - so a strip ONE column wide took nothing at all however
// much of its width a cut covered, which is why a cross-passage left the invert under a bore standing across it.
static partial class ArchMeshSweep {
// Slot COUNT is fixed per section gap at the most any row needs, so every row still comes out the same width
// and the grid can never twist. A row needing fewer stands its spare ribs on the gap's far end; the empty
// cells that leaves cost nothing, because ArchMesh reads a collapsed quad as a line and drops it.
internal static ArchMeshSweepRibs Ribbed( IReadOnlyList<Vector3[]> rows, IReadOnlyList<ArchCarveVolume> volumes ) {
var section = Developed( rows );
if ( volumes is not { Count: > 0 } ) {
return Plain( rows, section );
}
var gaps = rows[0].Length - 1;
var crossings = new List<float>[rows.Count][];
var slots = new int[gaps];
for ( var row = 0; row < rows.Count; row++ ) {
crossings[row] = new List<float>[gaps];
for ( var gap = 0; gap < gaps; gap++ ) {
var found = Crossed( rows[row][gap], rows[row][gap + 1], volumes );
crossings[row][gap] = found;
slots[gap] = Math.Max( slots[gap], found.Count );
}
}
return Array.TrueForAll( slots, count => count == 0 )
? Plain( rows, section )
: Split( rows, section, crossings, slots, gaps );
}
// Where a cut's boundary crosses one gap of one row, as parameters along it. Only a cut whose BAND stands at
// that height is asked - one passing clear over the strip wants no rib in it, and Carved would not take the
// cell either way.
static List<float> Crossed( Vector3 from, Vector3 to, IReadOnlyList<ArchCarveVolume> volumes ) {
var found = new List<float>();
var near = new Vector2( from.x, from.y );
var far = new Vector2( to.x, to.y );
foreach ( var volume in volumes ) {
if ( volume.Footprint is not { Count: >= 3 } ) {
continue;
}
foreach ( var at in ArchFootprint.Crossings( volume.Footprint, near, far ) ) {
if ( Banded( volume, Vector3.Lerp( from, to, at ) ) ) {
found.Add( at );
}
}
}
found.Sort();
return Spread( found, (to - from).Length );
}
// A rib a hair off one the section already stands, or off another cut's, is a sliver nobody can skin.
static List<float> Spread( List<float> found, float length ) {
var kept = new List<float>();
foreach ( var at in found ) {
if ( at * length < ArchGridService.FinestSize || (1f - at) * length < ArchGridService.FinestSize ) {
continue;
}
if ( kept.Count > 0 && (at - kept[^1]) * length < ArchGridService.FinestSize ) {
continue;
}
kept.Add( at );
}
return kept;
}
static ArchMeshSweepRibs Plain( IReadOnlyList<Vector3[]> rows, float[] section ) {
var ribs = new ArchMeshSweepRibs { Gap = Ordinals( rows[0].Length - 1 ) };
foreach ( var row in rows ) {
ribs.Rows.Add( row );
ribs.Across.Add( section );
}
return ribs;
}
static ArchMeshSweepRibs Split( IReadOnlyList<Vector3[]> rows, float[] section, List<float>[][] crossings, int[] slots, int gaps ) {
var mapping = new List<int>();
for ( var gap = 0; gap < gaps; gap++ ) {
for ( var slot = 0; slot <= slots[gap]; slot++ ) {
mapping.Add( gap );
}
}
var ribs = new ArchMeshSweepRibs { Gap = mapping.ToArray() };
for ( var row = 0; row < rows.Count; row++ ) {
var points = new List<Vector3> { rows[row][0] };
var across = new List<float> { section[0] };
for ( var gap = 0; gap < gaps; gap++ ) {
var found = crossings[row][gap];
for ( var slot = 0; slot < slots[gap]; slot++ ) {
var at = slot < found.Count ? found[slot] : 1f;
points.Add( Vector3.Lerp( rows[row][gap], rows[row][gap + 1], at ) );
across.Add( section[gap] + (section[gap + 1] - section[gap]) * at );
}
points.Add( rows[row][gap + 1] );
across.Add( section[gap + 1] );
}
ribs.Rows.Add( points.ToArray() );
ribs.Across.Add( across.ToArray() );
}
return ribs;
}
static int[] Ordinals( int count ) {
var ordinals = new int[count];
for ( var index = 0; index < count; index++ ) {
ordinals[index] = index;
}
return ordinals;
}
}