Editor-side geometry builder for Advanced Mapping Tools. Defines the window and door spec types (WindowType, WindowUnitSpec, DoorStyle, DoorSpec) and a static WindowUnit class that builds procedural frames, sashes, louvres, glass blocks, folding panels, hinges, handles, and several door kinds into a MeshKit.
namespace AdvancedMappingTools;
/// <summary>
/// How a window opens, or what it is made of.
/// </summary>
public enum WindowType
{
[Title( "Fixed / Picture" )] Fixed,
[Title( "Casement (Side Hinged)" )] Casement,
[Title( "Double Casement (French)" )] DoubleCasement,
[Title( "Single Hung" )] SingleHung,
[Title( "Double Hung" )] DoubleHung,
[Title( "Awning (Top Hinged)" )] Awning,
[Title( "Hopper (Bottom Hinged)" )] Hopper,
[Title( "Sliding" )] Sliding,
[Title( "Jalousie (Louvres)" )] Jalousie,
[Title( "Glass Block" )] GlassBlock,
[Title( "Accordion (Folding)" )] Accordion,
[Title( "Palladian" )] Palladian,
[Title( "Pivot, Horizontal Axis" )] PivotHorizontal,
[Title( "Pivot, Vertical Axis" )] PivotVertical
}
/// <summary>
/// One window unit: its type, how far it is open, and the look of its frames.
/// </summary>
public sealed class WindowUnitSpec
{
public WindowType Type { get; set; } = WindowType.Fixed;
/// <summary>0 closed, 1 fully open.</summary>
public float Open { get; set; }
/// <summary>Look of the frame and the sashes: corner, profile, detail, glazing, panes, materials.</summary>
public WindowSpec Look { get; set; } = new();
/// <summary>Handles, hinges and locks.</summary>
public bool Hardware { get; set; } = true;
public Material HardwareMaterial { get; set; }
/// <summary>A fixed light above the window, as a share of its height; 0 for none.</summary>
public float Transom { get; set; }
/// <summary>A thin storm sash in front of the window.</summary>
public bool Storm { get; set; }
/// <summary>Louvres of a jalousie; 0 picks a number from the height.</summary>
public int Slats { get; set; }
/// <summary>Panels of a folding window.</summary>
public int Panels { get; set; } = 4;
/// <summary>Size of one glass block.</summary>
public float BlockSize { get; set; } = 8;
}
/// <summary>
/// How a door leaf is made.
/// </summary>
public enum DoorStyle
{
[Title( "Panelled" )] Panelled,
[Title( "Plain" )] Plain,
[Title( "Glazed" )] Glazed
}
/// <summary>
/// A door in a profiled frame: one or two leaves opening inward, with handles.
/// </summary>
public sealed class DoorSpec
{
public WindowSpec Look { get; set; } = new();
public DoorStyle Style { get; set; } = DoorStyle.Panelled;
public int Leaves { get; set; } = 1;
/// <summary>0 closed, 1 open about 85 degrees, inward.</summary>
public float Open { get; set; }
public bool Hardware { get; set; } = true;
public Material LeafMaterial { get; set; }
public Material HardwareMaterial { get; set; }
/// <summary>Hinged leaves, a sliding leaf on a rail inside, a sectional garage door, or barn leaves on a rail outside.</summary>
public DoorKind Kind { get; set; } = DoorKind.Hinged;
/// <summary>How far the wall faces lie inside the frame's front and back: rails are fixed to the wall there.</summary>
public float Mount { get; set; }
/// <summary>
/// When set, hinged leaves are not built into the kit but handed out here, each in the frame of
/// its hinge with the way it turns open, so they can become objects that open in the game.
/// </summary>
public List<(Transform Hinge, PolygonMesh Mesh, float Turn)> SeparateLeaves { get; set; }
}
/// <summary>
/// Whole windows from <see cref="WindowKit"/>: a fixed frame with sashes that really open,
/// louvres, glass blocks, folding panels, transom lights, storm sashes and Palladian windows.
/// Same frame as WindowKit: the hole x0..x1, z0..z1, the front at y = front, depth behind it.
/// Every moving piece is placed so that it cuts into nothing at any opening.
/// </summary>
public static class WindowUnit
{
public static WindowSpec Copy( WindowSpec s ) => new()
{
Width = s.Width, Depth = s.Depth, Edge = s.Edge, CornerSize = s.CornerSize, Profile = s.Profile, Corner = s.Corner,
Detail = s.Detail, Glass = s.Glass, Glazing = s.Glazing, GlassInset = s.GlassInset, Columns = s.Columns, Rows = s.Rows,
BarWidth = s.BarWidth, FrameMaterial = s.FrameMaterial, GlassMaterial = s.GlassMaterial, DetailMaterial = s.DetailMaterial,
Arch = s.Arch, ArchRise = s.ArchRise, ArchSegments = s.ArchSegments, Shape = s.Shape
};
/// <summary>
/// The outline of the hole a unit needs, counter clockwise seen from the front.
/// </summary>
public static Vector2[] Outline( float x0, float x1, float z0, float z1, WindowUnitSpec u )
{
var s = u.Look;
if ( u.Type == WindowType.Palladian )
{
var (a, b, spring) = Palladian( x0, x1, z0, z1 );
var curve = MeshKit.ArchCurve( a, b, spring, z1 - spring, ArchStyle.Round, s.ArchSegments );
return [new( x0, z0 ), new( x1, z0 ), new( x1, spring ), .. curve.Reverse(), new( x0, spring )];
}
if ( s.Shape == WindowShape.Round )
{
var steps = Math.Clamp( s.ArchSegments, 6, 32 ) * 4;
return [.. Enumerable.Range( 0, steps ).Select( i => new Vector2( (x0 + x1) * 0.5f + MathF.Cos( MathF.PI * 2 * i / steps ) * (x1 - x0) * 0.5f, (z0 + z1) * 0.5f + MathF.Sin( MathF.PI * 2 * i / steps ) * (z1 - z0) * 0.5f ) )];
}
if ( s.Shape == WindowShape.HalfRound )
{
var steps = Math.Clamp( s.ArchSegments, 6, 32 ) * 2;
return [.. Enumerable.Range( 0, steps + 1 ).Select( i => new Vector2( (x0 + x1) * 0.5f + MathF.Cos( MathF.PI * i / steps ) * (x1 - x0) * 0.5f, z0 + MathF.Sin( MathF.PI * i / steps ) * (z1 - z0) ) )];
}
if ( s.Shape == WindowShape.Triangle )
return [new( x0, z0 ), new( x1, z0 ), new( (x0 + x1) * 0.5f, z1 )];
if ( s.Shape == WindowShape.Octagon )
{
var c = MathF.Min( x1 - x0, z1 - z0 ) * 0.29f;
return [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 )];
}
var rise = s.Arch == ArchStyle.Flat ? 0.0f : s.ArchRise > 0.0f ? s.ArchRise : MeshKit.ArchRise( s.Arch, x1 - x0 );
if ( rise > 0.5f && rise < z1 - z0 - 1.0f )
return [new( x0, z0 ), new( x1, z0 ), .. MeshKit.ArchCurve( x0, x1, z1 - rise, rise, s.Arch, s.ArchSegments ).Reverse()];
return WindowKit.Rect( x0, x1, z0, z1 );
}
static (float A, float B, float Spring) Palladian( float x0, float x1, float z0, float z1 )
{
var width = x1 - x0;
var a = x0 + width * 0.25f;
var b = x1 - width * 0.25f;
var spring = MathF.Max( z1 - (b - a) * 0.5f, z0 + (z1 - z0) * 0.35f );
return (a, b, spring);
}
public static void Build( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowUnitSpec u )
{
if ( x1 - x0 < 2.0f || z1 - z0 < 2.0f ) return;
var look = u.Look;
var material = kit.Material;
kit.BeginAssembly();
// A storm sash: outside, clear of hinges and pegs; inside where the sashes open outward;
// none where a pivot sash turns through both sides.
var outward = u.Type is WindowType.Casement or WindowType.DoubleCasement or WindowType.Awning or WindowType.Accordion;
if ( u.Storm && u.Type is not (WindowType.PivotHorizontal or WindowType.PivotVertical) )
{
var storm = Copy( look );
storm.Width = MathF.Min( 1.2f, look.Width );
storm.Depth = 1.0f;
storm.Profile = FrameProfile.Aluminium;
storm.Corner = FrameCorner.Mitred;
storm.Glazing = Glazing.Single;
storm.Columns = 1;
storm.Rows = storm.Shape == WindowShape.Rectangle && storm.Arch == ArchStyle.Flat && (z1 - z0) > (x1 - x0) * 1.2f ? 2 : 1;
storm.Detail = WindowDetail.Standard;
WindowKit.Build( kit, x0, x1, z0, z1, outward ? front + MathF.Max( look.Depth, 3.8f ) + 0.3f : front - 2.6f, storm );
}
var shaped = look.Shape != WindowShape.Rectangle || look.Arch != ArchStyle.Flat;
var type = shaped && u.Type != WindowType.Palladian ? WindowType.Fixed : u.Type;
if ( type == WindowType.Palladian )
{
var (a, b, spring) = Palladian( x0, x1, z0, z1 );
var side = Copy( look );
side.Shape = WindowShape.Rectangle;
side.Arch = ArchStyle.Flat;
var centre = Copy( side );
centre.Arch = ArchStyle.Round;
centre.ArchRise = z1 - spring;
WindowKit.Build( kit, x0, a, z0, spring, front, side );
WindowKit.Build( kit, a, b, z0, z1, front, centre );
WindowKit.Build( kit, b, x1, z0, spring, front, side );
}
else if ( u.Transom > 0.05f && type != WindowType.Fixed )
{
// A fixed light over the opening part, both in frames that meet on the transom.
var split = z1 - (z1 - z0) * Math.Clamp( u.Transom, 0.1f, 0.6f );
var light = Copy( look );
light.Columns = Math.Max( look.Columns, 1 );
light.Rows = 1;
WindowKit.Build( kit, x0, x1, split, z1, front, light );
Unit( kit, x0, x1, z0, split, front, u, type );
}
else
{
Unit( kit, x0, x1, z0, z1, front, u, type );
}
kit.EndAssembly();
kit.Material = material;
}
/// <summary>One window of the given type in the hole.</summary>
static void Unit( MeshKit kit, float x0, float x1, float z0, float z1, float front, WindowUnitSpec u, WindowType type )
{
var look = u.Look;
if ( type == WindowType.Fixed )
{
WindowKit.Build( kit, x0, x1, z0, z1, front, look );
return;
}
// The box frame: profiled like the look, without glass; the sashes or the infill go inside.
var box = Copy( look );
box.Glass = false;
box.Columns = 1;
box.Rows = 1;
// The sashes fill a square opening: corners that round it off or reach into it become plain
// joints in the frame around opening sashes; the sashes keep the look.
box.Corner = look.Corner switch
{
FrameCorner.Rounded or FrameCorner.Chamfered => FrameCorner.Mitred,
FrameCorner.Overlapping or FrameCorner.Decorative => FrameCorner.Butt,
_ => look.Corner
};
var frame = WindowKit.Build( kit, x0, x1, z0, z1, front, box );
var w = frame.Width;
var d = frame.Depth;
float ix0 = x0 + w, ix1 = x1 - w, iz0 = z0 + w, iz1 = z1 - w;
if ( ix1 - ix0 < 2.0f || iz1 - iz0 < 2.0f ) return;
var open = Math.Clamp( u.Open, 0.0f, 1.0f );
var sash = Copy( look );
sash.Width = MathF.Max( MathF.Min( w * 0.8f, MathF.Min( ix1 - ix0, iz1 - iz0 ) * 0.2f ), 0.4f );
sash.Shape = WindowShape.Rectangle;
sash.Arch = ArchStyle.Flat;
sash.Corner = WindowKit.IsSwept( look.Corner ) ? look.Corner : FrameCorner.Mitred;
var hardware = u.HardwareMaterial ?? look.DetailMaterial ?? look.FrameMaterial ?? kit.Material;
var origin = kit.Frame;
switch ( type )
{
case WindowType.Casement:
case WindowType.DoubleCasement:
{
var sd = MathF.Min( d * 0.6f, 2.5f );
sash.Depth = sd;
var angle = open * 85.0f;
var leaves = type == WindowType.DoubleCasement ? 2 : 1;
var span = (ix1 - ix0) / leaves;
// A leaf turning about its outer front edge sweeps a little past its width on the far side.
var gap = sd * sd / (2.0f * span) + 0.05f;
for ( var leaf = 0; leaf < leaves; leaf++ )
{
var left = leaf == 0;
var pivot = new Vector3( left ? ix0 : ix1, front, 0 );
kit.Frame = origin.ToWorld( new Transform( pivot, Rotation.FromYaw( left ? -angle : angle ) ) );
var l0 = left ? 0.0f : -(span - gap);
var l1 = left ? span - gap : 0.0f;
WindowKit.Build( kit, l0, l1, iz0, iz1, 0, sash );
if ( u.Hardware )
{
kit.Material = hardware;
var x = left ? l1 - sash.Width * 0.5f : l0 + sash.Width * 0.5f;
Handle( kit, x, (iz0 + iz1) * 0.5f, sd, left ? 1 : -1 );
}
kit.Frame = origin;
if ( u.Hardware )
{
kit.Material = hardware;
Hinges( kit, left ? ix0 - 0.5f : ix1, left ? ix0 : ix1 + 0.5f, iz0, iz1, front );
}
}
break;
}
case WindowType.Awning:
case WindowType.Hopper:
{
var sd = MathF.Min( d * 0.6f, 2.5f );
sash.Depth = sd;
var height = iz1 - iz0;
var gap = sd * sd / (2.0f * height) + 0.05f;
var angle = open * (type == WindowType.Awning ? 60.0f : 35.0f) * MathF.PI / 180.0f;
var up = new Vector3( 0, MathF.Sin( angle ), MathF.Cos( angle ) );
if ( type == WindowType.Awning )
{
// Hinged at the head, the foot swings out.
kit.Frame = origin.ToWorld( new Transform( new Vector3( 0, front, iz1 ), Rotation.LookAt( Vector3.Forward, up ) ) );
WindowKit.Build( kit, ix0, ix1, -(height - gap), 0, 0, sash );
if ( u.Hardware )
{
kit.Material = hardware;
Handle( kit, (ix0 + ix1) * 0.5f, -(height - gap) + sash.Width * 0.5f, sd, 0 );
}
}
else
{
// Hinged at the sill, the head tips in.
kit.Frame = origin.ToWorld( new Transform( new Vector3( 0, front + sd, iz0 ), Rotation.LookAt( Vector3.Forward, up ) ) );
WindowKit.Build( kit, ix0, ix1, 0, height - gap, -sd, sash );
if ( u.Hardware )
{
kit.Material = hardware;
Handle( kit, (ix0 + ix1) * 0.5f, height - gap - sash.Width * 0.5f, 0, 0 );
}
}
kit.Frame = origin;
break;
}
case WindowType.SingleHung:
case WindowType.DoubleHung:
case WindowType.Sliding:
{
// Two sashes in two tracks, overlapping at the meeting rail.
var sd = MathF.Min( (d - 0.1f) * 0.5f, 2.5f );
if ( sd < 0.3f ) { WindowKit.Build( kit, ix0, ix1, iz0, iz1, front, look ); break; }
sash.Depth = sd;
var outer = front + (d - sd * 2.0f - 0.05f) * 0.5f;
var inner = outer + sd + 0.05f;
var sliding = type == WindowType.Sliding;
var length = sliding ? ix1 - ix0 : iz1 - iz0;
var lap = sash.Width;
var piece = length * 0.5f + lap * 0.5f;
var travel = (length - piece) * open;
var fixedMove = type == WindowType.DoubleHung ? travel * 0.5f : 0.0f;
var moving = type == WindowType.DoubleHung ? travel * 0.5f : travel;
if ( sliding )
{
WindowKit.Build( kit, ix0, ix0 + piece, iz0, iz1, outer, sash );
WindowKit.Build( kit, ix1 - piece - moving, ix1 - moving, iz0, iz1, inner, sash );
if ( u.Hardware )
{
kit.Material = hardware;
Handle( kit, ix1 - piece - moving + sash.Width * 0.5f, (iz0 + iz1) * 0.5f, inner + sd, 1 );
}
}
else
{
// Upper sash in the outer track, lower sash in the inner one.
WindowKit.Build( kit, ix0, ix1, iz1 - piece - fixedMove, iz1 - fixedMove, outer, sash );
WindowKit.Build( kit, ix0, ix1, iz0 + moving, iz0 + piece + moving, inner, sash );
if ( u.Hardware )
{
kit.Material = hardware;
var rail = iz0 + piece + moving - sash.Width * 0.5f;
kit.BeginPart();
kit.Box( new Vector3( (ix0 + ix1) * 0.5f - 1.0f, inner + sd, rail - 0.4f ), new Vector3( (ix0 + ix1) * 0.5f + 1.0f, inner + sd + 0.4f, rail + 0.4f ) );
}
}
break;
}
case WindowType.PivotHorizontal:
case WindowType.PivotVertical:
{
// One sash turning about an axis through its middle: half goes out, half comes in.
var sd = MathF.Min( d * 0.6f, 2.5f );
sash.Depth = sd;
var level = type == WindowType.PivotHorizontal;
var half = (level ? iz1 - iz0 : ix1 - ix0) * 0.5f;
// The corners sweep a little past the half length.
var gap = MathF.Sqrt( half * half + sd * sd * 0.25f ) - half + 0.05f;
var angle = open * 80.0f;
var middle = new Vector3( (ix0 + ix1) * 0.5f, front + d * 0.5f, (iz0 + iz1) * 0.5f );
var turn = level
? Rotation.LookAt( Vector3.Forward, new Vector3( 0, MathF.Sin( angle * MathF.PI / 180.0f ), MathF.Cos( angle * MathF.PI / 180.0f ) ) )
: Rotation.FromYaw( angle );
kit.Frame = origin.ToWorld( new Transform( middle, turn ) );
if ( level )
WindowKit.Build( kit, ix0 - middle.x, ix1 - middle.x, -(half - gap), half - gap, -sd * 0.5f, sash );
else
WindowKit.Build( kit, -(half - gap), half - gap, iz0 - middle.z, iz1 - middle.z, -sd * 0.5f, sash );
if ( u.Hardware )
{
kit.Material = hardware;
if ( level )
Handle( kit, 0, -(half - gap) + sash.Width * 0.5f, sd * 0.5f, 0 );
else
Handle( kit, half - gap - sash.Width * 0.5f, 0, sd * 0.5f, 1 );
}
kit.Frame = origin;
break;
}
case WindowType.Jalousie:
Jalousie( kit, ix0, ix1, iz0, iz1, front, d, u, open, hardware );
break;
case WindowType.GlassBlock:
Blocks( kit, ix0, ix1, iz0, iz1, front, d, u );
break;
case WindowType.Accordion:
Accordion( kit, ix0, ix1, iz0, iz1, front, d, u, open, sash, hardware );
break;
}
kit.Frame = origin;
}
/// <summary>
/// A door in the hole x0..x1, z0..z1: a profiled frame with a threshold, and leaves of the chosen
/// kind. Hinged leaves open inward behind the frame's back face, sliding leaves run on a rail on the
/// inside wall, barn leaves on a rail outside, a garage door rolls its sections up into the room.
/// Nothing touches the frame at any opening.
/// </summary>
public static void BuildDoor( MeshKit kit, float x0, float x1, float z0, float z1, float front, DoorSpec door )
{
if ( x1 - x0 < 4.0f || z1 - z0 < 4.0f ) return;
var look = door.Look;
var material = kit.Material;
kit.BeginAssembly();
var box = Copy( look );
box.Glass = false;
box.Columns = 1;
box.Rows = 1;
box.Shape = WindowShape.Rectangle;
box.Arch = ArchStyle.Flat;
box.Corner = look.Corner switch
{
FrameCorner.Rounded or FrameCorner.Chamfered => FrameCorner.Mitred,
FrameCorner.Overlapping or FrameCorner.Decorative => FrameCorner.Butt,
_ => look.Corner
};
var frame = WindowKit.Build( kit, x0, x1, z0, z1, front, box );
var w = frame.Width;
var d = frame.Depth;
float ix0 = x0 + w, ix1 = x1 - w, iz0 = z0 + w, iz1 = z1 - w;
if ( ix1 - ix0 < 2.0f || iz1 - iz0 < 2.0f ) { kit.EndAssembly(); kit.Material = material; return; }
var open = Math.Clamp( door.Open, 0.0f, 1.0f );
var thick = MathF.Max( MathF.Min( 2.0f, d * 0.5f ), 0.3f );
var hardware = door.HardwareMaterial ?? look.DetailMaterial ?? look.FrameMaterial ?? kit.Material;
var leafMaterial = door.LeafMaterial ?? look.FrameMaterial ?? kit.Material;
var back = front + d;
var mount = Math.Clamp( door.Mount, 0.0f, d * 0.5f );
var origin = kit.Frame;
switch ( door.Kind )
{
case DoorKind.Sliding:
case DoorKind.Barn:
{
// Leaves hang from a rail fixed to the wall above the opening and slide along it,
// inside the room for a sliding door, outside for a barn door.
var inside = door.Kind == DoorKind.Sliding;
var leaves = Math.Clamp( door.Leaves, 1, 2 );
var lap = 2.0f;
var width = (ix1 - ix0) / leaves + lap;
var face = inside ? back + 0.1f : front - 0.1f;
var y0 = inside ? face : face - thick;
var y1 = inside ? face + thick : face;
var top = z1 + 1.0f;
for ( var leaf = 0; leaf < leaves; leaf++ )
{
var left = leaf == 0;
var travel = open * width;
var a = left ? ix0 - lap - travel : (ix0 + ix1) * 0.5f + travel;
var b = a + width;
if ( leaves == 1 ) { a = ix0 - lap - travel; b = ix1 + lap - travel; }
kit.Material = leafMaterial;
Plank( kit, a, b, z0 + 0.5f, top, y0, y1, door.Style, inside ? 1.0f : -1.0f );
if ( door.Hardware )
{
kit.Material = hardware;
var x = left ? b - 3.0f : a + 3.0f;
kit.BeginPart();
var yh = inside ? y1 : y0 - 0.8f;
kit.Box( new Vector3( x - 0.4f, yh, z0 + 34.0f ), new Vector3( x + 0.4f, yh + 0.8f, z0 + 46.0f ) );
}
}
// The rail, on the wall over the opening, reaching as far as the leaves can go.
var reach = (ix1 - ix0) / leaves + lap * 2.0f;
var r0 = ix0 - lap - reach;
var r1 = leaves == 1 ? ix1 + lap : ix1 + lap + reach;
kit.Material = hardware;
kit.BeginPart();
if ( inside )
kit.Box( new Vector3( r0, back - mount, top ), new Vector3( r1, y1 + 0.6f, top + 3.0f ) );
else
kit.Box( new Vector3( r0, y0 - 0.6f, top ), new Vector3( r1, front + mount, top + 3.0f ) );
break;
}
case DoorKind.Garage:
{
// Sections run up the opening and turn back into the room above it.
var height = iz1 - iz0;
var count = Math.Clamp( (int)MathF.Round( height / 20.0f ), 3, 12 );
var section = height / count;
var offset = open * height;
var middle = front + (d - thick) * 0.5f;
var stacked = 0;
kit.Material = leafMaterial;
for ( var i = count - 1; i >= 0; i-- )
{
var bottom = iz0 + offset + section * i;
kit.BeginPart();
if ( bottom + section - 0.1f <= iz1 + 0.001f )
{
kit.Box( new Vector3( ix0, middle, bottom ), new Vector3( ix1, middle + thick, bottom + section - 0.1f ) );
if ( door.Hardware && i == 0 )
{
kit.Material = hardware;
kit.BeginPart();
kit.Box( new Vector3( (ix0 + ix1) * 0.5f - 3.0f, middle - 0.8f, bottom + 3.0f ), new Vector3( (ix0 + ix1) * 0.5f + 3.0f, middle, bottom + 4.0f ) );
kit.Material = leafMaterial;
}
}
else
{
// Lying flat in the room under the ceiling, the first one up the furthest in.
var y = back + 0.5f + stacked * section;
kit.Box( new Vector3( ix0, y, z1 + 0.5f ), new Vector3( ix1, y + section - 0.1f, z1 + 0.5f + thick ) );
stacked++;
}
}
break;
}
default:
{
var leaves = Math.Clamp( door.Leaves, 1, 2 );
var span = (ix1 - ix0) / leaves;
var gap = thick * thick / (2.0f * span) + 0.05f;
var angle = open * 85.0f;
for ( var leaf = 0; leaf < leaves; leaf++ )
{
var left = leaf == 0;
// Hinged on the inside edge of the jamb, the leaf flush with the frame's back face.
var hinge = new Transform( new Vector3( left ? ix0 : ix1, back, 0 ), Rotation.FromYaw( left ? angle : -angle ) );
var l0 = left ? 0.0f : -(span - gap);
var l1 = left ? span - gap : 0.0f;
var target = kit;
if ( door.SeparateLeaves is not null )
target = new MeshKit { Material = leafMaterial };
else
kit.Frame = origin.ToWorld( hinge );
HingedLeaf( target, door, look, box.Corner, l0, l1, iz0, iz1, thick, leafMaterial, hardware, left );
if ( door.SeparateLeaves is not null )
door.SeparateLeaves.Add( (hinge, target.Finish(), left ? 1.0f : -1.0f) );
kit.Frame = origin;
}
break;
}
}
kit.Frame = origin;
kit.EndAssembly();
kit.Material = material;
}
/// <summary>One hinged leaf from l0 to l1 in the frame of its hinge, its back face at y = 0.</summary>
static void HingedLeaf( MeshKit kit, DoorSpec door, WindowSpec look, FrameCorner corner, float l0, float l1, float iz0, float iz1, float thick, Material leafMaterial, Material hardware, bool left )
{
kit.Material = leafMaterial;
if ( door.Style == DoorStyle.Plain )
{
kit.BeginPart();
kit.Box( new Vector3( l0, -thick, iz0 ), new Vector3( l1, 0, iz1 ) );
}
else
{
var stile = MathF.Min( 4.0f, MathF.Min( l1 - l0, iz1 - iz0 ) * 0.2f );
var leafSpec = Copy( look );
leafSpec.Width = stile;
leafSpec.Depth = thick;
leafSpec.Shape = WindowShape.Rectangle;
leafSpec.Arch = ArchStyle.Flat;
leafSpec.Corner = corner;
leafSpec.FrameMaterial = leafMaterial;
leafSpec.Glass = door.Style == DoorStyle.Glazed;
leafSpec.Columns = 1;
leafSpec.Rows = door.Style == DoorStyle.Glazed ? 3 : 1;
var made = WindowKit.Build( kit, l0, l1, iz0, iz1, -thick, leafSpec );
if ( door.Style == DoorStyle.Panelled )
{
// The panel sits in the middle of the leaf, inside its stiles and rails.
var sw = made.Width;
kit.BeginPart();
kit.Box( new Vector3( l0 + sw, -thick * 0.7f, iz0 + sw ), new Vector3( l1 - sw, -thick * 0.3f, iz1 - sw ) );
}
}
if ( !door.Hardware ) return;
kit.Material = hardware;
var x = left ? l1 - 3.0f : l0 + 3.0f;
var z = MathF.Min( iz0 + 40.0f, (iz0 + iz1) * 0.5f );
Handle( kit, x, z, 0, left ? 1 : -1 );
// The outside knob.
kit.BeginPart();
kit.Box( new Vector3( x - 0.4f, -thick - 0.6f, z - 0.4f ), new Vector3( x + 0.4f, -thick, z + 0.4f ) );
}
/// <summary>
/// A sliding or barn leaf from a to b, z0 to z1, between y0 and y1: boards for a barn door
/// with braces on its outer face (<paramref name="outward"/> -1 toward -Y), a framed panel otherwise.
/// </summary>
static void Plank( MeshKit kit, float a, float b, float z0, float z1, float y0, float y1, DoorStyle style, float outward )
{
kit.BeginPart();
kit.Box( new Vector3( a, y0, z0 ), new Vector3( b, y1, z1 ) );
if ( style == DoorStyle.Plain ) return;
// Two rails and a diagonal brace standing on the face away from the wall.
var face = outward > 0 ? y1 : y0;
var f0 = outward > 0 ? face : face - 0.6f;
var f1 = outward > 0 ? face + 0.6f : face;
var rail = MathF.Min( 6.0f, (z1 - z0) * 0.08f );
kit.BeginPart();
kit.Box( new Vector3( a + 1, f0, z0 + 4 ), new Vector3( b - 1, f1, z0 + 4 + rail ) );
kit.BeginPart();
kit.Box( new Vector3( a + 1, f0, z1 - 4 - rail ), new Vector3( b - 1, f1, z1 - 4 ) );
var lo = z0 + 4 + rail;
var hi = z1 - 4 - rail;
if ( hi - lo > rail * 2.0f )
{
var half = rail * 0.5f;
WindowKit.Loft( kit,
[new Vector3( a + 1, f0, lo ), new Vector3( a + 1 + rail, f0, lo ), new Vector3( b - 1, f0, hi - half * 0 ), new Vector3( b - 1 - rail, f0, hi )],
[new Vector3( a + 1, f1, lo ), new Vector3( a + 1 + rail, f1, lo ), new Vector3( b - 1, f1, hi - half * 0 ), new Vector3( b - 1 - rail, f1, hi )] );
}
}
/// <summary>
/// A lever handle on the inside face (y = back from the sash front), at x, z. Lever points
/// down; side +1 and -1 turn its rose toward the middle of the sash.
/// </summary>
static void Handle( MeshKit kit, float x, float z, float back, int side )
{
kit.BeginPart();
kit.Box( new Vector3( x - 0.4f, back, z - 1.0f ), new Vector3( x + 0.4f, back + 0.35f, z + 1.0f ) );
kit.BeginPart();
kit.Box( new Vector3( x - 0.25f, back + 0.35f, z - 0.25f ), new Vector3( x + 0.25f, back + 0.9f, z + 0.25f ) );
kit.BeginPart();
// On a rail the lever lies level, on a stile it hangs down.
if ( side == 0 )
kit.Box( new Vector3( x - 2.5f, back + 0.9f, z - 0.25f ), new Vector3( x + 0.25f, back + 1.3f, z + 0.25f ) );
else
kit.Box( new Vector3( x - 0.25f, back + 0.9f, z - 2.5f ), new Vector3( x + 0.25f, back + 1.3f, z + 0.25f ) );
}
/// <summary>Two hinge knuckles on the front of the frame beside the opening edge.</summary>
static void Hinges( MeshKit kit, float x0, float x1, float z0, float z1, float front )
{
foreach ( var z in new[] { z0 + (z1 - z0) * 0.15f, z1 - (z1 - z0) * 0.15f } )
{
kit.BeginPart();
kit.Box( new Vector3( x0, front - 0.5f, z - 1.25f ), new Vector3( x1, front, z + 1.25f ) );
}
}
/// <summary>
/// Glass louvres turning about level axes through the middle of the frame depth. Neighbours
/// stay apart at every angle; the top and bottom ones stay clear of head and sill.
/// </summary>
static void Jalousie( MeshKit kit, float x0, float x1, float z0, float z1, float front, float d, WindowUnitSpec u, float open, Material hardware )
{
var height = z1 - z0;
var count = u.Slats > 0 ? Math.Clamp( u.Slats, 2, 40 ) : Math.Clamp( (int)MathF.Round( height / 4.0f ), 3, 40 );
var tilt = (15.0f + open * 65.0f) * MathF.PI / 180.0f;
const float thick = 0.25f;
var pitch = height / count;
var slat = MathF.Min( pitch * 1.25f, (d - 0.2f - thick * MathF.Cos( tilt )) / MathF.Max( MathF.Sin( tilt ), 0.05f ) );
var reach = (slat * MathF.Cos( tilt ) + thick * MathF.Sin( tilt )) * 0.5f + 0.05f;
if ( height - reach * 2.0f < 0.5f || slat < 0.5f ) return;
var step = count > 1 ? (height - reach * 2.0f) / (count - 1) : 0.0f;
if ( count > 1 && step * MathF.Sin( tilt ) < thick + 0.05f )
{
count = Math.Max( 2, (int)((height - reach * 2.0f) / ((thick + 0.05f) / MathF.Sin( tilt ))) + 1 );
step = (height - reach * 2.0f) / (count - 1);
}
var y = front + d * 0.5f;
var axis = new Vector3( 0, MathF.Sin( tilt ), MathF.Cos( tilt ) );
var across = new Vector3( 0, MathF.Cos( tilt ), -MathF.Sin( tilt ) );
kit.Material = u.Look.GlassMaterial ?? Material.Load( "materials/glass/glass_a.vmat" );
for ( var i = 0; i < count; i++ )
{
var centre = new Vector3( 0, y, z0 + reach + step * i );
Vector3 P( float x, float a, float b ) => centre.WithX( x ) + axis * a + across * b;
WindowKit.Loft( kit, [P( x0, -slat * 0.5f, thick * 0.5f ), P( x0, slat * 0.5f, thick * 0.5f ), P( x0, slat * 0.5f, -thick * 0.5f ), P( x0, -slat * 0.5f, -thick * 0.5f )],
[P( x1, -slat * 0.5f, thick * 0.5f ), P( x1, slat * 0.5f, thick * 0.5f ), P( x1, slat * 0.5f, -thick * 0.5f ), P( x1, -slat * 0.5f, -thick * 0.5f )] );
}
}
/// <summary>Glass blocks in a grid with mortar joints between them.</summary>
static void Blocks( MeshKit kit, float x0, float x1, float z0, float z1, float front, float d, WindowUnitSpec u )
{
var size = Math.Clamp( u.BlockSize, 3.0f, 24.0f );
var columns = Math.Max( 1, (int)MathF.Round( (x1 - x0) / size ) );
var rows = Math.Max( 1, (int)MathF.Round( (z1 - z0) / size ) );
var joint = MathF.Min( 0.6f, MathF.Min( (x1 - x0) / columns, (z1 - z0) / rows ) * 0.15f );
var cw = (x1 - x0 - joint * (columns - 1)) / columns;
var ch = (z1 - z0 - joint * (rows - 1)) / rows;
var y0 = front + MathF.Min( 0.3f, d * 0.1f );
var y1 = front + d - MathF.Min( 0.3f, d * 0.1f );
var glass = u.Look.GlassMaterial ?? Material.Load( "materials/glass/glass_a.vmat" );
var mortar = u.Look.DetailMaterial ?? u.Look.FrameMaterial ?? kit.Material;
for ( var c = 0; c < columns; c++ )
for ( var r = 0; r < rows; r++ )
{
var bx = x0 + c * (cw + joint);
var bz = z0 + r * (ch + joint);
kit.Material = glass;
kit.BeginPart();
kit.Box( new Vector3( bx, y0, bz ), new Vector3( bx + cw, y1, bz + ch ) );
}
kit.Material = mortar;
for ( var c = 1; c < columns; c++ )
{
var x = x0 + c * (cw + joint) - joint;
kit.BeginPart();
kit.Box( new Vector3( x, y0, z0 ), new Vector3( x + joint, y1, z1 ) );
}
for ( var r = 1; r < rows; r++ )
for ( var c = 0; c < columns; c++ )
{
var z = z0 + r * (ch + joint) - joint;
var bx = x0 + c * (cw + joint);
kit.BeginPart();
kit.Box( new Vector3( bx, y0, z ), new Vector3( bx + cw, y1, z + joint ) );
}
}
/// <summary>
/// Glazed panels folding like an accordion toward the left jamb. The gap at every fold grows
/// with the angle so neighbouring panels never meet.
/// </summary>
static void Accordion( MeshKit kit, float x0, float x1, float z0, float z1, float front, float d, WindowUnitSpec u, float open, WindowSpec sash, Material hardware )
{
var count = Math.Clamp( u.Panels, 2, 12 );
var thick = MathF.Min( d * 0.5f, 2.0f );
var fold = open * 60.0f * MathF.PI / 180.0f;
var gap = 2.0f * thick * MathF.Sin( fold ) + 0.1f;
// The folds need room at a slant: the chain of panels must still fit between the jambs.
var panel = MathF.Min( (x1 - x0 - 0.1f * (count - 1)) / count, (x1 - x0 - gap * (count - 1) - 0.05f) / (count * MathF.Cos( fold )) );
if ( panel < 2.0f ) return;
var p = sash;
p.Depth = thick;
p.Width = MathF.Max( MathF.Min( p.Width, panel * 0.2f ), 0.3f );
var origin = kit.Frame;
var at = new Vector2( x0, front + (d - thick) * 0.5f );
for ( var i = 0; i < count; i++ )
{
var dir = new Vector2( MathF.Cos( fold ), i % 2 == 0 ? -MathF.Sin( fold ) : MathF.Sin( fold ) );
var yaw = MathF.Atan2( dir.y, dir.x ) * 180.0f / MathF.PI;
kit.Frame = origin.ToWorld( new Transform( new Vector3( at.x, at.y, 0 ), Rotation.FromYaw( yaw ) ) );
WindowKit.Build( kit, 0, panel, z0, z1, 0, p );
at += dir * panel + new Vector2( gap, 0 );
}
kit.Frame = origin;
}
}