Editor/Cabinet/ArchCabinetGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Carcasses along a wall, emitted as named pieces so the counter can be stone over timber boxes without being a
// part of its own. Every solid goes through ArchMesh.Beam, which lofts its box along the line it is given - a run
// on a wall that does not lie on the grid is the same code as one that does.
//
// Nothing here works out where a front goes: the mounts on the resolved shape say, and a prefab hung on one lands
// exactly where the extruded front it replaced did.
public static class ArchCabinetGen {
public static void Build(
ArchMesh canvas,
ArchBuiltPart built,
ArchCabinetPart part,
ArchRoom room,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
ArchStyle style ) {
var shape = ArchCabinetShape.Resolve( plan, kit, room, building, part );
if ( !shape.Standing ) {
return;
}
if ( shape.Type.Prefabbed ) {
Stand( built, shape, part );
return;
}
var chain = new[] { part.Palette, room?.Palette, building?.Palette }.Where( palette => palette is not null ).ToArray();
var carcass = style.Brush( ArchSurface.CabinetCarcass, chain );
var fronts = style.Brush( ArchSurface.CabinetFront, chain );
var counter = style.Brush( ArchSurface.Worktop, chain );
var ironmongery = style.Brush( ArchSurface.CabinetHandle, chain );
var inside = part.Interior || shape.Type.Interior;
foreach ( var cell in shape.Cells ) {
using ( canvas.Part( ArchPieces.Carcass ) ) {
Box( canvas, shape, cell, carcass, inside );
}
using ( canvas.Part( ArchPieces.Fronts ) ) {
Fronts( canvas, built, shape, cell, part, fronts, ironmongery );
}
if ( inside && shape.Shelves > 0 ) {
using ( canvas.Part( ArchPieces.Shelves ) ) {
Shelves( canvas, shape, cell, carcass );
}
}
if ( cell.Mount( ArchCabinetMounts.Plinth ) is { } plinth ) {
using ( canvas.Part( ArchPieces.Plinth ) ) {
Solid( canvas, plinth, carcass );
}
}
if ( cell.Mount( ArchCabinetMounts.Worktop ) is { } top ) {
using ( canvas.Part( ArchPieces.Worktop ) ) {
Solid( canvas, top, counter );
}
}
if ( cell.Mount( ArchCabinetMounts.Cornice ) is { } crown ) {
using ( canvas.Part( ArchPieces.Cornice ) ) {
Solid( canvas, crown, fronts );
}
}
}
foreach ( var joint in shape.Joints ) {
Corner( canvas, built, joint, part, shape.Type, carcass, fronts, counter, ironmongery );
}
}
// ART INSTEAD OF GEOMETRY. The run divided itself and found its corner exactly as a generated one does; what
// stands in each module is the author's own model, asked for here and hung by the scene pass. Nothing is drawn -
// a box under a prefab is a second cabinet inside the one the author made.
static void Stand( ArchBuiltPart built, ArchCabinetShape shape, ArchCabinetPart part ) {
var group = string.IsNullOrWhiteSpace( part.MaterialGroup ) ? shape.Type.MaterialGroup : part.MaterialGroup;
foreach ( var cell in shape.Cells ) {
if ( cell.Mount( ArchCabinetMounts.Carcass ) is { } mount ) {
Asked( built, shape.Type.In( cell.Slot ), mount, ArchNames.Fitting( cell.Index, ArchCabinetMounts.Carcass ), group );
}
}
foreach ( var joint in shape.Joints ) {
if ( joint.Mount( ArchCabinetMounts.CornerCarcass ) is { } mount ) {
Asked( built, shape.Type.In( CabinetSlot.Corner ), mount,
ArchNames.Fitting( joint.Index, ArchCabinetMounts.CornerCarcass ), group );
}
}
}
static void Asked( ArchBuiltPart built, ArchCabinetCarcass carcass, ArchCabinetMount mount, string name, string group ) {
if ( carcass is not { Standing: true } ) {
return;
}
built?.Fittings.Add( new ArchFittingRequest {
Name = name,
Prefab = carcass.Prefab,
Mount = mount.Local,
Space = mount.Space,
Fits = carcass.Fits,
MaterialGroup = group,
Anchor = new ArchCabinetAnchor( carcass.Anchor, carcass.AnchorAlong ),
Turn = carcass.AnchorTurn
} );
}
// The unit standing where two runs meet: a pentagon carcass with two backs on the walls, a return onto each run
// and the 45° front across the inside corner. Its top is the piece that carries the counter round the L, so it
// is emitted whether or not the front is the tool's - a prefabbed corner door replaces the slab and nothing else.
static void Corner( ArchMesh canvas, ArchBuiltPart built, ArchCabinetJoint joint, ArchCabinetPart part,
ArchCabinetType type, ArchBrush carcass, ArchBrush fronts, ArchBrush counter, ArchBrush ironmongery ) {
using ( canvas.Part( ArchPieces.Carcass ) ) {
Prism( canvas, joint.Loop, joint.Foot, joint.Head, carcass );
}
if ( joint.Plinth > 0.01f ) {
using ( canvas.Part( ArchPieces.Plinth ) ) {
Prism( canvas, joint.Kick, joint.Seat, joint.Seat + joint.Plinth, carcass );
}
}
if ( joint.Cornice > 0.01f ) {
using ( canvas.Part( ArchPieces.Cornice ) ) {
Prism( canvas, joint.Crown, joint.Head, joint.Head + joint.Cornice, fronts );
}
}
if ( joint.Counter > 0.01f ) {
using ( canvas.Part( ArchPieces.Worktop ) ) {
Prism( canvas, joint.Top, joint.Head, joint.Head + joint.Counter, counter );
}
}
foreach ( var name in new[]
{
ArchCabinetMounts.CornerFront, ArchCabinetMounts.CornerFrontUpper, ArchCabinetMounts.CornerDrawer
} ) {
if ( joint.Mount( name ) is not { } front ) {
continue;
}
var own = Fitted( part, type, name );
var following = Fitted( part, type, ArchCabinetMounts.Flat( name ) );
// ART ONLY EVER HANGS WHERE IT WAS NAMED. A door modelled for a module is the wrong width on a 45° face,
// so the corner follows the run's extruded STYLE and never its prefab.
if ( own is { Prefabbed: true } ) {
built?.Fittings.Add( new ArchFittingRequest {
Name = ArchNames.Fitting( joint.Index, name ),
Prefab = own.Prefab,
Mount = front.Local,
Space = front.Space,
Fits = own.Fits,
Anchor = new ArchCabinetAnchor( own.Anchor, own.AnchorAlong ),
Turn = own.AnchorTurn
} );
continue;
}
var style = own?.Front ?? (following is { Prefabbed: false } ? following.Front : CabinetFront.Slab);
if ( style == CabinetFront.None ) {
continue;
}
using ( canvas.Part( ArchPieces.Fronts ) ) {
Front( canvas, front, style, type, fronts );
CornerGrip( canvas, joint, name, type, ironmongery );
}
}
}
static void CornerGrip( ArchMesh canvas, ArchCabinetJoint joint, string front, ArchCabinetType type, ArchBrush brush ) {
var name = ArchCabinetMounts.Same( front, ArchCabinetMounts.CornerFrontUpper )
? ArchCabinetMounts.CornerHandleUpper
: ArchCabinetMounts.CornerHandle;
if ( ArchCabinetMounts.Same( front, ArchCabinetMounts.CornerDrawer ) || joint.Mount( name ) is not { } grip ) {
return;
}
Ironmongery( canvas, grip, type.Grip, brush );
}
static void Prism( ArchMesh canvas, IReadOnlyList<Vector2> loop, float foot, float head, ArchBrush brush ) {
if ( loop.Count < 3 || head - foot < 0.05f ) {
return;
}
var bottom = loop.Select( point => new Vector3( point.x, point.y, foot ) ).ToList();
var top = loop.Select( point => new Vector3( point.x, point.y, head ) ).ToList();
canvas.Prism( bottom, top, brush );
}
// Interior off is ONE solid: five outward faces and nothing a player who never opens it can see. Interior on
// is the real panels - two ends, a back, a bottom and a top - which is what a door that swings costs.
static void Box( ArchMesh canvas, ArchCabinetShape shape, ArchCabinetCell cell, ArchBrush brush, bool inside ) {
var face = shape.Depth - shape.Type.FrontThickness;
if ( !inside ) {
canvas.Beam( cell.Start, cell.End, 0f, face, cell.Foot, cell.Head, brush );
return;
}
var panel = MathF.Max( 0.25f, shape.Type.PanelThickness );
var inner = InnerRun( shape, cell, panel, out var from, out var to );
canvas.Beam( cell.Start, cell.Start + shape.Along * panel, 0f, face, cell.Foot, cell.Head, brush );
canvas.Beam( cell.End - shape.Along * panel, cell.End, 0f, face, cell.Foot, cell.Head, brush );
if ( !inner ) {
return;
}
canvas.Beam( from, to, 0f, panel, cell.Foot, cell.Head, brush );
canvas.Beam( from, to, panel, face, cell.Foot, cell.Foot + panel, brush );
canvas.Beam( from, to, panel, face, cell.Head - panel, cell.Head, brush );
}
// Divided by ArchDivide over the clear height, so shelves land evenly whatever the carcass turned out to be -
// the count is the type's, the spacing is never authored.
static void Shelves( ArchMesh canvas, ArchCabinetShape shape, ArchCabinetCell cell, ArchBrush brush ) {
var panel = MathF.Max( 0.25f, shape.Type.PanelThickness );
if ( !InnerRun( shape, cell, panel, out var from, out var to ) ) {
return;
}
var foot = cell.Foot + panel;
var head = cell.Head - panel;
var clear = head - foot;
if ( clear < panel * 4f ) {
return;
}
var division = ArchDivide.Into( clear, shape.Shelves + 1 );
foreach ( var station in division.Inner ) {
var seat = foot + station;
canvas.Beam( from, to, panel, shape.Depth - shape.Type.FrontThickness - panel, seat, seat + panel, brush );
}
}
static bool InnerRun( ArchCabinetShape shape, ArchCabinetCell cell, float panel, out Vector2 from, out Vector2 to ) {
from = cell.Start + shape.Along * panel;
to = cell.End - shape.Along * panel;
return (to - from).Length > panel;
}
static void Fronts( ArchMesh canvas, ArchBuiltPart built, ArchCabinetShape shape, ArchCabinetCell cell,
ArchCabinetPart part, ArchBrush brush, ArchBrush ironmongery ) {
foreach ( var name in new[] { ArchCabinetMounts.Front, ArchCabinetMounts.FrontUpper, ArchCabinetMounts.Drawer } ) {
if ( cell.Mount( name ) is not { } mount ) {
continue;
}
var fitting = Fitted( part, shape.Type, name );
if ( fitting is null ) {
continue;
}
// A prefab is not the tool's to draw: it is asked for here and hung by the scene pass, and drawing a
// slab under it would leave a second door inside the one the author modelled.
if ( fitting.Prefabbed ) {
built?.Fittings.Add( new ArchFittingRequest {
Name = ArchNames.Fitting( cell.Index, name ),
Prefab = fitting.Prefab,
Mount = mount.Local,
Space = mount.Space,
Fits = fitting.Fits,
Anchor = new ArchCabinetAnchor( fitting.Anchor, fitting.AnchorAlong ),
Turn = fitting.AnchorTurn
} );
continue;
}
if ( fitting.Front == CabinetFront.None ) {
continue;
}
Front( canvas, mount, fitting.Front, shape.Type, brush );
Grip( canvas, shape, cell, name, ironmongery );
}
}
static void Front( ArchMesh canvas, ArchCabinetMount mount, CabinetFront style, ArchCabinetType type, ArchBrush brush ) {
if ( style == CabinetFront.Slab ) {
Solid( canvas, mount, brush );
return;
}
// A framed front is stiles, rails and a field between them - the same four-member walk a grille and a
// wainscot panel take, held to one thickness so the joint reads at the corner.
var stile = MathF.Max( 1.5f, MathF.Min( mount.Space.y * 0.3f, 2.5f ) );
var rail = MathF.Max( 1.5f, MathF.Min( mount.Space.z * 0.3f, 2.5f ) );
Slice( canvas, mount, 0f, stile, 0f, mount.Space.z, mount.Space.x, brush );
Slice( canvas, mount, mount.Space.y - stile, mount.Space.y, 0f, mount.Space.z, mount.Space.x, brush );
Slice( canvas, mount, stile, mount.Space.y - stile, 0f, rail, mount.Space.x, brush );
Slice( canvas, mount, stile, mount.Space.y - stile, mount.Space.z - rail, mount.Space.z, mount.Space.x, brush );
if ( style == CabinetFront.Glass ) {
return;
}
if ( style == CabinetFront.Panel ) {
Fielded( canvas, mount, type, stile, rail, brush );
return;
}
Slice( canvas, mount, stile, mount.Space.y - stile, rail, mount.Space.z - rail, mount.Space.x * 0.4f, brush );
}
// A raised panel is TWO steps, not one: the field sinks into the frame and a fielded centre comes back out of
// it, so the door throws a shadow at its frame and a second one round its own middle.
static void Fielded( ArchMesh canvas, ArchCabinetMount mount, ArchCabinetType type, float stile, float rail,
ArchBrush brush ) {
var thickness = MathF.Max( 0.2f, mount.Space.x );
var sunk = MathF.Min( MathF.Max( 0.05f, type.PanelInset ), thickness * 0.6f );
var field = thickness - sunk;
var raised = MathF.Min( field + MathF.Max( 0f, type.PanelRaise ), thickness - 0.05f );
Slice( canvas, mount, stile, mount.Space.y - stile, rail, mount.Space.z - rail, field, brush );
var across = mount.Space.y - stile * 2f;
var down = mount.Space.z - rail * 2f;
var margin = MathF.Min( MathF.Max( 0f, type.PanelMargin ), MathF.Min( across, down ) * 0.3f );
if ( margin < 0.05f || raised - field < 0.05f ) {
return;
}
Slice( canvas, mount, stile + margin, mount.Space.y - stile - margin, rail + margin,
mount.Space.z - rail - margin, raised, brush );
}
// What the door is opened by, in the grip mount's own frame - along it by Space.y, down it by Space.z and out
// of the face by Space.x.
static void Ironmongery( ArchMesh canvas, ArchCabinetMount grip, CabinetGrip style, ArchBrush brush ) {
var stem = MathF.Max( 0.2f, grip.Space.z );
switch ( style ) {
case CabinetGrip.Knob:
var knob = MathF.Min( grip.Space.y, MathF.Max( 0.6f, grip.Space.z * 2f ) );
Slice( canvas, grip, grip.Space.y - knob, grip.Space.y, 0f, knob, grip.Space.x * 1.4f, brush );
break;
case CabinetGrip.Cup:
Slice( canvas, grip, 0f, grip.Space.y, 0f, grip.Space.z, grip.Space.x * 0.35f, brush );
Slice( canvas, grip, 0f, grip.Space.y, 0f, grip.Space.z * 0.45f, grip.Space.x, brush );
break;
default:
Slice( canvas, grip, 0f, grip.Space.y, 0f, grip.Space.z, grip.Space.x, brush );
Slice( canvas, grip, 0f, stem, 0f, grip.Space.z, -grip.Space.x, brush );
Slice( canvas, grip, grip.Space.y - stem, grip.Space.y, 0f, grip.Space.z, -grip.Space.x, brush );
break;
}
}
// A grip hangs off the front it belongs to, in whatever the type's ironmongery is - the smallest thing that
// reads as a handle at the range a kitchen is seen from.
static void Grip( ArchMesh canvas, ArchCabinetShape shape, ArchCabinetCell cell, string front, ArchBrush brush ) {
var name = ArchCabinetMounts.Same( front, ArchCabinetMounts.FrontUpper )
? ArchCabinetMounts.HandleUpper
: ArchCabinetMounts.Handle;
if ( ArchCabinetMounts.Same( front, ArchCabinetMounts.Drawer ) || cell.Mount( name ) is not { } grip ) {
return;
}
Ironmongery( canvas, grip, shape.Type.Grip, brush );
}
static void Solid( ArchMesh canvas, ArchCabinetMount mount, ArchBrush brush ) {
Slice( canvas, mount, 0f, mount.Space.y, 0f, mount.Space.z, mount.Space.x, brush );
}
// One box inside a mount's own frame: along it from-to, down from its top, and out of its -x edge. A negative
// reach goes the other way, which is how a handle's stems stand back into the door.
static void Slice( ArchMesh canvas, ArchCabinetMount mount, float from, float to, float down, float under,
float reach, ArchBrush brush ) {
var along = mount.Local.Rotation.Left;
var origin = mount.Local.Position + along * from;
var end = mount.Local.Position + along * to;
var start = new Vector2( origin.x, origin.y );
var finish = new Vector2( end.x, end.y );
var near = MathF.Min( 0f, reach );
var far = MathF.Max( 0f, reach );
canvas.Beam( start, finish, near, far, mount.Local.Position.z - under, mount.Local.Position.z - down, brush );
}
// The run's own answer first, then the type's. A run says only what it disagrees with, so a retyped run picks
// every other front up from the type it was pointed at.
static ArchCabinetFitting Fitted( ArchCabinetPart part, ArchCabinetType type, string mount ) {
return part.On( mount ) ?? type.On( mount );
}
}