Editor/Wall/ArchWindowGen.cs

Editor utility that generates window geometry for architecture pieces. It builds frame, sash, glazing and optional broken shards into ArchMesh objects based on opening and kit parameters, computing layout (lights, sashes, panes) deterministically from opening id and presets.

File Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Wall local space: x along, y across, exterior at -half, z up.
public static class ArchWindowGen
{
	public static void Build(
		ArchMesh canvas,
		ArchMesh panes,
		ArchOpening opening,
		ArchKit kit,
		float half,
		float wallHeight,
		ArchBrush frame,
		ArchBrush sash,
		ArchBrush glass )
	{
		var left = opening.Left;
		var right = opening.Right;
		var bottom = MathF.Max( 0f, opening.SillHeight );
		var top = MathF.Min( wallHeight, opening.Top );

		var face = MathF.Max( 1f, kit.FrameFace );
		var bite = ArchContact.Bite( kit );

		if ( right - left < face * 3f || top - bottom < face * 3f )
		{
			return;
		}

		// Keyed off the id so identical windows vary; deterministic across rebuilds.
		var grain = Grain( opening );

		frame = frame.Shifted( grain );
		sash = sash.Shifted( grain );

		// Set back from both faces; a fixed outer depth left the inner half bare.
		var setback = Math.Clamp( kit.FrameSetback, 0f, half * 0.5f );
		var outer = -half + setback;
		var inner = MathF.Max( outer + MathF.Max( 1f, kit.FrameDepth ), half - setback );

		canvas.Box( new Vector3( left, outer, bottom ), new Vector3( left + face, inner, top ), frame );
		canvas.Box( new Vector3( right - face, outer, bottom ), new Vector3( right, inner, top ), frame );
		canvas.Box( new Vector3( left + face - bite, outer, top - face ), new Vector3( right - face + bite, inner, top ), frame );
		canvas.Box( new Vector3( left + face - bite, outer, bottom ), new Vector3( right - face + bite, inner, bottom + face ), frame );

		var lightBottom = bottom + face;
		var lightTop = top - face;
		var interiorWidth = right - left - face * 2f;
		var mullion = MathF.Max( face, kit.FrameFace * 1.4f );
		var lights = CountThatFits( RequestedLights( opening, kit ), interiorWidth, mullion, face * 3f );
		var unit = (interiorWidth - mullion * (lights - 1)) / lights;

		for ( var light = 0; light < lights; light++ )
		{
			var unitLeft = left + face + light * (unit + mullion);
			var unitRight = unitLeft + unit;

			// A shared member goes in once; touching jambs would z-fight down the seam.
			if ( light > 0 )
			{
				canvas.Box( new Vector3( unitLeft - mullion, outer, lightBottom - bite ), new Vector3( unitLeft, inner, lightTop + bite ), frame );
			}

			Stack( canvas, panes, opening, kit, light, unitLeft, unitRight, lightBottom, lightTop, outer, inner, face, frame, sash, glass );
		}
	}

	static void Stack(
		ArchMesh canvas,
		ArchMesh panes,
		ArchOpening opening,
		ArchKit kit,
		int light,
		float left,
		float right,
		float bottom,
		float top,
		float outer,
		float inner,
		float face,
		ArchBrush frame,
		ArchBrush sash,
		ArchBrush glass )
	{
		var member = MathF.Max( 0.8f, kit.SashFace );
		var sashes = CountThatFits( RequestedSashes( opening, kit ), top - bottom, face, member * 2.5f );
		var height = (top - bottom) / sashes;

		for ( var index = 0; index < sashes; index++ )
		{
			var from = bottom + index * height;
			var to = from + height;

			// The meeting rail belongs to the frame: one per boundary, not two touching.
			if ( index > 0 )
			{
				var bite = ArchContact.Bite( kit );
				canvas.Box(
					new Vector3( left - bite, outer, from - face * 0.5f - bite ),
					new Vector3( right + bite, inner, from + face * 0.5f + bite ),
					frame );
				from += face * 0.5f;
			}

			if ( index < sashes - 1 )
			{
				to -= face * 0.5f;
			}

			Sash( canvas, panes, opening, kit, light, index, left, right, from, to, outer, inner, sash, glass );
		}
	}

