Editor/Wall/ArchWall.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public enum WallCladding {
None,
Weatherboard,
Profiled
}
// A wall carries a LIST of them - a real elevation has several fields.
public enum BandSeat {
Centred,
// A cap finishes a field rather than dividing two, so it sits ON it.
Above
}
public sealed class ArchWallBand {
public float Height { get; set; }
// Relative - a preset naming an absolute height caps only walls that tall.
public bool AtHead { get; set; }
// The role the field BELOW this band takes.
public ArchSurface Field { get; set; } = ArchSurface.WallBase;
public bool Course { get; set; } = true;
public BandSeat Seat { get; set; } = BandSeat.Centred;
// 0 falls back to the kit's own band section.
public float CourseHeight { get; set; }
public float CourseDepth { get; set; }
// A row of blocks under the course. Their pitch is kit stock, so no band carries a count.
public bool Dentils { get; set; }
public float At( float wallHeight ) => AtHead ? wallHeight : Height;
public ArchWallBand Copy() {
return new ArchWallBand {
Height = Height,
AtHead = AtHead,
Field = Field,
Course = Course,
Seat = Seat,
CourseHeight = CourseHeight,
CourseDepth = CourseDepth,
Dentils = Dentils
};
}
}
// Marks no change of field, so it can sit anywhere.
public sealed class ArchWallTrim {
public float Height { get; set; } = 96f;
// Relative - one treatment is worn by walls of several heights.
public bool AtHead { get; set; }
public float Rise { get; set; } = 3f;
public float Depth { get; set; } = 1.5f;
public float Bevel { get; set; } = 0.75f;
public ArchSurface Field { get; set; } = ArchSurface.Trim;
public bool Interior { get; set; }
public float At( float wallHeight ) => AtHead ? wallHeight - Rise : Height;
public ArchWallTrim Copy() {
return new ArchWallTrim {
Height = Height,
AtHead = AtHead,
Rise = Rise,
Depth = Depth,
Bevel = Bevel,
Field = Field,
Interior = Interior
};
}
}
// A run of expressed strips - one part, not one per strip.
public sealed class ArchPilasterRun {
public float Bay { get; set; }
// Wins over Bay when set.
public int Count { get; set; }
public float Width { get; set; } = 12f;
public float Depth { get; set; } = 3f;
// So a pilaster can die under an eave rather than into it.
public float Headroom { get; set; }
// So a column line can stand on a plinth rather than run into the paving.
public float Foot { get; set; }
// An authored climb honoured PAST the storey head, so one pier runs every storey to the roof-edge cornice as a
// single layer. Zero stops it at its own storey's head, less Headroom.
public float Rise { get; set; }
// 0 is a smooth shaft; above it the pier is rusticated, blocks Course tall parted by Joint.
public float Course { get; set; }
public float Joint { get; set; }
// A taller, deeper block at the foot and another under whatever the pier dies into.
public float Plinth { get; set; }
public float Cap { get; set; }
// End strips straddle the corner so two elevations wrap one column.
public bool OnCorners { get; set; } = true;
public bool Stands => (Bay > 1f || Count > 0) && Width > 0.1f && Depth > 0.05f;
public ArchPilasterRun Copy() {
return new ArchPilasterRun {
Bay = Bay,
Count = Count,
Width = Width,
Depth = Depth,
Headroom = Headroom,
Foot = Foot,
Rise = Rise,
Course = Course,
Joint = Joint,
Plinth = Plinth,
Cap = Cap,
OnCorners = OnCorners
};
}
}
// A rhythm the whole wall wears, standing real openings rather than deriving holes - a rhythm's window has
// to wear the frame, sash and sill a dragged one wears, and only an authored unit does.
public sealed class ArchOpeningRun {
public string Preset { get; set; } = "";
public float Bay { get; set; }
// Wins over Bay when set.
public int Count { get; set; }
// 0 takes the preset's own.
public float Width { get; set; }
public float Height { get; set; }
public float SillHeight { get; set; }
public bool Stands => !string.IsNullOrWhiteSpace( Preset ) && (Bay > 1f || Count > 0);
public ArchOpeningRun Copy() {
return new ArchOpeningRun {
Preset = Preset,
Bay = Bay,
Count = Count,
Width = Width,
Height = Height,
SillHeight = SillHeight
};
}
}
// One list of fields, so a setting can't go missing between wall and preset.
public interface IArchWallTreatment {
float Height { get; set; }
float Thickness { get; set; }
bool Exterior { get; set; }
bool SingleSided { get; set; }
bool Baseboard { get; set; }
bool Cap { get; set; }
float CapHeight { get; set; }
float CapOverhang { get; set; }
string Cornice { get; set; }
WallCladding Cladding { get; set; }
bool Wainscot { get; set; }
List<ArchWallBand> Bands { get; set; }
List<ArchWallTrim> Trims { get; set; }
ArchPilasterRun Pilasters { get; set; }
ArchOpeningRun OpeningRun { get; set; }
}
public static class ArchWallTreatments {
// Copied, not shared - two walls holding one list is one wall's band moving both.
public static void Dress( IArchWallTreatment source, IArchWallTreatment target ) {
target.Height = source.Height;
target.Thickness = source.Thickness;
target.Exterior = source.Exterior;
target.SingleSided = source.SingleSided;
target.Baseboard = source.Baseboard;
target.Cap = source.Cap;
target.CapHeight = source.CapHeight;
target.CapOverhang = source.CapOverhang;
target.Cornice = source.Cornice;
target.Cladding = source.Cladding;
target.Wainscot = source.Wainscot;
target.Bands = source.Bands.Select( band => band.Copy() ).ToList();
target.Trims = source.Trims.Select( trim => trim.Copy() ).ToList();
target.Pilasters = source.Pilasters.Copy();
target.OpeningRun = source.OpeningRun.Copy();
}
// The order depends on the wall height, so every caller names it.
public static IEnumerable<ArchWallBand> Ordered( IEnumerable<ArchWallBand> bands, float wallHeight ) {
return (bands ?? Enumerable.Empty<ArchWallBand>())
.Where( band => band.At( wallHeight ) > 0.05f )
.OrderBy( band => band.At( wallHeight ) );
}
// Low edge first; one clearing the plate belongs to the elevation above.
public static IEnumerable<ArchWallTrim> Standing( IEnumerable<ArchWallTrim> trims, float wallHeight ) {
return (trims ?? Enumerable.Empty<ArchWallTrim>())
.Where( trim => trim.Rise > 0.05f && trim.Depth > 0.05f )
.Where( trim => trim.At( wallHeight ) >= 0f && trim.At( wallHeight ) + trim.Rise <= wallHeight + 0.05f )
.OrderBy( trim => trim.At( wallHeight ) );
}
}
public sealed class ArchWall : IArchWallTreatment, IArchPainted {
public int Id { get; set; }
public Vector2 Start { get; set; }
public Vector2 End { get; set; }
public float Height { get; set; }
public float Thickness { get; set; }
public bool Exterior { get; set; } = true;
// WHETHER the far face exists, where Exterior only decides which SKIN each face takes. A silhouette
// filling the space between the viewer and the landmark is never entered and never seen from behind,
// so half its triangles are paying for nothing.
public bool SingleSided { get; set; }
public bool Baseboard { get; set; } = true;
public bool Cap { get; set; }
// 0 falls back to the kit's own coping section.
public float CapHeight { get; set; }
// How far the coping oversails each face - a parapet reads as brick because it stands proud of both.
public float CapOverhang { get; set; }
// The entablature the head wears, by name off the kit's shelf. Empty is a bare head; a style that stands
// finishes the wall itself, so the plain coping steps aside for it.
public string Cornice { get; set; } = "";
public WallCladding Cladding { get; set; }
public bool Wainscot { get; set; }
// Rides the SAME z split the openings make - a banded elevation costs no second wall.
public List<ArchWallBand> Bands { get; set; } = new();
// Proud strips at heights of their own, marking nothing.
public List<ArchWallTrim> Trims { get; set; } = new();
public ArchPilasterRun Pilasters { get; set; } = new();
public ArchOpeningRun OpeningRun { get; set; } = new();
public ArchPalette Palette { get; set; } = new();
public List<ArchOpening> Openings { get; set; } = new();
// Placed work at a station on this run - a pilaster where the rhythm above puts none, a recess.
public List<ArchWallModPart> Modifiers { get; set; } = new();
public float Length => (End - Start).Length;
public Vector2 Direction => Length < 0.001f ? new Vector2( 1f, 0f ) : (End - Start) / Length;
public Vector2 Normal {
get {
var direction = Direction;
return new Vector2( -direction.y, direction.x );
}
}
public Vector2 PointAt( float distance ) => Start + Direction * distance;
}