editor/AdvancedMappingTools/Core/WindowKit.cs
namespace AdvancedMappingTools;
/// <summary>
/// How the four members of a window frame meet. New kinds go at the end.
/// </summary>
public enum FrameCorner
{
[Title( "Butt Joint" )] Butt,
Mitred,
Overlapping,
[Title( "Reinforced (Gusset Plates)" )] Reinforced,
Rounded,
Chamfered,
[Title( "Decorative Corner Blocks" )] Decorative,
[Title( "Timber Joint (Pegged)" )] TimberJoint,
Welded
}
/// <summary>
/// The cross section of a frame member. New kinds go at the end.
/// </summary>
public enum FrameProfile
{
Rectangular,
Slim,
Deep,
Rounded,
Chamfered,
Bevelled,
Stepped,
Classical,
Timber,
[Title( "Steel (T Section)" )] Steel,
Aluminium,
[Title( "Multi Layer" )] MultiLayer,
Flush,
Recessed,
Projecting
}
/// <summary>
/// How much of a window is really built. New kinds go at the end.
/// </summary>
public enum WindowDetail
{
Basic,
Standard,
Detailed,
[Title( "High Detail" )] High
}
public enum Glazing
{
Single,
Double,
Triple
}
/// <summary>
/// The outline of a window. Every shape but the rectangle is framed as one seamless ring.
/// </summary>
public enum WindowShape
{
Rectangle,
[Title( "Round / Oval" )] Round,
[Title( "Half Round" )] HalfRound,
Triangle,
Octagon
}
/// <summary>
/// Everything that decides how one window is put together.
/// </summary>
public sealed class WindowSpec
{
/// <summary>Width of a frame member, seen from the front.</summary>
public float Width { get; set; } = 3.0f;
/// <summary>Depth of the frame, from its front to its back.</summary>
public float Depth { get; set; } = 4.0f;
/// <summary>Size of the moulding on the edge of the profile.</summary>
public float Edge { get; set; } = 0.75f;
/// <summary>Radius or cut of rounded and chamfered corners, size of corner blocks above the frame width.</summary>
public float CornerSize { get; set; } = 2.0f;
public FrameProfile Profile { get; set; }
public FrameCorner Corner { get; set; } = FrameCorner.Mitred;
public WindowDetail Detail { get; set; } = WindowDetail.Standard;
public bool Glass { get; set; } = true;
public Glazing Glazing { get; set; }
/// <summary>Where the glass sits in the depth of the frame, 0 at the front and 1 at the back.</summary>
public float GlassInset { get; set; } = 0.5f;
public int Columns { get; set; } = 1;
public int Rows { get; set; } = 1;
public float BarWidth { get; set; } = 1.0f;
public Material FrameMaterial { get; set; }
public Material GlassMaterial { get; set; }
/// <summary>Gaskets, bolts and other small parts. The frame material when not set.</summary>
public Material DetailMaterial { get; set; }
/// <summary>An arched head. The frame then runs as one seamless ring along the arch, whatever the corner joint.</summary>
public ArchStyle Arch { get; set; } = ArchStyle.Flat;
/// <summary>Rise of the arch above its springing line; 0 takes the usual rise of the style.</summary>
public float ArchRise { get; set; }
public int ArchSegments { get; set; } = 12;
public WindowShape Shape { get; set; } = WindowShape.Rectangle;
}
/// <summary>
/// Builds windows from parts that really fit: frame members with a profile, corners that are
/// joined and not stacked, glass that sits in a rebate, beads, gaskets and glazing bars.
/// A window is built in the XZ plane of the kit's frame: +X to the right, +Z up, its front
/// looks along -Y. The rectangle given is the hole, the frame stands inside its edges.
/// </summary>
/// <summary>
/// The pieces of a window, to build them into separate parts.
/// </summary>
[Flags]
public enum WindowPieces
{
Frame = 1,
Glass = 2,
Bars = 4,
All = Frame | Glass | Bars
}
public static class WindowKit
{
const float GasketSize = 0.12f;
const float ButtStep = 0.12f;
public static bool IsSwept( FrameCorner corner ) =>
corner is FrameCorner.Mitred or FrameCorner.Welded or FrameCorner.Rounded or FrameCorner.Chamfered;
/// <summary>
/// What <see cref="Build"/> made, for the parts a caller puts around it.
/// </summary>
public readonly record struct Result( float Width, float Depth, float GlassFront, float GlassBack );
sealed class Job
{
public MeshKit Kit;
public WindowSpec S;
public float X0, X1, Z0, Z1, Front;
public float W, D, E;
public bool Swept, Steel, Glazed, Arched, Shaped;
/// <summary>Where an arched head springs from its straight sides.</summary>
public float Spring;
public int Round;
/// <summary>Front of the inner face, front of the glass, thickness of the glass.</summary>
public float Vf, Gv, Gt;
/// <summary>Depth of the rebate the glass stands in, size of beads, in-plane size of corner blocks.</summary>
public float R, Bead, Block;
public Vector2[] Outer, Inner;
public Material Frame, Glass, Small;
}
public static Vector2[] Rect( float x0, float x1, float z0, float z1 ) =>
[new( x0, z0 ), new( x1, z0 ), new( x1, z1 ), new( x0, z1 )];
/// <summary>
/// A closed outline that runs counter clockwise seen from the front, moved inward by the same
/// distance everywhere. Corners are mitred, so a rectangle stays one.
/// </summary>
public static Vector2[] Inset( IReadOnlyList<Vector2> path, float distance )
{
var count = path.Count;
var result = new Vector2[count];
for ( var i = 0; i < count; i++ )
{
var before = (path[i] - path[(i + count - 1) % count]).Normal;
var after = (path[(i + 1) % count] - path[i]).Normal;
var n0 = new Vector2( -before.y, before.x );
var n1 = new Vector2( -after.y, after.x );
result[i] = path[i] + (n0 + n1) * (distance / MathF.Max( 1.0f + n0.Dot( n1 ), 0.3f ));
}
return result;
}
/// <summary>
/// A closed band between two outlines with the same number of points, as one body without
/// a seam. The shape is its cross section: x from the first outline (0) to the second one
/// (<paramref name="width"/>), y the depth behind <paramref name="front"/>.
/// </summary>
public static void Ring( MeshKit kit, IReadOnlyList<Vector2> from, IReadOnlyList<Vector2> to, float width, IReadOnlyList<Vector2> shape, float front )
{
var rings = new List<Vector3[]>( shape.Count );
foreach ( var p in Wound( shape ) )
{
var ring = new Vector3[from.Count];
for ( var i = 0; i < ring.Length; i++ )
{
var q = from[i] + (to[i] - from[i]) * (p.x / width);
ring[i] = new Vector3( q.x, front + p.y, q.y );
}
rings.Add( ring );
}
kit.Sweep( rings, true );
}
/// <summary>
/// A straight member with this cross section from a to b. Its x runs to the left of that way.
/// </summary>
public static void Bar( MeshKit kit, Vector2 a, Vector2 b, IReadOnlyList<Vector2> shape, float front )
{
var along = (b - a).Normal;
var inward = new Vector2( -along.y, along.x );
var rings = new List<Vector3[]>( shape.Count );
foreach ( var p in Wound( shape ) )
{
var q0 = a + inward * p.x;
var q1 = b + inward * p.x;
rings.Add( [new Vector3( q0.x, front + p.y, q0.y ), new Vector3( q1.x, front + p.y, q1.y )] );
}
kit.Sweep( rings, false );
}
/// <summary>
/// A cross section always runs the same way round, whichever way it was drawn.
/// </summary>
static IEnumerable<Vector2> Wound( IReadOnlyList<Vector2> shape )
{
var area = 0.0f;
for ( var i = 0; i < shape.Count; i++ )
{
var a = shape[i];
var b = shape[(i + 1) % shape.Count];
area += a.x * b.y - b.x * a.y;
}
return area > 0.0f ? shape.Reverse() : shape;
}
/// <summary>
/// A closed body between two rings with the same number of corners, whichever way they run.
/// </summary>
public static void Loft( MeshKit kit, IReadOnlyList<Vector3> near, IReadOnlyList<Vector3> far )
{
var normal = Vector3.Zero;
var a = Vector3.Zero;
var b = Vector3.Zero;
for ( var i = 0; i < near.Count; i++ )
{
normal += Vector3.Cross( near[i] - near[0], near[(i + 1) % near.Count] - near[0] );
a += near[i];
b += far[i];
}
if ( normal.Dot( b - a ) > 0.0f )
{
near = [.. near.Reverse()];
far = [.. far.Reverse()];
}
kit.BeginPart();
kit.Face( [.. near] );
kit.Face( [.. far.Reverse()] );
for ( var i = 0; i < near.Count; i++ )
{
var j = (i + 1) % near.Count;
kit.Face( near[i], far[i], far[j], near[j] );
}
}
static void Slab( MeshKit kit, float x0, float x1, float z0, float z1, float y0, float y1 )
{
if ( x1 - x0 < 0.01f || z1 - z0 < 0.01f || y1 - y0 < 0.01f ) return;
kit.Box( new Vector3( x0, y0, z0 ), new Vector3( x1, y1, z1 ) );
}
/// <summary>
/// The front of a profile from its outer edge (x 0) to its inner edge (x width). y is the
/// depth behind the front of the frame.
/// </summary>
public static List<Vector2> FrontLine( FrameProfile profile, float w, float e, int round )
{
var line = new List<Vector2>();
switch ( profile )
{
case FrameProfile.Rounded:
line.Add( new( 0, 0 ) );
for ( var i = 0; i <= round; i++ )
{
var t = MathF.PI * 0.5f * i / round;
line.Add( new( w - e + MathF.Sin( t ) * e, e - MathF.Cos( t ) * e ) );
}
break;
case FrameProfile.Chamfered:
line.AddRange( [new( 0, 0 ), new( w - e, 0 ), new( w, e )] );
break;
case FrameProfile.Bevelled:
line.AddRange( [new( 0, 0 ), new( w * 0.15f, 0 ), new( w, e )] );
break;
case FrameProfile.Stepped:
line.AddRange( [new( 0, 0 ), new( w - e, 0 ), new( w - e, e * 0.5f ), new( w - e * 0.5f, e * 0.5f ), new( w - e * 0.5f, e ), new( w, e )] );
break;
case FrameProfile.Classical:
{
var a = w * 0.3f;
var b = w * 0.15f;
var s = e * 0.3f;
line.AddRange( [new( 0, 0 ), new( a, 0 )] );
for ( var i = 0; i <= round; i++ )
{
var t = MathF.PI * 0.5f * i / round;
line.Add( new( a + MathF.Sin( t ) * (w - b - a), e - MathF.Cos( t ) * (e - s) ) );
}
line.Add( new( w, e ) );
break;
}
case FrameProfile.Timber:
line.AddRange( [new( 0, e * 0.5f ), new( e * 0.5f, 0 ), new( w - e * 0.5f, 0 ), new( w, e * 0.5f )] );
break;
case FrameProfile.Aluminium:
line.AddRange( [new( 0, 0 ), new( w * 0.4f, 0 ), new( w * 0.4f, e * 0.35f ), new( w * 0.55f, e * 0.35f ), new( w * 0.55f, 0 ), new( w - e * 0.25f, 0 ), new( w, e * 0.25f )] );
break;
case FrameProfile.MultiLayer:
line.AddRange( [new( 0, 0 ), new( w * 0.4f, 0 ), new( w * 0.4f, e * 0.5f ), new( w * 0.75f, e * 0.5f ), new( w * 0.75f, e ), new( w, e )] );
break;
case FrameProfile.Recessed:
line.AddRange( [new( 0, 0 ), new( w * 0.2f, 0 ), new( w * 0.2f, e ), new( w, e )] );
break;
case FrameProfile.Projecting:
line.AddRange( [new( 0, -e ), new( w * 0.5f, -e ), new( w * 0.5f, 0 ), new( w, 0 )] );
break;
default:
line.AddRange( [new( 0, 0 ), new( w, 0 )] );
break;
}
return line;
}
static Job Plan( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowSpec s )
{
var job = new Job { Kit = kit, S = s, X0 = x0, X1 = x1, Z0 = z0, Z1 = z1, Front = front };
var span = MathF.Min( x1 - x0, z1 - z0 );
job.W = MathF.Max( MathF.Min( s.Width * (s.Profile == FrameProfile.Slim ? 0.55f : 1.0f), span * 0.25f ), 0.2f );
job.D = MathF.Max( s.Depth * (s.Profile == FrameProfile.Deep ? 1.75f : s.Profile == FrameProfile.Flush ? 0.6f : 1.0f), 0.2f );
job.E = MathF.Max( MathF.Min( s.Edge, MathF.Min( job.W * 0.45f, job.D * 0.3f ) ), 0.02f );
var rise = s.Arch == ArchStyle.Flat ? 0.0f : s.ArchRise > 0.0f ? s.ArchRise : MeshKit.ArchRise( s.Arch, x1 - x0 );
// The inner line is an arch of its own, lower by the frame width: it needs some rise left.
if ( rise > 0.0f ) rise = MathF.Min( rise, z1 - z0 - job.W * 2.0f - 0.5f );
job.Shaped = s.Shape != WindowShape.Rectangle;
job.Arched = !job.Shaped && rise > job.W + 0.4f;
job.Swept = IsSwept( s.Corner ) || job.Arched || job.Shaped;
job.Steel = s.Profile == FrameProfile.Steel;
job.Glazed = s.Glass;
job.Round = s.Detail switch { WindowDetail.Basic => 2, WindowDetail.Standard => 3, WindowDetail.Detailed => 5, _ => 8 };
job.Frame = s.FrameMaterial ?? kit.Material;
job.Glass = s.GlassMaterial ?? Material.Load( "materials/glass/glass_a.vmat" );
job.Small = s.DetailMaterial ?? job.Frame;
var w = job.W;
var d = job.D;
var line = FrontLine( s.Profile, w, job.E, job.Round );
job.Vf = job.Steel ? 0.0f : line[^1].y;
job.Bead = MathF.Min( MathF.Min( 0.6f, d * 0.15f ), w * 0.5f );
job.Gt = MathF.Min( s.Glazing switch { Glazing.Single => 0.25f, Glazing.Double => 0.75f, _ => 1.25f }, d * 0.25f );
if ( job.Steel )
{
// A flange in front and a web on the outside: the glass stands behind the flange.
job.Gv = MathF.Max( MathF.Min( job.E, d * 0.25f ), 0.15f );
job.R = w - MathF.Min( job.Gv, w * 0.4f );
}
else
{
job.Gv = Math.Clamp( d * s.GlassInset, job.Vf + job.Bead, MathF.Max( d - job.Gt - d * 0.1f, job.Vf + job.Bead ) );
job.R = s.Detail == WindowDetail.Basic || !job.Swept ? 0.0f : MathF.Max( MathF.Min( w * 0.35f, 1.0f ), 0.1f );
}
job.Block = s.Corner switch
{
FrameCorner.Overlapping => w * 1.15f,
FrameCorner.Decorative => w + MathF.Min( MathF.Max( s.CornerSize, 0.0f ) * 0.5f + w * 0.2f, span * 0.1f ),
_ => w
};
if ( job.Shaped )
{
(job.Outer, job.Inner) = Outlines( s, x0, x1, z0, z1, w );
}
else if ( job.Arched )
{
// The head follows the arch. The inside is the same arch, narrower by the frame width on
// both sides and lower by it at the crown, from the same springing line. An offset line
// would cross itself under sharp crowns such as an ogee.
job.Spring = z1 - rise;
var curve = MeshKit.ArchCurve( x0, x1, job.Spring, rise, s.Arch, s.ArchSegments );
var inside = MeshKit.ArchCurve( x0 + w, x1 - w, job.Spring, rise - w, s.Arch, s.ArchSegments );
job.Outer = [new( x0, z0 ), new( x1, z0 ), .. curve.Reverse()];
job.Inner = [new( x0 + w, z0 + w ), new( x1 - w, z0 + w ), .. inside.Reverse()];
}
else if ( job.Swept )
{
var outer = Rect( x0, x1, z0, z1 );
var inner = Rect( x0 + w, x1 - w, z0 + w, z1 - w );
var size = MathF.Min( MathF.Max( s.CornerSize, 0.05f ), (span - w * 2.0f) * 0.45f );
if ( s.Corner == FrameCorner.Rounded )
{
var steps = job.Round + 1;
var o = new List<Vector2>();
var n = new List<Vector2>();
for ( var c = 0; c < 4; c++ )
{
// Centre of the arc and the angle it starts at, going round counter clockwise.
var centre = inner[c] + new Vector2( c is 0 or 3 ? size : -size, c < 2 ? size : -size );
var start = MathF.PI * (1.0f + c * 0.5f);
for ( var i = 0; i <= steps; i++ )
{
var t = start + MathF.PI * 0.5f * i / steps;
o.Add( outer[c] );
n.Add( centre + new Vector2( MathF.Cos( t ), MathF.Sin( t ) ) * size );
}
}
outer = [.. o];
inner = [.. n];
}
else if ( s.Corner == FrameCorner.Chamfered )
{
var o = new List<Vector2>();
var n = new List<Vector2>();
for ( var c = 0; c < 4; c++ )
{
var before = (inner[c] - inner[(c + 3) % 4]).Normal;
var after = (inner[(c + 1) % 4] - inner[c]).Normal;
o.AddRange( [outer[c], outer[c]] );
n.AddRange( [inner[c] - before * size, inner[c] + after * size] );
}
outer = [.. o];
inner = [.. n];
}
job.Outer = outer;
job.Inner = inner;
}
return job;
}
/// <summary>
/// The cross section of a frame member: outside at x 0, the opening at x width.
/// </summary>
static List<Vector2> Section( Job job, float? lowered = null )
{
var w = job.W;
var d = job.D;
var shape = new List<Vector2> { new( 0, d ) };
foreach ( var p in FrontLine( job.S.Profile, w, job.E, job.Round ) )
shape.Add( new( p.x, lowered is float low ? MathF.Max( p.y, MathF.Min( low, job.Gv - job.Bead ) ) : p.y ) );
if ( job.Steel )
{
shape.AddRange( [new( w, job.Gv ), new( w - job.R, job.Gv ), new( w - job.R, d )] );
}
else if ( !job.Glazed || job.R <= 0.0f )
{
shape.Add( new( w, d ) );
}
else if ( job.S.Detail == WindowDetail.Standard )
{
// The glass stands in a groove.
shape.AddRange( [new( w, job.Gv ), new( w - job.R, job.Gv ), new( w - job.R, job.Gv + job.Gt ), new( w, job.Gv + job.Gt ), new( w, d )] );
}
else
{
// A rebate, closed by a bead of its own behind the glass.
shape.AddRange( [new( w, job.Gv ), new( w - job.R, job.Gv ), new( w - job.R, d )] );
}
return MeshKit.Separate( shape );
}
static Vector2[] Sides( Job job, int side )
{
var b = job.Block;
return side switch
{
0 => [new( job.X0 + b, job.Z0 ), new( job.X1 - b, job.Z0 )],
1 => [new( job.X1, job.Z0 + b ), new( job.X1, job.Z1 - b )],
2 => [new( job.X1 - b, job.Z1 ), new( job.X0 + b, job.Z1 )],
_ => [new( job.X0, job.Z1 - b ), new( job.X0, job.Z0 + b )]
};
}
/// <summary>
/// A window in the hole from x0 to x1 and z0 to z1. Its front lies at y = front, its depth
/// runs back from there.
/// </summary>
public static Result Build( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowSpec s, WindowPieces pieces = WindowPieces.All )
{
if ( x1 - x0 < 1.0f || z1 - z0 < 1.0f ) return default;
var job = Plan( kit, x0, x1, z0, z1, front, s );
var material = kit.Material;
var w = job.W;
var d = job.D;
var detailed = s.Detail >= WindowDetail.Detailed;
kit.Material = job.Frame;
kit.BeginAssembly();
if ( (pieces & WindowPieces.Frame) == 0 )
{
}
else if ( job.Swept )
{
Ring( kit, job.Outer, job.Inner, w, Section( job ), front );
if ( job.Glazed && detailed && !job.Steel && job.R > 0.0f )
{
// The bead that holds the glass, a part of its own.
var back = MathF.Min( job.Gv + job.Gt + job.Bead, d );
Ring( kit, job.Outer, job.Inner, w, [new( w - job.R, job.Gv + job.Gt ), new( w, job.Gv + job.Gt ), new( w, back ), new( w - job.R, back )], front );
}
if ( job.Glazed && detailed && job.Steel )
{
// Putty behind the glass.
var u = w - job.R;
var size = MathF.Min( job.Bead, job.R * 0.5f );
kit.Material = job.Small;
Ring( kit, job.Outer, job.Inner, w, [new( u, job.Gv + job.Gt ), new( u + size, job.Gv + job.Gt ), new( u, job.Gv + job.Gt + size )], front );
kit.Material = job.Frame;
}
if ( s.Corner == FrameCorner.Welded && s.Detail != WindowDetail.Basic && !job.Arched && !job.Shaped )
Welds( job );
}
else
{
Blocks( job );
}
if ( job.Glazed && (pieces & WindowPieces.Glass) != 0 )
Glaze( job );
if ( (pieces & WindowPieces.Bars) != 0 )
Bars( job );
kit.EndAssembly();
kit.Material = material;
return new Result( w, d, front + job.Gv, front + job.Gv + job.Gt );
}
static void Blocks( Job job )
{
var kit = job.Kit;
var s = job.S;
var w = job.W;
var b = job.Block;
var top = FrontLine( s.Profile, w, job.E, job.Round ).Min( p => p.y );
var proud = s.Corner switch
{
FrameCorner.Overlapping => MathF.Min( job.D * 0.15f, 0.5f ),
FrameCorner.Decorative => MathF.Min( job.D * 0.2f, 0.75f ),
_ => 0.0f
};
var y0 = job.Front + top - proud;
var y1 = job.Front + job.D + (s.Corner == FrameCorner.Overlapping ? proud : 0.0f);
for ( var side = 0; side < 4; side++ )
{
var ends = Sides( job, side );
if ( (ends[1] - ends[0]).Length < 0.05f ) continue;
// On a butt joint the rails stand back a little behind the stiles that run through.
Bar( kit, ends[0], ends[1], Section( job, s.Corner == FrameCorner.Butt && side % 2 == 0 ? top + ButtStep : null ), job.Front );
}
var corners = new (float X, float Z, int Sx, int Sz)[] { (job.X0, job.Z0, 1, 1), (job.X1, job.Z0, -1, 1), (job.X1, job.Z1, -1, -1), (job.X0, job.Z1, 1, -1) };
foreach ( var (x, z, sx, sz) in corners )
{
var xa = MathF.Min( x, x + sx * b );
var za = MathF.Min( z, z + sz * b );
kit.Material = job.Frame;
Slab( kit, xa, xa + b, za, za + b, y0, y1 );
if ( s.Detail == WindowDetail.Basic ) continue;
var centre = new Vector2( xa + b * 0.5f, za + b * 0.5f );
if ( s.Corner == FrameCorner.Decorative )
{
// A rosette: a low pyramid with a button on it.
var r0 = b * 0.38f;
var r1 = b * 0.2f;
var rise = MathF.Min( b * 0.15f, 0.5f );
Loft( kit, Square( centre, r0, y0 ), Square( centre, r1, y0 - rise ) );
if ( s.Detail >= WindowDetail.Detailed )
Slab( kit, centre.x - r1 * 0.5f, centre.x + r1 * 0.5f, centre.y - r1 * 0.5f, centre.y + r1 * 0.5f, y0 - rise * 1.6f, y0 - rise );
}
else if ( s.Corner == FrameCorner.TimberJoint )
{
// Two pegs through the joint.
var peg = MathF.Min( b * 0.14f, 0.4f );
foreach ( var t in new[] { 0.3f, 0.7f } )
{
var p = new Vector2( xa + b * t, za + b * (1.0f - t) );
Slab( kit, p.x - peg, p.x + peg, p.y - peg, p.y + peg, y0 - MathF.Min( peg, 0.25f ), y0 );
}
}
else if ( s.Corner == FrameCorner.Reinforced )
{
// A gusset plate over the joint, bolted to both members.
var leg = b * 2.0f;
var plate = 0.15f;
var corner = new Vector3( x, y0, z );
var along = new Vector3( sx * leg, 0, 0 );
var up = new Vector3( 0, 0, sz * leg );
var lift = new Vector3( 0, -plate, 0 );
kit.Material = job.Small;
Loft( kit, [corner + lift, corner + along + lift, corner + up + lift], [corner, corner + along, corner + up] );
if ( s.Detail >= WindowDetail.Detailed )
{
var bolt = MathF.Min( b * 0.12f, 0.3f );
foreach ( var (u, v) in new[] { (0.5f, 0.5f), (1.35f, 0.3f), (0.3f, 1.35f) } )
{
var p = new Vector2( x + sx * b * u, z + sz * b * v );
Slab( kit, p.x - bolt, p.x + bolt, p.y - bolt, p.y + bolt, y0 - plate - bolt * 0.6f, y0 - plate );
}
}
}
}
kit.Material = job.Frame;
if ( !job.Glazed || s.Detail == WindowDetail.Basic ) return;
// Without a groove in the members the glass is held by a stop in front and a bead behind.
var clear = Clear( job );
var bead = job.Bead;
var inner = Inset( clear, bead );
Ring( kit, clear, inner, bead, [new( 0, job.Gv - bead ), new( bead, job.Gv - bead ), new( bead, job.Gv ), new( 0, job.Gv )], job.Front );
var back = MathF.Min( job.Gv + job.Gt + bead, job.D );
if ( back - job.Gv - job.Gt > 0.02f )
Ring( kit, clear, inner, bead, [new( 0, job.Gv + job.Gt ), new( bead, job.Gv + job.Gt ), new( bead, back ), new( 0, back )], job.Front );
}
static Vector3[] Square( Vector2 centre, float half, float y ) =>
[new( centre.x - half, y, centre.y - half ), new( centre.x + half, y, centre.y - half ), new( centre.x + half, y, centre.y + half ), new( centre.x - half, y, centre.y + half )];
/// <summary>
/// The outline of what stays open between the members and the corner blocks.
/// </summary>
static Vector2[] Clear( Job job )
{
var w = job.W;
var b = job.Block;
float x0 = job.X0, x1 = job.X1, z0 = job.Z0, z1 = job.Z1;
if ( job.Swept ) return MeshKit.Separate( job.Inner ).ToArray();
if ( b - w < 0.01f ) return Rect( x0 + w, x1 - w, z0 + w, z1 - w );
return
[
new( x0 + b, z0 + w ), new( x1 - b, z0 + w ), new( x1 - b, z0 + b ), new( x1 - w, z0 + b ),
new( x1 - w, z1 - b ), new( x1 - b, z1 - b ), new( x1 - b, z1 - w ), new( x0 + b, z1 - w ),
new( x0 + b, z1 - b ), new( x0 + w, z1 - b ), new( x0 + w, z0 + b ), new( x0 + b, z0 + b )
];
}
static void Welds( Job job )
{
var kit = job.Kit;
var w = job.W;
var half = MathF.Min( 0.2f, w * 0.12f );
var rise = MathF.Min( 0.12f, half );
var line = FrontLine( job.S.Profile, w, job.E, job.Round );
var corners = new (float X, float Z, int Sx, int Sz)[] { (job.X0, job.Z0, 1, 1), (job.X1, job.Z0, -1, 1), (job.X1, job.Z1, -1, -1), (job.X0, job.Z1, 1, -1) };
kit.Material = job.Small;
foreach ( var (x, z, sx, sz) in corners )
{
// A seam along the mitre, on the flat faces of the profile. To either side of the mitre the
// face belongs to the bar there, a little nearer its outer edge, so a seam only lies flat
// where the profile is level that far back.
var along = new Vector2( sx, sz );
var across = new Vector2( -sz, sx ).Normal * half;
var side = half * 0.70711f;
for ( var i = 0; i + 1 < line.Count; i++ )
{
var from = line[i];
var to = line[i + 1];
if ( MathF.Abs( to.y - from.y ) > 0.001f || to.x - from.x - side < 0.1f ) continue;
var a = new Vector2( x, z ) + along * (from.x + side);
var b = new Vector2( x, z ) + along * to.x;
var y = job.Front + from.y;
Loft( kit,
[P( a - across, y - rise ), P( b - across, y - rise ), P( b + across, y - rise ), P( a + across, y - rise )],
[P( a - across, y ), P( b - across, y ), P( b + across, y ), P( a + across, y )] );
}
}
kit.Material = job.Frame;
}
static Vector3 P( Vector2 p, float y ) => new( p.x, y, p.y );
static void Glaze( Job job )
{
var kit = job.Kit;
var s = job.S;
var w = job.W;
var y0 = job.Front + job.Gv;
var y1 = y0 + job.Gt;
var panes = s.Glazing switch { Glazing.Single => 1, Glazing.Double => 2, _ => 3 };
var split = job.Swept && s.Detail >= WindowDetail.Detailed && panes > 1 && job.Gt > 0.5f;
kit.Material = job.Glass;
if ( job.Swept )
{
var u = (w - job.R) / w;
var outline = MeshKit.Separate( job.Outer.Select( ( o, i ) => o + (job.Inner[i] - o) * u ) );
if ( split )
{
// Real panes with the gap between them.
var pane = job.Gt / (panes * 2 - 1);
for ( var i = 0; i < panes; i++ )
kit.Profile( outline, y0 + pane * i * 2, y0 + pane * (i * 2 + 1) );
kit.Material = job.Small;
for ( var i = 0; i + 1 < panes; i++ )
{
var from = job.Gv + pane * (i * 2 + 1);
Ring( kit, job.Outer, job.Inner, w, [new( w - job.R, from ), new( w + GasketSize, from ), new( w + GasketSize, from + pane ), new( w - job.R, from + pane )], job.Front );
}
}
else
{
kit.Profile( outline, y0, y1 );
}
if ( s.Detail >= WindowDetail.Detailed && s.Columns * s.Rows <= 1 && job.Gv - job.Vf > GasketSize * 2.0f )
{
// The gasket between the frame and the glass.
var clear = Clear( job );
kit.Material = job.Small;
Ring( kit, clear, Inset( clear, GasketSize ), GasketSize, [new( 0, job.Gv - GasketSize ), new( GasketSize, job.Gv - GasketSize ), new( GasketSize, job.Gv ), new( 0, job.Gv )], job.Front );
}
}
else
{
var b = job.Block;
float x0 = job.X0, x1 = job.X1, z0 = job.Z0, z1 = job.Z1;
// One pane, cut out around the corner blocks. Its pieces meet on whole faces and become one body.
Slab( kit, x0 + b, x1 - b, z0 + b, z1 - b, y0, y1 );
Slab( kit, x0 + w, x0 + b, z0 + b, z1 - b, y0, y1 );
Slab( kit, x1 - b, x1 - w, z0 + b, z1 - b, y0, y1 );
Slab( kit, x0 + b, x1 - b, z0 + w, z0 + b, y0, y1 );
Slab( kit, x0 + b, x1 - b, z1 - b, z1 - w, y0, y1 );
}
kit.Material = job.Frame;
}
/// <summary>
/// Glazing bars. The upright ones run through, the level ones stand between them.
/// </summary>
static void Bars( Job job )
{
var s = job.S;
var columns = Math.Max( s.Columns, 1 );
var rows = Math.Max( s.Rows, 1 );
if ( columns * rows <= 1 ) return;
var kit = job.Kit;
var beads = !job.Swept && job.Glazed && s.Detail != WindowDetail.Basic;
var edge = job.W + (beads ? job.Bead : 0.0f);
float x0 = job.X0 + edge, x1 = job.X1 - edge, z0 = job.Z0 + edge, z1 = job.Z1 - edge;
var half = MathF.Min( s.BarWidth, MathF.Min( (x1 - x0) / columns, (z1 - z0) / rows ) * 0.4f ) * 0.5f;
if ( half < 0.05f ) return;
var layers = new List<(float From, float To)>();
var depth = beads ? job.Bead : MathF.Min( job.Bead, job.Gv - job.Vf );
// Under an arch the level bars stay in the straight part, the upright ones end below the curve.
var straight = job.Arched ? MathF.Min( job.Spring, z1 ) : z1;
if ( job.Glazed )
{
if ( depth > 0.05f ) layers.Add( (job.Gv - depth, job.Gv) );
if ( !job.Steel && job.D - job.Gv - job.Gt > 0.05f ) layers.Add( (job.Gv + job.Gt, MathF.Min( job.Gv + job.Gt + job.Bead, job.D )) );
}
else
{
layers.Add( (MathF.Max( job.Vf, job.D * 0.5f - job.Bead ), MathF.Min( job.D, job.D * 0.5f + job.Bead )) );
}
kit.Material = job.Frame;
foreach ( var (from, to) in layers )
{
var y0 = job.Front + from;
var y1 = job.Front + to;
var cuts = new List<float> { x0 };
for ( var c = 1; c < columns; c++ )
{
var x = x0 + (x1 - x0) * c / columns;
var top = job.Arched || job.Shaped ? MathF.Min( Under( job.Inner, x - half ), Under( job.Inner, x + half ) ) - 0.02f : z1;
var bottom = job.Shaped ? MathF.Max( Across( job.Inner, x - half, true ).Low, Across( job.Inner, x + half, true ).Low ) + 0.02f : z0;
Slab( kit, x - half, x + half, bottom, top, y0, y1 );
cuts.Add( x - half );
cuts.Add( x + half );
}
cuts.Add( x1 );
for ( var r = 1; r < rows; r++ )
{
var z = z0 + (straight - z0) * r / rows;
var left = x0;
var right = x1;
if ( job.Shaped )
{
// A level bar ends where the outline crosses it, at both its edges.
var below = Across( job.Inner, z - half, false );
var above = Across( job.Inner, z + half, false );
left = MathF.Max( below.Low, above.Low ) + 0.02f;
right = MathF.Min( below.High, above.High ) - 0.02f;
}
for ( var i = 0; i + 1 < cuts.Count; i += 2 )
Slab( kit, MathF.Max( cuts[i], left ), MathF.Min( cuts[i + 1], right ), z - half, z + half, y0, y1 );
}
}
}
/// <summary>
/// Where a straight line crosses a closed outline: upright through x (<paramref name="upright"/>)
/// or level through z. The lowest and highest crossing.
/// </summary>
static (float Low, float High) Across( IReadOnlyList<Vector2> outline, float at, bool upright )
{
var low = float.MaxValue;
var high = float.MinValue;
for ( var i = 0; i < outline.Count; i++ )
{
var a = outline[i];
var b = outline[(i + 1) % outline.Count];
var (ka, kb, va, vb) = upright ? (a.x, b.x, a.y, b.y) : (a.y, b.y, a.x, b.x);
if ( (at < ka || at > kb) && (at < kb || at > ka) || MathF.Abs( kb - ka ) < 0.0001f ) continue;
var v = va + (vb - va) * (at - ka) / (kb - ka);
low = MathF.Min( low, v );
high = MathF.Max( high, v );
}
return low > high ? (0.0f, 0.0f) : (low, high);
}
/// <summary>
/// Outer and inner outline of a shaped window in the hole x0..x1, z0..z1, with the same number
/// of points, the inner one the frame width inside.
/// </summary>
static (Vector2[] Outer, Vector2[] Inner) Outlines( WindowSpec s, float x0, float x1, float z0, float z1, float w )
{
var cx = (x0 + x1) * 0.5f;
var rx = (x1 - x0) * 0.5f;
var steps = Math.Clamp( s.ArchSegments, 6, 32 ) * 2;
switch ( s.Shape )
{
case WindowShape.Round:
{
// An ellipse filling the hole; a circle in a square one.
var cz = (z0 + z1) * 0.5f;
var rz = (z1 - z0) * 0.5f;
Vector2[] Ellipse( float a, float b ) => [.. Enumerable.Range( 0, steps * 2 ).Select( i => new Vector2( cx + MathF.Cos( MathF.PI * i / steps ) * a, cz + MathF.Sin( MathF.PI * i / steps ) * b ) )];
return (Ellipse( rx, rz ), Ellipse( rx - w, rz - w ));
}
case WindowShape.HalfRound:
{
// A flat sill and a half ellipse over it.
var rz = z1 - z0;
var a = rx - w;
var bz = rz - w;
var start = MathF.Asin( Math.Clamp( w / MathF.Max( bz, 0.01f ), 0.0f, 0.99f ) );
var outer = new List<Vector2>();
var inner = new List<Vector2>();
for ( var i = 0; i <= steps; i++ )
{
var t = MathF.PI * i / steps;
outer.Add( new Vector2( cx + MathF.Cos( t ) * rx, z0 + MathF.Sin( t ) * rz ) );
var u = start + (MathF.PI - start * 2.0f) * i / steps;
inner.Add( new Vector2( cx + MathF.Cos( u ) * a, z0 + MathF.Sin( u ) * bz ) );
}
return ([.. outer], [.. inner]);
}
case WindowShape.Triangle:
{
Vector2[] outer = [new( x0, z0 ), new( x1, z0 ), new( cx, z1 )];
return (outer, Inset( outer, w ));
}
default:
{
var c = MathF.Min( x1 - x0, z1 - z0 ) * 0.29f;
Vector2[] outer = [new( x0 + c, z0 ), new( x1 - c, z0 ), new( x1, z0 + c ), new( x1, z1 - c ), new( x1 - c, z1 ), new( x0 + c, z1 ), new( x0, z1 - c ), new( x0, z0 + c )];
return (outer, Inset( outer, w ));
}
}
}
/// <summary>
/// The highest point of a closed outline straight above x.
/// </summary>
static float Under( IReadOnlyList<Vector2> outline, float x )
{
var best = float.MinValue;
for ( var i = 0; i < outline.Count; i++ )
{
var a = outline[i];
var b = outline[(i + 1) % outline.Count];
if ( (x < a.x || x > b.x) && (x < b.x || x > a.x) || MathF.Abs( b.x - a.x ) < 0.0001f ) continue;
best = MathF.Max( best, a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x) );
}
return best;
}
/// <summary>
/// Sheet metal laid on the surface around a hole, thin at its outer edge and standing up
/// against the frame. The surface lies at y = surface, the sheet on its front.
/// </summary>
public static void Flashing( MeshKit kit, float x0, float x1, float z0, float z1, float surface, float width, float apron, float upstand )
{
if ( width <= 0.05f ) return;
var hole = Rect( x0, x1, z0, z1 );
var outer = new Vector2[] { new( x0 - width, z0 - width - apron ), new( x1 + width, z0 - width - apron ), new( x1 + width, z1 + width ), new( x0 - width, z1 + width ) };
Ring( kit, outer, hole, 1.0f, [new( 0, 0 ), new( 0, -0.08f ), new( 0.8f, -0.12f ), new( 1, -MathF.Max( upstand, 0.15f ) ), new( 1, 0 )], surface );
}
/// <summary>
/// The lining of a hole behind a window: four boards with mitred corners, from y0 back to y1.
/// </summary>
public static void Lining( MeshKit kit, float x0, float x1, float z0, float z1, float y0, float y1, float thickness )
{
if ( y1 - y0 < 0.05f || thickness <= 0.02f ) return;
var hole = Rect( x0, x1, z0, z1 );
Ring( kit, hole, Inset( hole, thickness ), thickness, [new( 0, y0 ), new( thickness, y0 ), new( thickness, y1 ), new( 0, y1 )], 0.0f );
}
/// <summary>
/// A sill under an opening: its top falls to the front, it reaches past the opening on both
/// sides by <paramref name="horn"/>. Its back lies at y = back, its top edge there at z.
/// </summary>
public static void Sill( MeshKit kit, float x0, float x1, float z, float back, float projection, float thickness, float horn )
{
if ( projection <= 0.05f || thickness <= 0.05f ) return;
var fall = MathF.Min( thickness * 0.3f, projection * 0.2f );
var nose = MathF.Min( thickness * 0.35f, 0.4f );
Vector3[] End( float x ) =>
[
new( x, back, z ), new( x, back - projection, z - fall ), new( x, back - projection, z - thickness ),
new( x, back - projection + nose, z - thickness ), new( x, back - projection + nose, z - thickness + nose * 0.5f ), new( x, back, z - thickness + nose * 0.5f )
];
Loft( kit, End( x0 - horn ), End( x1 + horn ) );
}
}