Editor/Services/ArchRowPlacement.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One unit of a street row: a base-grid rectangle, and everything the seed decided about it.
public sealed class ArchRowBay {
public int Index { get; init; }
public Vector2 Min { get; init; }
public Vector2 Max { get; init; }
public int Storeys { get; init; }
public bool Parapet { get; init; }
public Vector2 Size => Max - Min;
}
// One resolve, read by the ghost, the commit and the report - a preview that divided the frontage again would
// draw a different row than the one it stands.
public sealed class ArchRowShape {
public List<ArchRowBay> Bays { get; init; } = new();
public bool AlongX { get; init; }
public float Frontage { get; init; }
public float Depth { get; init; }
public int RoadId { get; init; }
public int Blocked { get; init; }
public int Seed { get; init; }
public bool IsUsable => Bays.Count > 0;
}
public sealed class ArchRowResult {
public ArchSiteAssembly Group { get; init; }
public List<ArchBuilding> Buildings { get; init; } = new();
}
// Staircase of grid-aligned rectangles, not splayed plots — grid snapping outranks the kink.
public static class ArchRowPlacement {
const float Sway = 0.3f;
const float LeastShare = 0.75f;
const int LeastStoreys = 1;
const int MostStoreys = 3;
const int WidthSalt = 5;
const int StoreySalt = 17;
const int ParapetSalt = 37;
// How near the drag has to run to a road before the row takes that road's verge as its frontage.
public static float Verge( ArchRoadPart road ) => road.Reach() * 1.5f;
// The drag START, not the whole rectangle: seeding off both corners reshuffles every bay the author has
// already reviewed as the drag grows.
public static int Seeded( Vector2 start ) {
return unchecked((int)start.x * 73856093 ^ (int)start.y * 19349663);
}
public static ArchRowShape Resolve(
ArchPlan plan,
ArchKit kit,
ArchArchetype archetype,
int level,
Vector2 from,
Vector2 to,
ArchRoadPart road,
ArchCurve centreline,
bool parapets ) {
if ( plan is null || kit is null || archetype is null ) {
return new ArchRowShape();
}
var grid = new ArchGridService();
var unit = MathF.Max( grid.BaseSize, archetype.MinimumPlot.x );
var least = MathF.Max( grid.BaseSize, grid.Base( unit * LeastShare ) );
var seed = Seeded( grid.Base( from ) );
var drafted = road is null
? Straight( grid, from, to, unit, least, seed, parapets )
: Verged( grid, road, centreline ?? road.Curve(), from, to, unit, least, seed, parapets );
return Vacant( plan, kit, level, drafted );
}
public static ArchRowResult Stand(
ArchPlan plan,
ArchKit kit,
ArchRowShape row,
ArchArchetype archetype,
ArchSectionRules rules,
int level,
float baseHeight,
RoofStyle? style,
RidgeRun ridge ) {
if ( plan is null || kit is null || rules is null || row is null || !row.IsUsable ) {
return null;
}
var standing = ArchArchetypeRules.Standing( rules, kit );
var storey = standing + kit.FloorThickness;
var floor = rules.Floor ?? true;
var gutters = rules.Gutters ?? true;
var stood = new List<ArchBuilding>();
foreach ( var bay in row.Bays ) {
var building = new ArchBuilding {
Id = plan.AllocateId(),
Name = $"{archetype?.Title ?? "Unit"}{plan.Buildings.Count + 1}",
Archetype = archetype?.Name ?? "",
GuttersEnabled = gutters && !bay.Parapet
};
var shell = ArchBuild.Shell(
plan, building, kit, level, baseHeight, bay.Min, bay.Max,
floor, style, building.GuttersEnabled, ridge );
if ( shell is null ) {
continue;
}
ArchArchetypeRules.Apply( shell, rules, kit );
Capped( shell.Roof, bay );
plan.Units.Add( building );
Stacked( plan, kit, building, bay, rules, level, baseHeight, storey, standing, floor, style, ridge );
stood.Add( building );
}
if ( stood.Count == 0 ) {
return null;
}
return new ArchRowResult {
Group = ArchLayerGroups.Create( plan, ArchLayerGroups.Named( plan ), ArchAssemblyKind.Group, stood.Select( unit => unit.Id ) ),
Buildings = stood
};
}
static void Stacked(
ArchPlan plan,
ArchKit kit,
ArchBuilding building,
ArchRowBay bay,
ArchSectionRules rules,
int level,
float baseHeight,
float storey,
float standing,
bool floor,
RoofStyle? style,
RidgeRun ridge ) {
for ( var above = 1; above < bay.Storeys; above++ ) {
var section = ArchBuild.Storey(
plan, building, kit, level + above, baseHeight + storey * above, bay.Min, bay.Max,
floor, style, building.GuttersEnabled, ridge );
if ( section is null ) {
return;
}
ArchArchetypeRules.Apply( section, rules, kit );
// A lifted deck keeps the storey Storey measured, so the row restates the one it authored.
section.Room.WallHeight = standing;
if ( section.Roof is { } lifted ) {
lifted.BaseHeight = section.Room.BaseHeight + standing;
}
Capped( section.Roof, bay );
}
}
// A parapet stands on the wall line, so the eave trims that would hang off it go with it.
static void Capped( ArchRoofPart roof, ArchRowBay bay ) {
if ( roof is null || !bay.Parapet ) {
return;
}
roof.Parapet = true;
roof.Overhang = 0f;
roof.Fascia = false;
roof.Soffit = false;
roof.Gutters = false;
}
static ArchRowShape Straight( ArchGridService grid, Vector2 from, Vector2 to, float unit, float least, int seed, bool parapets ) {
var lo = grid.Base( Vector2.Min( from, to ) );
var hi = grid.Base( Vector2.Max( from, to ) );
var span = hi - lo;
var alongX = span.x >= span.y;
var frontage = alongX ? span.x : span.y;
var depth = alongX ? span.y : span.x;
if ( frontage < least || depth < grid.BaseSize ) {
return new ArchRowShape { AlongX = alongX, Frontage = frontage, Depth = depth, Seed = seed };
}
var division = ArchDivide.AtLeast( frontage, unit );
var stations = Stations( division, seed, least );
var start = alongX ? lo.x : lo.y;
var edges = Rising( stations.Select( station => start + grid.Base( station ) ).ToList() );
var near = alongX ? lo.y : lo.x;
var bays = new List<ArchRowBay>();
for ( var index = 0; index < edges.Count - 1; index++ ) {
if ( edges[index + 1] - edges[index] < grid.BaseSize ) {
continue;
}
bays.Add( Bay( index, seed, alongX, edges[index], edges[index + 1], near, near + depth, parapets ) );
}
return new ArchRowShape { Bays = bays, AlongX = alongX, Frontage = frontage, Depth = depth, Seed = seed };
}
static ArchRowShape Verged(
ArchGridService grid,
ArchRoadPart road,
ArchCurve curve,
Vector2 from,
Vector2 to,
float unit,
float least,
int seed,
bool parapets ) {
if ( curve is null || !curve.IsUsable
|| !curve.Nearest( from, out var head, out var headGap )
|| !curve.Nearest( to, out var tail, out var tailGap ) ) {
return Straight( grid, from, to, unit, least, seed, parapets );
}
var furthest = headGap >= tailGap ? head : tail;
var pull = (headGap >= tailGap ? from : to) - furthest.Flat;
var right = Vector2.Dot( pull, Across( furthest ) ) >= 0f;
var reach = road.Reach( right );
var near = MathF.Min( head.Distance, tail.Distance );
var far = MathF.Max( head.Distance, tail.Distance );
var run = far - near;
var depth = MathF.Max( grid.BaseSize, grid.Base( MathF.Max( headGap, tailGap ) - reach ) );
if ( run < least || !curve.Sample( near, out var opening ) || !curve.Sample( far, out var closing ) ) {
return new ArchRowShape { Frontage = run, Depth = depth, RoadId = road.Id, Seed = seed };
}
var chord = closing.Flat - opening.Flat;
var alongX = MathF.Abs( chord.x ) >= MathF.Abs( chord.y );
var forward = (alongX ? chord.x : chord.y) >= 0f;
var outward = (alongX ? pull.y : pull.x) < 0f ? -1f : 1f;
var division = ArchDivide.AtLeast( run, unit );
var stations = Stations( division, seed, least );
float Stationed( float offset ) => forward ? near + offset : far - offset;
var edges = Rising( stations.Select( station => grid.Base( Edge( curve, Stationed( station ), alongX ) ) ).ToList() );
var bays = new List<ArchRowBay>();
for ( var index = 0; index < edges.Count - 1; index++ ) {
if ( edges[index + 1] - edges[index] < grid.BaseSize ) {
continue;
}
if ( !curve.Sample( Stationed( (stations[index] + stations[index + 1]) * 0.5f ), out var frame ) ) {
continue;
}
var kerb = frame.Flat + Across( frame ) * (right ? reach : -reach);
var verge = grid.Base( alongX ? kerb.y : kerb.x );
bays.Add( Bay( index, seed, alongX, edges[index], edges[index + 1], verge, verge + outward * depth, parapets ) );
}
return new ArchRowShape { Bays = bays, AlongX = alongX, Frontage = run, Depth = depth, RoadId = road.Id, Seed = seed };
}
static ArchRowBay Bay( int index, int seed, bool alongX, float opening, float closing, float verge, float back, bool parapets ) {
var near = MathF.Min( verge, back );
var far = MathF.Max( verge, back );
return new ArchRowBay {
Index = index,
Min = alongX ? new Vector2( opening, near ) : new Vector2( near, opening ),
Max = alongX ? new Vector2( closing, far ) : new Vector2( far, closing ),
Storeys = Storeyed( seed, index ),
Parapet = parapets && ArchBarrierShape.Noise( seed, index, ParapetSalt ) < 0.5f
};
}
static int Storeyed( int seed, int index ) {
var reach = MostStoreys - LeastStoreys + 1;
return Math.Clamp( LeastStoreys + (int)MathF.Floor( ArchBarrierShape.Noise( seed, index, StoreySalt ) * reach ), LeastStoreys, MostStoreys );
}
// The party wall between two units is ONE node used by both, so a jittered width can never open a gap.
static List<float> Stations( ArchDivision division, int seed, float least ) {
var stations = new List<float>();
for ( var index = 0; index <= division.Count; index++ ) {
var station = division.At( index );
if ( index > 0 && index < division.Count ) {
station += ArchBarrierShape.Signed( seed, index, WidthSalt ) * division.Step * Sway;
}
stations.Add( station );
}
for ( var index = 1; index < stations.Count - 1; index++ ) {
var earliest = stations[index - 1] + least;
var latest = MathF.Max( earliest, division.Span - least * (stations.Count - 1 - index) );
stations[index] = Math.Clamp( stations[index], earliest, latest );
}
return stations;
}
// A street that doubles back would hand the same edge out twice; the bay between them collapses and drops.
static List<float> Rising( List<float> edges ) {
for ( var index = 1; index < edges.Count; index++ ) {
edges[index] = MathF.Max( edges[index], edges[index - 1] );
}
return edges;
}
static float Edge( ArchCurve curve, float distance, bool alongX ) {
if ( !curve.Sample( distance, out var frame ) ) {
return 0f;
}
return alongX ? frame.Flat.x : frame.Flat.y;
}
static Vector2 Across( ArchFrame frame ) {
var across = new Vector2( frame.Across.x, frame.Across.y );
return across.Length < 0.01f ? new Vector2( 0f, 1f ) : across.Normal;
}
// A trimmed bay loses the party wall it shares and steps out of the staircase, so occupied ground drops the
// whole bay rather than shrinking it.
static ArchRowShape Vacant( ArchPlan plan, ArchKit kit, int level, ArchRowShape drafted ) {
if ( !drafted.IsUsable ) {
return drafted;
}
var boundary = new ArchBoundaryPlacement( plan, kit );
var standing = new List<ArchRowBay>();
var blocked = 0;
foreach ( var bay in drafted.Bays ) {
var placement = boundary.Outside( level, bay.Min, bay.Max );
if ( !placement.IsUsable || placement.Min != bay.Min || placement.Max != bay.Max ) {
blocked++;
continue;
}
standing.Add( bay );
}
return new ArchRowShape {
Bays = standing,
AlongX = drafted.AlongX,
Frontage = drafted.Frontage,
Depth = drafted.Depth,
RoadId = drafted.RoadId,
Blocked = blocked,
Seed = drafted.Seed
};
}
}