	static void Sash(
		ArchMesh canvas,
		ArchMesh panes,
		ArchOpening opening,
		ArchKit kit,
		int light,
		int tier,
		float left,
		float right,
		float bottom,
		float top,
		float outer,
		float inner,
		ArchBrush sash,
		ArchBrush glass )
	{
		var member = MathF.Max( 0.8f, kit.SashFace );
		var depth = Math.Clamp( kit.SashDepth, 0.4f, inner - outer );
		var bite = ArchContact.Bite( kit );

		if ( right - left < member * 2.5f || top - bottom < member * 2.5f )
		{
			return;
		}

		var front = outer + (inner - outer - depth) * 0.5f;
		var back = front + depth;

		canvas.Box( new Vector3( left, front, bottom ), new Vector3( left + member, back, top ), sash );
		canvas.Box( new Vector3( right - member, front, bottom ), new Vector3( right, back, top ), sash );
		canvas.Box( new Vector3( left + member - bite, front, bottom ), new Vector3( right - member + bite, back, bottom + member ), sash );
		canvas.Box( new Vector3( left + member - bite, front, top - member ), new Vector3( right - member + bite, back, top ), sash );

		var lightLeft = left + member;
		var lightRight = right - member;
		var lightBottom = bottom + member;
		var lightTop = top - member;

		// Bars sit on the outer half, glass on the inner: the two never intersect.
		var glassFront = front + depth * 0.5f - 0.2f;
		var glassBack = MathF.Min( back, glassFront + 0.4f );
		var bar = MathF.Max( 0.3f, kit.GlazingBar );
		var minimumPane = MathF.Max( 2f, bar * 3f );
		var across = CountThatFits( opening.PanesAcross, lightRight - lightLeft, bar, minimumPane );
		var down = CountThatFits( opening.PanesDown, lightTop - lightBottom, bar, minimumPane );

		for ( var index = 1; index < across; index++ )
		{
			var centre = lightLeft + (lightRight - lightLeft) * index / across;
			canvas.Box( new Vector3( centre - bar * 0.5f, front, lightBottom - bite ), new Vector3( centre + bar * 0.5f, back, lightTop + bite ), sash );
		}

		for ( var index = 1; index < down; index++ )
		{
			var centre = lightBottom + (lightTop - lightBottom) * index / down;
			canvas.Box( new Vector3( lightLeft - bite, front, centre - bar * 0.5f ), new Vector3( lightRight + bite, back, centre + bar * 0.5f ), sash );
		}

		Glazing(
			panes,
			opening,
			light,
			tier,
			lightLeft,
			lightRight,
			lightBottom,
			lightTop,
			glassFront,
			glassBack,
			across,
			down,
			bar * 0.5f,
			glass );
	}

	static int CountThatFits( int requested, float span, float separator, float minimumCell )
	{
		var capacity = Math.Max( 1, (int)MathF.Floor( (span + separator) / (minimumCell + separator) ) );
		return Math.Min( Math.Max( 1, requested ), capacity );
	}

	static int RequestedLights( ArchOpening opening, ArchKit kit )
	{
		if ( !opening.AutoLayout || kit.FindOpening( opening.Preset ) is not { Width: > 0.01f } preset )
		{
			return opening.Lights;
		}

		var repeats = Math.Max( 1, (int)MathF.Round( opening.Width / preset.Width ) );
		return Math.Max( 1, preset.Lights ) * repeats;
	}

	static int RequestedSashes( ArchOpening opening, ArchKit kit )
	{
		if ( !opening.AutoLayout || kit.FindOpening( opening.Preset ) is not { Height: > 0.01f } preset )
		{
			return opening.Sashes;
		}

		var repeats = Math.Max( 1, (int)MathF.Round( opening.Height / preset.Height ) );
		return Math.Max( 1, preset.Sashes ) * repeats;
	}

	// One box per light unless broken; a gone pane leaves two corner shards.
	static void Glazing(
		ArchMesh panes,
		ArchOpening opening,
		int light,
		int tier,
		float left,
		float right,
		float bottom,
		float top,
		float front,
		float back,
		int across,
		int down,
		float separation,
		ArchBrush glass )
	{
		for ( var x = 0; x < across; x++ )
		{
			var paneLeft = left + (right - left) * x / across + (x > 0 ? separation : 0f);
			var paneRight = left + (right - left) * (x + 1) / across - (x < across - 1 ? separation : 0f);

			for ( var y = 0; y < down; y++ )
			{
				var paneBottom = bottom + (top - bottom) * y / down + (y > 0 ? separation : 0f);
				var paneTop = bottom + (top - bottom) * (y + 1) / down - (y < down - 1 ? separation : 0f);

				if ( opening.Glass == GlassState.Broken && Gone( opening, light, tier, x, y ) )
				{
					Shards( panes, paneLeft, paneRight, paneBottom, paneTop, front, glass );
					continue;
				}

				panes.Box( new Vector3( paneLeft, front, paneBottom ), new Vector3( paneRight, back, paneTop ), glass );
			}
		}
	}

	static void Shards( ArchMesh panes, float left, float right, float bottom, float top, float depth, ArchBrush glass )
	{
		var width = (right - left) * 0.45f;
		var height = (top - bottom) * 0.4f;

		panes.Polygon( new List<Vector3>
		{
			new( left, depth, top ),
			new( left, depth, top - height ),
			new( left + width, depth, top )
		}, glass );

		panes.Polygon( new List<Vector3>
		{
			new( right, depth, bottom ),
			new( right, depth, bottom + height * 0.7f ),
			new( right - width * 0.6f, depth, bottom )
		}, glass );
	}

	static Vector2 Grain( ArchOpening opening )
	{
		unchecked
		{
			var hash = opening.Id * 2654435761u;

			return new Vector2( (hash >> 8) & 0xff, (hash >> 20) & 0xff );
		}
	}

	// Deterministic: a rebuild must break the same panes.
	static bool Gone( ArchOpening opening, int light, int tier, int x, int y )
	{
		unchecked
		{
			var hash = opening.Id * 9781 + light * 6151 + tier * 673 + x * 97 + y * 31;
			hash = (hash ^ (hash >> 13)) * 60493;

			return ((hash >> 7) & 3) != 0;
		}
	}
}