Editor-side code for balcony fixtures. Defines ArchBalconyShape (geometry helpers for projecting/recessed balconies) and static ArchBalcony methods to place, resolve, host lookup, create slab platform, and remove associated cut parts.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Resolved once, for the generator, the placement ghost, the affector and the report.
public readonly struct ArchBalconyShape
{
public Vector2 Anchor { get; init; }
public Vector2 Facing { get; init; }
public Vector2 Along { get; init; }
public float HalfWidth { get; init; }
// As authored: outward projects a slab, zero is a Juliet, inward bites a loggia out of the elevation.
public float Reach { get; init; }
public float Deck { get; init; }
public float Thickness { get; init; }
public float RailHeight { get; init; }
public float Head { get; init; }
public float Bury { get; init; }
public float Bite { get; init; }
public bool Rail { get; init; }
public bool Coping { get; init; }
public int Level { get; init; }
public bool Projects => Reach > 0.01f;
public bool IsRecessed => Reach < -0.01f;
public bool IsJuliet => !Projects && !IsRecessed;
public float Soffit => Deck - Thickness;
public Vector2 Corner( float side, float outward ) => Anchor + Along * (HalfWidth * side) + Facing * outward;
// The slab a projecting balcony stands on, buried into the elevation so the two share no plane.
public List<Vector2> Slab()
{
if ( !Projects )
{
return new List<Vector2>();
}
return new List<Vector2> { Corner( 1f, -Bury ), Corner( -1f, -Bury ), Corner( -1f, Reach ), Corner( 1f, Reach ) };
}
// What a loggia takes out of the elevation - the mouth is the wall face, the depth runs inward.
public List<Vector2> Recess()
{
if ( !IsRecessed )
{
return new List<Vector2>();
}
return new List<Vector2> { Corner( 1f, Bite ), Corner( -1f, Bite ), Corner( -1f, Reach ), Corner( 1f, Reach ) };
}
// Three sides of a projecting deck, one line across the mouth of a Juliet or a loggia. Either way it
// dies against the elevation at both ends, which is why the run is open rather than a ring.
public List<Vector3> Edge()
{
if ( !Projects )
{
return new List<Vector3> { Raised( Corner( -1f, 0f ) ), Raised( Corner( 1f, 0f ) ) };
}
return new List<Vector3>
{
Raised( Corner( -1f, 0f ) ),
Raised( Corner( -1f, Reach ) ),
Raised( Corner( 1f, Reach ) ),
Raised( Corner( 1f, 0f ) )
};
}
Vector3 Raised( Vector2 point ) => new( point.x, point.y, Deck );
}
public static class ArchBalcony
{
// Every building on the storey, because ActiveBuilding falls back to the empty placeholder and the active
// room is whichever one the picker happened to be on. The wall is the datum, and ArchFixtureAnchor is the
// one answer to which wall, which way it faces and where along it the unit stands.
public static ArchBalconyPart Place( ArchPlan plan, ArchKit kit, int level, Vector2 point, ArchBalconyPart draft, out ArchBuilding host )
{
host = null;
if ( plan is null || draft is null )
{
return null;
}
var station = ArchFixtureAnchor.At( plan.Buildings, level, new ArchGridService(), point, draft.Width, plan );
return Place( plan, kit, station, draft, out host );
}
public static ArchBalconyPart Place( ArchPlan plan, ArchKit kit, ArchFixtureStation? at, ArchBalconyPart draft, out ArchBuilding host )
{
host = null;
if ( plan is null || draft is null || at is not { } station )
{
return null;
}
host = station.Building;
var thickness = station.Wall.Thickness > 0f ? station.Wall.Thickness : kit.WallThickness;
return new ArchBalconyPart
{
Id = plan.AllocateId(),
Name = $"Balcony{plan.Filed<ArchBalconyPart>( host ).Count() + 1}",
Anchor = station.Centre + station.Outward * (thickness * 0.5f),
Outward = station.Outward,
BaseHeight = station.Room.BaseHeight + draft.Lift,
Width = station.Width,
Reach = draft.Reach,
Thickness = draft.Thickness,
Lift = draft.Lift,
Rail = draft.Rail,
RailHeight = draft.RailHeight,
Coping = draft.Coping
};
}
public static ArchBalconyShape Resolve( ArchBalconyPart balcony, ArchBuilding building, ArchKit kit )
{
var wall = Hosting( building, balcony, out var room );
var thickness = wall?.Thickness > 0f ? wall.Thickness : kit.WallThickness;
var bite = ArchContact.Bite( kit );
var storey = room is null ? kit.WallHeight : ArchFloorGen.WallHeight( room, kit );
return new ArchBalconyShape
{
Anchor = balcony.Anchor,
Facing = balcony.Facing,
Along = balcony.Along,
HalfWidth = MathF.Max( 6f, balcony.Width * 0.5f ),
Reach = Depth( balcony, thickness, bite ),
Deck = balcony.BaseHeight,
Thickness = balcony.Thickness > 0.5f ? balcony.Thickness : kit.BalconySlab,
RailHeight = balcony.RailHeight > 1f ? balcony.RailHeight : kit.BalconyRail,
Head = balcony.BaseHeight + storey,
Bury = thickness * 0.5f + bite,
Bite = bite,
Rail = balcony.Rail,
Coping = balcony.Coping,
Level = room?.Floor ?? 0
};
}
static float Depth( ArchBalconyPart balcony, float thickness, float bite )
{
if ( balcony.IsJuliet )
{
return 0f;
}
// A loggia shallower than the wall it bites would leave the elevation standing across its own mouth.
if ( balcony.IsRecessed )
{
return -MathF.Max( thickness + bite * 2f, -balcony.Reach );
}
return balcony.Reach;
}
// The wall it was placed against, on the storey its deck stands on - a stacked elevation has one wall
// per storey at the same point in plan, and reading the wrong one files the bite through the wrong slab.
public static ArchWall Hosting( ArchBuilding building, ArchBalconyPart balcony, out ArchRoom room )
{
room = null;
if ( building is null )
{
return null;
}
var storey = building.Rooms.Where( entry => MathF.Abs( entry.BaseHeight - balcony.BaseHeight ) < 1f ).ToList();
var wall = ArchTool.NearestWall( storey, balcony.Anchor, out room, out _, out _ );
return wall ?? ArchTool.NearestWall( building.Rooms, balcony.Anchor, out room, out _, out _ );
}
// The slab is a hung platform, so a balcony deck and a plinth are the same solid resolved the same way.
public static ArchPlatformPart Slab( ArchBalconyPart balcony, ArchBalconyShape shape )
{
var slab = new ArchPlatformPart
{
Id = balcony.Id,
Name = balcony.Name,
Level = shape.Level,
GradeHeight = shape.Soffit,
TopHeight = shape.Deck,
Hung = true,
Coping = shape.Coping,
Palette = balcony.Palette
};
slab.Reshape( shape.Slab() );
return slab;
}
// Deleting a balcony takes the bite it owns; the group it stood in is the author's and stays.
public static void Remove( ArchPlan plan, ArchBuilding building, ArchBalconyPart balcony )
{
var kinds = ArchKinds.Load();
foreach ( var owned in plan.Filed( ArchKind.Cut, building, kinds ).OfType<ArchCutPart>().Where( entry => entry.OwnerId == balcony.Id ).ToList() )
{
plan.Unfile( owned, kinds );
ArchCutAffector.Remove( plan, owned );
}
ArchLayerGroups.Leave( plan, balcony.Id );
}
}