Editor/Carve/ArchExtrudeAffector.cs
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A Pier zone's columns. Every other extrude kind is work the WALL carries, so the wall generator dresses it and
// nothing has to be stored. A pier cannot be: it is a real dressed ArchPillarPart with a footing, a plinth, a
// banded shaft and a capital off an authored type, and it belongs to the room rather than to the wall.
//
// So they are an effect, and effects obey the contract: re-derived from where the zone stands NOW on every commit,
// scoped to the group the zone was placed in, and closed plan-wide whatever the scope. Drag the zone and the piers
// follow it; disable it and ArchLayerGate stops them being read without deleting anything.
public static class ArchExtrudeAffector {
// The narrowest stretch worth a column of its own - below this the zone is clipping a wall's corner.
const float LeastPier = 6f;
public static int Resolve( ArchPlan plan, ArchKit kit ) {
var stood = 0;
foreach ( var building in plan.Buildings.ToList() ) {
foreach ( var zone in building.Cuts.ToList() ) {
if ( zone.Extrude == ArchExtrudeKind.Pier && Restand( plan, kit, building, zone ) ) {
stood++;
}
}
}
return stood;
}
static bool Restand( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart zone ) {
var wanted = Wanted( plan, kit, home, zone );
// Compared by where the column STANDS, not by which wall carries it - a zone slid along the same wall wants
// the same wall and a different station, and an id check would skip the re-stand.
if ( Standing( plan, zone ).SetEquals( wanted.Select( Signature ) ) ) {
return false;
}
Close( plan, zone );
var type = Dressing( zone );
foreach ( var pier in wanted ) {
var part = type is null
? new ArchPillarPart()
: type.Standing( pier.Wall.PointAt( pier.Along ), pier.Foot, pier.Rise );
part.Id = plan.AllocateId();
part.OwnerId = zone.Id;
part.Name = $"Pier {pier.Wall.Id}";
part.Placement = PillarPlacement.Pilaster;
part.WallId = pier.Wall.Id;
part.WallOffset = pier.Along;
part.Origin = pier.Wall.PointAt( pier.Along );
part.BaseHeight = pier.Foot;
part.Height = pier.Rise;
part.PilasterWidth = pier.Width;
part.PilasterProtrusion = ArchExtrude.Reach( zone );
part.PilasterBothFaces = zone.ExtrudeBothFaces;
part.Palette = zone.Palette;
if ( zone.ExtrudeFooting is { } founding ) {
part.Footing = founding;
}
pier.Room.Pillars.Add( part );
}
return true;
}
// Only the storey the zone's FOOT stands on. A zone crossing three floors is one support, not one per storey -
// which is the whole point of dragging it tall - so the column takes the band's own rise and the storeys above
// are left to the wall behind it.
static List<Pier> Wanted( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart zone ) {
var piers = new List<Pier>();
if ( !zone.HasContent || !ArchLayerGate.On( zone ) || !ArchCut.Affects( zone, ArchCutAffects.Walls ) ) {
return piers;
}
var reach = ArchLayerGroups.Reach( plan, zone.Id, home.Id );
foreach ( var room in plan.AllRooms().Where( room => room.Floor == zone.Level ) ) {
if ( reach is not null && plan.OwnerOf( room ) is { } owner && !reach.Contains( owner.Id ) ) {
continue;
}
var building = plan.OwnerOf( room );
var height = ArchFloorGen.WallHeight( room, kit );
foreach ( var wall in room.Walls ) {
var join = new ArchConnections( building, kit )
.JoinWall( wall, room, ArchWallSection.Thickness( wall, kit ) );
foreach ( var stretch in ArchWallExtrude.Stretches( plan, kit, building, room, wall, join.StartExtend, join.EndExtend, height ) ) {
if ( stretch.Zone.Id != zone.Id || stretch.Width < LeastPier ) {
continue;
}
piers.Add( new Pier {
Room = room,
Wall = wall,
Along = stretch.Along,
Width = stretch.Width,
// The zone's OWN foot, not the stretch the storey clipped to its floor: a footing is buried
// by definition, so a column held to the slab it stands on could never show one.
Foot = Foot( zone ),
// And the zone's whole band, not the stretch either, so one gesture climbs past every floor
// line it crosses.
Rise = Band( zone )
} );
}
}
}
return piers;
}
static float Band( ArchCutPart zone ) {
var rise = 0f;
foreach ( var leg in zone.Legs() ) {
rise = MathF.Max( rise, leg.TopHeight - leg.BaseHeight );
}
return MathF.Max( 1f, rise );
}
static float Foot( ArchCutPart zone ) {
var foot = float.MaxValue;
foreach ( var leg in zone.Legs() ) {
foot = MathF.Min( foot, leg.BaseHeight );
}
return foot >= float.MaxValue ? 0f : foot;
}
static ArchPillarType Dressing( ArchCutPart zone ) {
var shelf = ArchAsks.PillarTypes();
if ( shelf.Count == 0 ) {
return null;
}
return shelf.FirstOrDefault( type => type.Name == zone.ExtrudePillarType ) ?? shelf[0];
}
public static void Close( ArchPlan plan, ArchCutPart zone ) {
foreach ( var room in plan.AllRooms() ) {
room.Pillars.RemoveAll( pillar => pillar.OwnerId == zone.Id );
}
}
static HashSet<string> Standing( ArchPlan plan, ArchCutPart zone ) {
return plan.AllRooms()
.SelectMany( room => room.Pillars )
.Where( pillar => pillar.OwnerId == zone.Id )
.Select( pillar => $"{pillar.WallId}:{pillar.WallOffset:0.##}:{pillar.PilasterWidth:0.##}:{pillar.BaseHeight:0.##}:{pillar.Height:0.##}" )
.ToHashSet();
}
static string Signature( Pier pier ) {
return $"{pier.Wall.Id}:{pier.Along:0.##}:{pier.Width:0.##}:{pier.Foot:0.##}:{pier.Rise:0.##}";
}
sealed class Pier {
public ArchRoom Room { get; init; }
public ArchWall Wall { get; init; }
public float Along { get; init; }
public float Width { get; init; }
public float Foot { get; init; }
public float Rise { get; init; }
}
}