// .example-captures/audit-build/procedural-tower-building-entry.ts import * as THREE2 from "three/webgpu"; function createRandom(seed) { let state = seed >>> 0; const next = () => { state += 1831565813; let value = state; value = Math.imul(value ^ value >>> 15, value | 1); value ^= value + Math.imul(value ^ value >>> 7, value | 61); return ((value ^ value >>> 14) >>> 0) / 4294967296; }; return { next, range: (min, max) => min + (max - min) * next(), int: (min, max) => Math.floor(min + (max - min + 1) * next()), chance: (odds) => next() < odds }; } function exposedEdgesFor(rect, allRects) { return ["front", "back", "left", "right"].flatMap((side) => exposedSegments(rect, allRects, side).map((segment, index) => toEdge(rect, side, segment, index))); } function exposedSegments(rect, allRects, side) { const base = side === "front" || side === "back" ? { a: rect.x - rect.width / 2, b: rect.x + rect.width / 2 } : { a: rect.z - rect.depth / 2, b: rect.z + rect.depth / 2 }; const blockers = allRects.filter((candidate) => candidate !== rect).flatMap((candidate) => blockerSegment(rect, candidate, side)); return subtractSegments([base], blockers); } function blockerSegment(rect, other, side) { const epsilon = 1e-3; if (side === "front") { const touches2 = Math.abs(rect.z + rect.depth / 2 - (other.z - other.depth / 2)) < epsilon; return touches2 ? overlap(rect.x - rect.width / 2, rect.x + rect.width / 2, other.x - other.width / 2, other.x + other.width / 2) : []; } if (side === "back") { const touches2 = Math.abs(rect.z - rect.depth / 2 - (other.z + other.depth / 2)) < epsilon; return touches2 ? overlap(rect.x - rect.width / 2, rect.x + rect.width / 2, other.x - other.width / 2, other.x + other.width / 2) : []; } if (side === "right") { const touches2 = Math.abs(rect.x + rect.width / 2 - (other.x - other.width / 2)) < epsilon; return touches2 ? overlap(rect.z - rect.depth / 2, rect.z + rect.depth / 2, other.z - other.depth / 2, other.z + other.depth / 2) : []; } const touches = Math.abs(rect.x - rect.width / 2 - (other.x + other.width / 2)) < epsilon; return touches ? overlap(rect.z - rect.depth / 2, rect.z + rect.depth / 2, other.z - other.depth / 2, other.z + other.depth / 2) : []; } function overlap(a0, a1, b0, b1) { const a = Math.max(a0, b0); const b = Math.min(a1, b1); return b - a > 0.01 ? [{ a, b }] : []; } function subtractSegments(source, blockers) { let result = source; for (const blocker of blockers) { result = result.flatMap((segment) => subtractSegment(segment, blocker)); } return result.filter((segment) => segment.b - segment.a > 0.25); } function subtractSegment(segment, blocker) { if (blocker.b <= segment.a || blocker.a >= segment.b) return [segment]; const result = []; if (blocker.a > segment.a) result.push({ a: segment.a, b: blocker.a }); if (blocker.b < segment.b) result.push({ a: blocker.b, b: segment.b }); return result; } function toEdge(rect, side, segment, index) { const length = segment.b - segment.a; const axisCenter = (segment.a + segment.b) / 2; const localCenter = side === "front" || side === "back" ? axisCenter - rect.x : axisCenter - rect.z; return { id: `${rect.suffix}-${side}-${index}`, side, center: localCenter, length, x: rect.x, z: rect.z, isOuterCornerStart: true, isOuterCornerEnd: true, isInnerCornerStart: length < (side === "front" || side === "back" ? rect.width : rect.depth) - 0.01, isInnerCornerEnd: length < (side === "front" || side === "back" ? rect.width : rect.depth) - 0.01 }; } var BAY_WIDTH = 3.2; var FLOOR_HEIGHT = 3.35; var PODIUM_FLOOR_HEIGHT = 4.45; function createMassTiers(settings) { const random = createRandom(settings.seed); const podiumHeight = settings.podiumFloors * PODIUM_FLOOR_HEIGHT; const crownFloors = settings.crown ? Math.max(1, settings.setbackFloors) : 0; const shaftFloors = Math.max(1, settings.floors - settings.podiumFloors - crownFloors); const fullWidth = settings.widthBays * BAY_WIDTH; const fullDepth = settings.depthBays * BAY_WIDTH; const towerScale = clamp(settings.towerScale + random.range(-0.05, 0.04), 0.62, 0.96); const setbackInset = BAY_WIDTH * (1 - towerScale) * random.range(0.86, 1.08); if (settings.massingPattern === "twin-towers") { return createTwinTowerTiers(settings, random, { podiumHeight, crownFloors, shaftFloors, fullWidth, fullDepth, setbackInset }); } const tiers = createFootprintTiers({ name: "podium", role: "podium", style: podiumFootprintStyle(settings), width: fullWidth, depth: fullDepth, y0: 0, height: podiumHeight, floors: settings.podiumFloors, inset: 0, settings }); const shaftSlices = splitShaftFloors(settings.variant, shaftFloors, random); let y = podiumHeight; for (let index = 0; index < shaftSlices.length; index++) { const floors = shaftSlices[index]; const inset = shaftInset(settings.variant, setbackInset, index, random); const shaftStyle = tierFootprintStyle(settings, "shaft", index); const adjusted = directionalInset(settings, clampedSpan(fullWidth, inset), clampedSpan(fullDepth, inset), index); tiers.push(...createFootprintTiers({ name: `shaft-${index + 1}`, role: "shaft", style: shaftStyle, width: adjusted.width, depth: adjusted.depth, baseX: adjusted.x, baseZ: adjusted.z, y0: y, height: floors * FLOOR_HEIGHT, floors, inset, settings })); y += floors * FLOOR_HEIGHT; } if (settings.crown) { const crownInset = (tiers[tiers.length - 1]?.inset ?? setbackInset) + BAY_WIDTH * random.range(0.42, 0.68); const adjusted = directionalInset(settings, clampedSpan(fullWidth, crownInset), clampedSpan(fullDepth, crownInset), shaftSlices.length); tiers.push(...createFootprintTiers({ name: "crown", role: "crown", style: tierFootprintStyle(settings, "crown", shaftSlices.length), width: adjusted.width, depth: adjusted.depth, baseX: adjusted.x, baseZ: adjusted.z, y0: y, height: crownFloors * FLOOR_HEIGHT, floors: crownFloors, inset: crownInset, settings })); } return tiers; } function createFootprintTiers(input) { const pieces = footprintPieces(input.style, input.width, input.depth, input.settings); const baseX = input.baseX ?? 0; const baseZ = input.baseZ ?? 0; return pieces.map((piece) => ({ name: piece.suffix === "main" ? input.name : `${input.name}-${piece.suffix}`, role: input.role, width: piece.width, depth: piece.depth, x: baseX + piece.x, z: baseZ + piece.z, y0: input.y0, height: input.height, floors: input.floors, inset: input.inset, facadeEdges: exposedEdgesFor(piece, pieces).map((edge) => ({ ...edge, x: baseX + edge.x, z: baseZ + edge.z })) })); } function footprintPieces(style, width, depth, settings) { if (style === "free-court") return freeCourtPieces(width, depth, settings); if (style === "l-shape") { const frontDepth = depth * 0.58; const rearDepth = depth - frontDepth; const wingWidth = width * 0.44; return [ { suffix: "front-bar", width, depth: frontDepth, x: 0, z: depth / 2 - frontDepth / 2 }, { suffix: "rear-wing", width: wingWidth, depth: rearDepth, x: -width / 2 + wingWidth / 2, z: -depth / 2 + rearDepth / 2 } ]; } if (style === "t-shape") { const crossDepth = depth * 0.36; const stemDepth = depth - crossDepth; const stemWidth = width * 0.46; return [ { suffix: "cross-bar", width, depth: crossDepth, x: 0, z: depth / 2 - crossDepth / 2 }, { suffix: "stem", width: stemWidth, depth: stemDepth, x: 0, z: -depth / 2 + stemDepth / 2 } ]; } if (style === "u-shape") { const frontDepth = depth * 0.34; const wingDepth = depth - frontDepth; const wingWidth = width * 0.26; return [ { suffix: "front-bar", width, depth: frontDepth, x: 0, z: depth / 2 - frontDepth / 2 }, { suffix: "left-wing", width: wingWidth, depth: wingDepth, x: -width / 2 + wingWidth / 2, z: -depth / 2 + wingDepth / 2 }, { suffix: "right-wing", width: wingWidth, depth: wingDepth, x: width / 2 - wingWidth / 2, z: -depth / 2 + wingDepth / 2 } ]; } if (style === "courtyard-block") { const bar = Math.max(BAY_WIDTH * 2, Math.min(width, depth) * 0.24); return [ { suffix: "front-bar", width, depth: bar, x: 0, z: depth / 2 - bar / 2 }, { suffix: "back-bar", width, depth: bar, x: 0, z: -depth / 2 + bar / 2 }, { suffix: "left-bar", width: bar, depth: depth - bar * 2, x: -width / 2 + bar / 2, z: 0 }, { suffix: "right-bar", width: bar, depth: depth - bar * 2, x: width / 2 - bar / 2, z: 0 } ]; } const blockScale = style === "high-rise-block" ? 0.86 : 1; return [{ suffix: "main", width: width * blockScale, depth: depth * blockScale, x: 0, z: 0 }]; } function createTwinTowerTiers(settings, random, input) { const tiers = createFootprintTiers({ name: "podium", role: "podium", style: "rectangle", width: input.fullWidth, depth: input.fullDepth, y0: 0, height: input.podiumHeight, floors: settings.podiumFloors, inset: 0, settings }); const gap = Math.max(BAY_WIDTH * 2.2, input.fullWidth * 0.18); const towerWidth = Math.max(BAY_WIDTH * 4, (input.fullWidth - gap) * 0.46); const towerDepth = input.fullDepth * 0.82; const towerOffset = gap / 2 + towerWidth / 2; const shaftSlices = splitShaftFloors(settings.variant, input.shaftFloors, random); let y = input.podiumHeight; for (let index = 0; index < shaftSlices.length; index++) { const floors = shaftSlices[index]; const inset = shaftInset(settings.variant, input.setbackInset * 0.72, index, random); const towerSpan = { width: clampedSpan(towerWidth, inset * 0.55), depth: clampedSpan(towerDepth, inset * 0.55) }; tiers.push(...createFootprintTiers({ name: `shaft-${index + 1}-west-tower`, role: "shaft", style: tierFootprintStyle(settings, "shaft", index), width: towerSpan.width, depth: towerSpan.depth, baseX: -towerOffset + inset * 0.18, y0: y, height: floors * FLOOR_HEIGHT, floors, inset, settings })); tiers.push(...createFootprintTiers({ name: `shaft-${index + 1}-east-tower`, role: "shaft", style: settings.secondaryFootprintStyle, width: towerSpan.width, depth: towerSpan.depth, baseX: towerOffset - inset * 0.18, y0: y, height: floors * FLOOR_HEIGHT, floors, inset, settings })); y += floors * FLOOR_HEIGHT; } if (settings.skybridgeEnabled) { const bridgeY = input.podiumHeight + clamp(settings.skybridgeFloor, 1, input.shaftFloors) * FLOOR_HEIGHT; tiers.push(...createFootprintTiers({ name: "skybridge", role: "bridge", style: "rectangle", width: gap + towerWidth * 0.42, depth: Math.max(BAY_WIDTH * 1.2, input.fullDepth * 0.18), y0: bridgeY, height: FLOOR_HEIGHT * 1.15, floors: 1, inset: 0, settings })); } if (settings.crown) { const crownInset = input.setbackInset + BAY_WIDTH * random.range(0.42, 0.68); const crownSpan = { width: clampedSpan(towerWidth, crownInset * 0.55), depth: clampedSpan(towerDepth, crownInset * 0.55) }; for (const [name, baseX, style] of [ ["crown-west-tower", -towerOffset + crownInset * 0.18, settings.footprintStyle], ["crown-east-tower", towerOffset - crownInset * 0.18, settings.secondaryFootprintStyle] ]) { tiers.push(...createFootprintTiers({ name, role: "crown", style, width: crownSpan.width, depth: crownSpan.depth, baseX, y0: y, height: input.crownFloors * FLOOR_HEIGHT, floors: input.crownFloors, inset: crownInset, settings })); } } return tiers; } function freeCourtPieces(width, depth, settings) { const minBar = BAY_WIDTH * 1.8; const innerWidth = clamp(width * settings.innerCourtWidth, BAY_WIDTH * 2, width - minBar * 2); const innerDepth = clamp(depth * settings.innerCourtDepth, BAY_WIDTH * 2, depth - minBar * 2); const cxLimit = Math.max(0, (width - innerWidth) / 2 - minBar); const czLimit = Math.max(0, (depth - innerDepth) / 2 - minBar); const cx = clamp(settings.innerCourtOffsetX * width * 0.5, -cxLimit, cxLimit); const cz = clamp(settings.innerCourtOffsetZ * depth * 0.5, -czLimit, czLimit); const outerX0 = -width / 2; const outerX1 = width / 2; const outerZ0 = -depth / 2; const outerZ1 = depth / 2; const courtX0 = cx - innerWidth / 2; const courtX1 = cx + innerWidth / 2; const courtZ0 = cz - innerDepth / 2; const courtZ1 = cz + innerDepth / 2; return [ { suffix: "front-bar", width, depth: outerZ1 - courtZ1, x: 0, z: (outerZ1 + courtZ1) / 2 }, { suffix: "back-bar", width, depth: courtZ0 - outerZ0, x: 0, z: (outerZ0 + courtZ0) / 2 }, { suffix: "left-bar", width: courtX0 - outerX0, depth: innerDepth, x: (outerX0 + courtX0) / 2, z: cz }, { suffix: "right-bar", width: outerX1 - courtX1, depth: innerDepth, x: (courtX1 + outerX1) / 2, z: cz } ].filter((piece) => piece.width > 0.25 && piece.depth > 0.25); } function splitShaftFloors(variant, floors, random) { if (floors <= 4) return [floors]; if (variant === "classic-bank") return [floors]; if (variant === "corner-hq") { const lower2 = Math.max(3, Math.ceil(floors * random.range(0.56, 0.72))); return [lower2, floors - lower2].filter(Boolean); } const lower = Math.max(3, Math.floor(floors * random.range(0.36, 0.5))); const middle = Math.max(3, Math.floor(floors * random.range(0.26, 0.38))); const upper = floors - lower - middle; return [lower, middle, Math.max(1, upper)].filter(Boolean); } function shaftInset(variant, baseInset, step, random) { if (variant === "classic-bank") return baseInset; const jitter = random.range(-0.08, 0.12); if (variant === "corner-hq") return baseInset * (0.65 + step * 0.95 + jitter); return baseInset * (0.75 + step * 1.15 + jitter); } function tierFootprintStyle(settings, role, index) { if (settings.massingPattern === "outer-ring") return "free-court"; if (settings.footprintStyle === "high-rise-block") return "high-rise-block"; if (settings.footprintHeightMode === "full-height") return settings.footprintStyle; if (settings.footprintHeightMode === "lower-tiers-only") return role === "shaft" && index === 0 ? settings.footprintStyle : "rectangle"; return "rectangle"; } function podiumFootprintStyle(settings) { if (settings.massingPattern === "outer-ring") return "free-court"; return settings.footprintStyle; } function directionalInset(settings, width, depth, step) { const amount = Math.min(BAY_WIDTH * settings.hardInsetAmount * (step + 1) * 0.42, Math.min(width, depth) * 0.38); if (settings.hardInsetSide === "none" || amount <= 1e-3) return { width, depth, x: 0, z: 0 }; if (settings.hardInsetSide === "front") return { width, depth: Math.max(BAY_WIDTH * 4, depth - amount), x: 0, z: -amount / 2 }; if (settings.hardInsetSide === "back") return { width, depth: Math.max(BAY_WIDTH * 4, depth - amount), x: 0, z: amount / 2 }; if (settings.hardInsetSide === "left") return { width: Math.max(BAY_WIDTH * 4, width - amount), depth, x: amount / 2, z: 0 }; return { width: Math.max(BAY_WIDTH * 4, width - amount), depth, x: -amount / 2, z: 0 }; } function clampedSpan(span, inset) { return Math.max(BAY_WIDTH * 4, span - inset * 2); } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } var facadeSides = ["front", "back", "left", "right"]; function sideSpan(tier, side) { return side === "front" || side === "back" ? tier.width : tier.depth; } function exposedEdges(tier, side) { return tier.facadeEdges?.filter((edge) => edge.side === side) ?? [{ id: `${tier.name}-${side}`, side, center: 0, length: sideSpan(tier, side), x: tier.x, z: tier.z, isOuterCornerStart: true, isOuterCornerEnd: true, isInnerCornerStart: false, isInnerCornerEnd: false }]; } function edgeBayCount(edge, minimum = 3) { return Math.max(minimum, Math.round(edge.length / BAY_WIDTH)); } function edgeBayCenter(edge, index, count) { const width = edge.length / count; return edge.center - edge.length / 2 + width * (index + 0.5); } function edgeSeamCenter(edge, index, count) { return edge.center - edge.length / 2 + edge.length / count * index; } function place(input) { return { id: input.id, side: input.side, tierName: input.tier.name, center: input.center, y: input.y, width: input.width, height: input.height, depth: input.depth, floorIndex: input.floorIndex, bayIndex: input.bayIndex, edgeId: input.edge?.id, xOffset: input.edge?.x, zOffset: input.edge?.z, normalOffset: input.normalOffset, moduleVariant: input.moduleVariant }; } function roofPlace(input) { return { id: input.id, side: "roof", tierName: "roof", center: input.center, roofZ: input.roofZ, y: input.y, width: input.width, height: input.height, depth: input.depth, floorIndex: 0, bayIndex: input.bayIndex ?? 0 }; } function createCrownPlacements(settings, tier, side) { return exposedEdges(tier, side).flatMap((edge) => createCrownEdgePlacements(settings, tier, side, edge)); } function createCrownEdgePlacements(settings, tier, side, edge) { const count = edgeBayCount(edge, 3); const bay = edge.length / count; const result = []; for (let index = 0; index < count; index++) { const id = crownModule(settings, side, index, count); result.push(place({ id, side, tier, center: edgeBayCenter(edge, index, count), y: tier.y0, width: bay, height: tier.height, depth: id === "corner-parapet" ? 1.16 : 0.92, floorIndex: 0, bayIndex: index, edge })); } result.push(...crownTrim(tier, side, edge)); result.push(...crownTopOrnaments(settings, tier, side, edge)); return result; } function crownModule(settings, side, index, count) { const edge = index === 0 || index === count - 1; if (edge && settings.crownStyle !== "flat-parapet") return "corner-parapet"; if (settings.crownStyle === "flat-parapet") return edge ? "corner-parapet" : "parapet-section"; if (settings.crownStyle === "corner-parapets" && index % 2 === 0) return "parapet-section"; if (side === "back" && index % 3 === 1) return "parapet-section"; return "crown-window-bay"; } function crownTrim(tier, side, edge) { const span = edge.length; return [ place({ id: "small-cornice", side, tier, center: edge.center, y: tier.y0 - 0.26, width: span, height: 0.36, depth: 0.62, floorIndex: -1, bayIndex: -1, edge }), place({ id: "large-cornice", side, tier, center: edge.center, y: tier.y0 + tier.height - 0.74, width: span, height: 0.8, depth: 1.06, floorIndex: 0, bayIndex: -2, edge }), place({ id: "corner-cornice", side, tier, center: edge.center - span / 2 + 0.72, y: tier.y0 + tier.height - 0.82, width: 1.44, height: 0.86, depth: 1.14, floorIndex: 0, bayIndex: -3, edge }), place({ id: "corner-cornice", side, tier, center: edge.center + span / 2 - 0.72, y: tier.y0 + tier.height - 0.82, width: 1.44, height: 0.86, depth: 1.14, floorIndex: 0, bayIndex: -4, edge }), ...cornerTrimJoints(tier, side, edge, tier.y0 - 0.24, "belt-corner-joint", 0.62), ...cornerTrimJoints(tier, side, edge, tier.y0 + tier.height - 0.78, "cornice-corner-joint", 1) ]; } function crownTopOrnaments(settings, tier, side, edge) { const span = edge.length; const count = Math.max(1, Math.round(span / 4 * settings.crownDecorationDensity)); const panelWidth = span / count; const y = tier.y0 + tier.height - 0.02; const result = []; for (let index = 0; index < count; index++) { const id = crownDecorationPanel(settings, index); result.push(place({ id, side, tier, center: edge.center - span / 2 + panelWidth * (index + 0.5), y, width: panelWidth, height: id === "crown-cartouche-panel" ? 1.25 : 1, depth: 0.84, floorIndex: 1, bayIndex: index, edge })); } if ((side === "front" || side === "back") && settings.crownStyle !== "flat-parapet") { result.push(place({ id: "crown-pediment", side, tier, center: edge.center, y: y + 0.82, width: Math.min(5.4, span * 0.36), height: 1.45, depth: 1, floorIndex: 2, bayIndex: -5, edge })); } for (const edgeSign of [-1, 1]) { const id = crownFinial(settings); result.push(place({ id, side, tier, center: edge.center + edgeSign * (span / 2 - 0.5), y: y + 0.86, width: 0.8, height: id === "crown-obelisk-finial" ? 2.2 : 1.6, depth: 0.8, floorIndex: 2, bayIndex: edgeSign < 0 ? -6 : -7, edge })); } result.push(...edgeFinials(settings, tier, side, edge, y + 0.78)); return result; } function crownDecorationPanel(settings, index) { if (settings.crownDecorationStyle === "restrained") return "attic-crest-panel"; if (settings.crownDecorationStyle === "skyline") return index % 2 === 0 ? "crown-cartouche-panel" : "attic-crest-panel"; return index % 3 === 1 ? "crown-cartouche-panel" : "attic-crest-panel"; } function crownFinial(settings) { if (settings.crownDecorationStyle === "restrained") return "corner-finial"; if (settings.crownDecorationStyle === "skyline") return "crown-obelisk-finial"; return "crown-urn-finial"; } function edgeFinials(settings, tier, side, edge, y) { if (settings.crownFinialRhythm === "corners-only" || settings.crownFinialDensity <= 0.01) return []; const spacing = finialSpacing(settings); const usableSpan = Math.max(0, edge.length - 2.2); const count = Math.max(0, Math.floor(usableSpan / spacing * densityMultiplier(settings.crownFinialDensity))); if (count <= 0) return []; const step = usableSpan / (count + 1); const id = edgeFinialId(settings); const result = []; for (let index = 0; index < count; index++) { const center = edge.center - usableSpan / 2 + step * (index + 1); result.push(place({ id, side, tier, center, y, width: id === "crown-obelisk-finial" ? 0.8 : 0.68, height: id === "crown-obelisk-finial" ? 2 : 1.16, depth: id === "crown-obelisk-finial" ? 0.8 : 0.68, floorIndex: 3, bayIndex: 100 + index, edge })); } return result; } function finialSpacing(settings) { if (settings.crownFinialRhythm === "edge-sparse") return 5.2; if (settings.crownFinialRhythm === "edge-dense") return 2.1; if (settings.crownFinialRhythm === "skyline-spikes") return 3.4; return 3.2; } function densityMultiplier(value) { return 0.35 + value * 1.35; } function edgeFinialId(settings) { if (settings.crownFinialRhythm === "skyline-spikes") return "crown-obelisk-finial"; if (settings.crownDecorationStyle === "classical") return "crown-pillar-finial"; if (settings.crownDecorationStyle === "skyline") return "crown-obelisk-finial"; return "corner-finial"; } function cornerTrimJoints(tier, side, edge, y, id, depth) { return [-1, 1].map((edgeSign) => place({ id, side, tier, center: edge.center + edgeSign * (edge.length / 2 - 0.32), y, width: 0.86, height: id === "cornice-corner-joint" ? 0.92 : 0.46, depth, floorIndex: -9, bayIndex: edgeSign < 0 ? -90 : -91, edge })); } var PLINTH_HEIGHT = 0.74; function createPodiumPlacements(settings, tier, side) { return exposedEdges(tier, side).flatMap((edge) => createPodiumEdgePlacements(settings, tier, side, edge)); } function createPodiumEdgePlacements(settings, tier, side, edge) { const count = edgeBayCount(edge, side === "front" || side === "back" ? 5 : 3); const bay = edge.length / count; const result = []; for (let index = 0; index < count; index++) { const center = edgeBayCenter(edge, index, count); result.push(place({ id: settings.buildingArchetype === "federal-fortress" ? "rusticated-base-block" : "granite-plinth", side, tier, center, y: 0, width: bay, height: settings.buildingArchetype === "federal-fortress" ? 2.1 : PLINTH_HEIGHT, depth: 0.72, floorIndex: -1, bayIndex: index, edge })); result.push(...podiumBayStack(settings, tier, side, edge, center, bay, index, count)); } result.push(...podiumHorizontalTrim(tier, side, edge)); result.push(...podiumColumns(settings, tier, side, edge, count)); result.push(...frontageDetails(settings, tier, side, edge, count)); return result; } function podiumBayStack(settings, tier, side, edge, center, bay, index, count) { const result = []; for (let floor = 0; floor < tier.floors; floor++) { const y = floor === 0 ? PLINTH_HEIGHT : floor * PODIUM_FLOOR_HEIGHT; const height = floor === 0 ? PODIUM_FLOOR_HEIGHT - PLINTH_HEIGHT + 0.08 : PODIUM_FLOOR_HEIGHT - 0.18; const id = floor === 0 ? groundModule(settings, side, index, count) : upperPodiumModule(side, index, count, floor); result.push(place({ id, side, tier, center, y, width: bay, height, depth: podiumDepth(id), floorIndex: floor, bayIndex: index, edge })); if (floor > 0 && settings.ornamentDensity > 0.45 && index % 3 === 1) { result.push(place({ id: "spandrel-panel", side, tier, center, y: y + 0.28, width: bay * 0.82, height: 0.82, depth: 0.54, floorIndex: floor, bayIndex: index, edge })); } } return result; } function groundModule(settings, side, index, count) { const center = Math.floor(count / 2); const edge = index === 0 || index === count - 1; if (side === "front") { if (settings.buildingArchetype === "federal-fortress") return index % 2 === 0 ? "bank-grille" : "barred-window"; if (settings.buildingArchetype === "temple-bank-podium" && Math.abs(index - center) <= 2) return "storefront-curtain-wall"; if ((settings.cornerEntrance || settings.entranceType === "corner-bank" || settings.podiumStyle === "corner-entrance") && edge) { return "corner-entrance"; } if (index === center && settings.entranceType === "center-revolving") return "revolving-door-bay"; if (Math.abs(index - center) === 1 || settings.entranceType === "paired-lobby") return "lobby-door"; if (settings.colonnade && settings.podiumStyle === "colonnade" && index % 2 === 0) return "paired-column"; return "tall-lobby-window"; } if (side === "back") return index % 3 === 0 ? "loading-dock-bay" : "security-door"; if (settings.podiumStyle === "service-bank" && index % 2 === 0) return "loading-dock-bay"; if (edge) return settings.buildingArchetype === "federal-fortress" ? "bank-grille" : "service-door"; return "tall-lobby-window"; } function upperPodiumModule(side, index, count, floor) { if (index === 0 || index === count - 1) return "corner-bay"; if (side === "back" && (index + floor) % 4 === 0) return "blank-bay"; if (floor % 2 === 0 && index % 3 === 1) return "deep-window-well"; return side === "front" ? "tall-lobby-window" : "window-4m"; } function podiumDepth(id) { if (id === "paired-column") return 1.8; if (id === "corner-entrance") return 1.55; if (id === "revolving-door-bay") return 1.5; if (id === "loading-dock-bay") return 1.2; return 1.1; } function podiumHorizontalTrim(tier, side, edge) { const span = edge.length; const top = tier.y0 + tier.height; const result = [ place({ id: "belt-course-large", side, tier, center: edge.center, y: PODIUM_FLOOR_HEIGHT - 0.26, width: span, height: 0.52, depth: 0.72, floorIndex: 1, bayIndex: -1, edge }), place({ id: "large-cornice", side, tier, center: edge.center, y: top - 0.78, width: span, height: 0.78, depth: 1, floorIndex: tier.floors, bayIndex: -1, edge }) ]; if (tier.floors > 2) { result.push(place({ id: "small-cornice", side, tier, center: edge.center, y: PODIUM_FLOOR_HEIGHT * 2 - 0.18, width: span, height: 0.36, depth: 0.62, floorIndex: 2, bayIndex: -1, edge })); } for (const edgeSign of [-1, 1]) { result.push(place({ id: "corner-cornice", side, tier, center: edge.center + edgeSign * (span / 2 - 0.8), y: top - 0.92, width: 1.6, height: 0.92, depth: 1.1, floorIndex: tier.floors, bayIndex: edgeSign < 0 ? 0 : 999, edge })); } result.push(...cornerTrimJoints2(tier, side, edge, PODIUM_FLOOR_HEIGHT - 0.24, "belt-corner-joint", 0.72)); result.push(...cornerTrimJoints2(tier, side, edge, top - 0.82, "cornice-corner-joint", 1.05)); return result; } function cornerTrimJoints2(tier, side, edge, y, id, depth) { return [-1, 1].map((edgeSign) => place({ id, side, tier, center: edge.center + edgeSign * (edge.length / 2 - 0.32), y, width: 0.86, height: id === "cornice-corner-joint" ? 0.9 : 0.46, depth, floorIndex: -9, bayIndex: edgeSign < 0 ? -90 : -91, edge })); } function podiumColumns(settings, tier, side, edge, count) { const result = []; const firstFloorHeight = PODIUM_FLOOR_HEIGHT - PLINTH_HEIGHT + 0.08; if (side === "front" && settings.colonnade && settings.podiumStyle === "colonnade") { const bedY = porticoBedY(tier); const columnHeight = bedY + 0.08; const projection = settings.porticoProjection; const bedWidth = Math.max(edge.length - 0.8, edge.length * 0.9); result.push(place({ id: "large-cornice", side, tier, center: edge.center, y: bedY - 0.08, width: bedWidth, height: 1.05, depth: projection + 1.45, floorIndex: 0, bayIndex: -40, edge, normalOffset: projection * 0.5 })); result.push(place({ id: "dentil-corbel-course", side, tier, center: edge.center, y: bedY - 0.34, width: bedWidth, height: 0.52, depth: projection + 1.2, floorIndex: 0, bayIndex: -41, edge, normalOffset: projection * 0.5 })); for (const seam of [0, count]) { result.push(place({ id: "square-corner-pylon", side, tier, center: edgeSeamCenter(edge, seam, count), y: 0, width: 1.55, height: columnHeight, depth: 1.35, floorIndex: 0, bayIndex: seam, edge, normalOffset: projection })); } for (let index = 1; index < count; index++) { if (index % 2 === 0) continue; result.push(place({ id: "colossal-column", side, tier, center: edgeSeamCenter(edge, index, count), y: 0, width: 1.7, height: columnHeight, depth: 1.7, floorIndex: 0, bayIndex: index, edge, normalOffset: projection })); } return result; } for (let index = 1; index < count; index++) { const center = edgeSeamCenter(edge, index, count); const useRound = side === "front" && settings.colonnade && settings.podiumStyle === "colonnade"; const everyOtherFront = side === "front" && index % 2 === 1; const sidePier = side !== "front" && index % 3 === 0; if (!everyOtherFront && !sidePier) continue; const id = side !== "front" && settings.buildingArchetype === "temple-bank-podium" ? "round-column" : side === "front" && index % 2 === 1 ? "round-column" : useRound ? "round-column" : "square-column"; result.push(place({ id, side, tier, center, y: PLINTH_HEIGHT, width: useRound ? 1.15 : 0.92, height: firstFloorHeight, depth: useRound ? 1.35 : 0.86, floorIndex: 0, bayIndex: index, edge, normalOffset: side === "front" ? settings.porticoProjection * 0.55 : 0 })); } return result; } function frontageDetails(settings, tier, side, edge, count) { if (side !== "front") return []; const y = Math.max(0.8, tier.height - 3.2); const result = []; const projection = settings.podiumStyle === "colonnade" ? settings.porticoProjection : 0; const ceremonialSpan = settings.podiumStyle === "colonnade" ? Math.max(edge.length - 0.8, edge.length * 0.9) : Math.min(edge.length * 0.82, 14); if (settings.buildingArchetype === "temple-bank-podium" || settings.buildingArchetype === "board-of-trade-tower") { result.push(place({ id: "triangular-pediment", side, tier, center: edge.center, y: tier.height - 0.2, width: ceremonialSpan, height: 3, depth: projection + 1.55, floorIndex: 10, bayIndex: -10, edge, normalOffset: projection * 0.5 })); result.push(place({ id: "company-frieze", side, tier, center: edge.center, y: tier.height - 1, width: ceremonialSpan * 0.96, height: 0.9, depth: projection + 0.9, floorIndex: 10, bayIndex: -11, edge, normalOffset: projection * 0.5 })); result.push(place({ id: "pediment-eagle", side, tier, center: edge.center, y: tier.height + 0.75, width: 2.4, height: 1.2, depth: 0.45, floorIndex: 10, bayIndex: -12, edge, normalOffset: projection + 0.12 })); for (const sideSign of [-1, 1]) { result.push(place({ id: "acroterion-scroll", side, tier, center: edge.center + sideSign * Math.min(edge.length * 0.36, 6), y: tier.height + 1.35, width: 0.9, height: 1, depth: 0.5, floorIndex: 10, bayIndex: -13 + sideSign, edge, normalOffset: projection + 0.18 })); } result.push(place({ id: "clock-medallion", side, tier, center: edge.center, y, width: 1.8, height: 1.8, depth: 0.5, floorIndex: 9, bayIndex: -20, edge, normalOffset: 0.12 })); } result.push(place({ id: "wall-plaque", side, tier, center: edgeBayCenter(edge, Math.max(0, count - 2), count), y: 1.65, width: 1.15, height: 1.4, depth: 0.28, floorIndex: 0, bayIndex: -21, edge })); result.push(place({ id: "address-plaque", side, tier, center: edgeBayCenter(edge, 1, count), y: 1.3, width: 0.85, height: 0.55, depth: 0.2, floorIndex: 0, bayIndex: -22, edge })); result.push(place({ id: "flag-mount", side, tier, center: edge.center - edge.length * 0.32, y: 3.1, width: 1.35, height: 1.6, depth: 0.7, floorIndex: 0, bayIndex: -23, edge })); result.push(place({ id: "wall-lamp", side, tier, center: edge.center + edge.length * 0.28, y: 2.25, width: 0.55, height: 1.4, depth: 0.65, floorIndex: 0, bayIndex: -24, edge })); if (settings.buildingArchetype === "federal-fortress") { result.push(place({ id: "wall-camera", side, tier, center: edge.center + edge.length * 0.36, y: 3.9, width: 0.55, height: 0.45, depth: 0.7, floorIndex: 0, bayIndex: -25, edge })); } result.push(place({ id: "sidewalk-entry", side, tier, center: edge.center, y: -0.02, width: Math.min(5.2, edge.length * 0.38), height: 0.45, depth: 1.4, floorIndex: -2, bayIndex: -26, edge, normalOffset: projection + 0.45 })); for (let index = 1; index < count; index += 3) { result.push(place({ id: "bollard", side, tier, center: edgeSeamCenter(edge, index, count), y: -0.02, width: 0.45, height: 1.1, depth: 0.45, floorIndex: -2, bayIndex: -30 - index, edge })); } return result; } function porticoBedY(tier) { return Math.min(tier.height - 3.2, PODIUM_FLOOR_HEIGHT * 2 - 0.08); } function createRoofPlacements(settings, tier) { const y = tier.y0 + tier.height + 0.12; if (settings.roofStyle === "pyramidal-metal" || settings.roofStyle === "statue-tower") { return [ roofPlace({ id: "sloped-metal-roof", center: tier.x, roofZ: tier.z, y, width: tier.width * 0.92, height: Math.min(6.2, tier.depth * 0.32), depth: tier.depth * 0.92, bayIndex: 0 }), ...settings.roofStyle === "statue-tower" ? [ roofPlace({ id: "roof-lantern", center: tier.x, roofZ: tier.z, y: y + Math.min(6.2, tier.depth * 0.32), width: 2.4, height: 1.8, depth: 2.4, bayIndex: 1 }), roofPlace({ id: "roof-statue-mast", center: tier.x, roofZ: tier.z, y: y + Math.min(8, tier.depth * 0.32 + 1.5), width: 1.7, height: 5.8, depth: 1.7, bayIndex: 2 }) ] : [ roofPlace({ id: "roof-crest", center: tier.x, roofZ: tier.z, y: y + Math.min(6.2, tier.depth * 0.32), width: 1.4, height: 2.4, depth: 1.4, bayIndex: 2 }) ] ]; } const result = [ ...terraceRailings(tier, y, 0) ]; if (settings.roofEquipmentDensity > 0.12) { result.push(roofPlace({ id: "roof-mech-box", center: tier.x - tier.width * 0.22, roofZ: tier.z - tier.depth * 0.04, y, width: 3.6, height: 2, depth: 2.8, bayIndex: 2 })); } if (settings.roofEquipmentDensity > 0.32) { result.push(roofPlace({ id: "hvac-cluster", center: tier.x + tier.width * 0.2, roofZ: tier.z - tier.depth * 0.12, y, width: 4.3, height: 2.2, depth: 3, bayIndex: 3 })); } if (settings.roofEquipmentDensity > 0.58) { result.push(roofPlace({ id: "roof-mech-box", center: tier.x, roofZ: tier.z + tier.depth * 0.22, y, width: 2.6, height: 1.55, depth: 2.2, bayIndex: 4 })); } if (settings.roofEquipmentDensity > 0.66) { result.push(roofPlace({ id: "antenna", center: tier.x + tier.width * 0.36, roofZ: tier.z + tier.depth * 0.28, y, width: 0.8, height: 4.6, depth: 0.8, bayIndex: 5 })); } return result; } function terraceRailings(tier, y, offset) { return [ roofPlace({ id: "roof-railing", center: tier.x - tier.width * 0.18, roofZ: tier.z - tier.depth * 0.44, y, width: tier.width * 0.58, height: 1.1, depth: 0.4, bayIndex: offset }), roofPlace({ id: "roof-railing", center: tier.x + tier.width * 0.18, roofZ: tier.z + tier.depth * 0.44, y, width: tier.width * 0.58, height: 1.1, depth: 0.4, bayIndex: offset + 1 }) ]; } function createShaftPlacements(settings, tier, side) { return exposedEdges(tier, side).flatMap((edge) => createShaftEdgePlacements(settings, tier, side, edge)); } function createShaftEdgePlacements(settings, tier, side, edge) { const count = edgeBayCount(edge, 4); const bay = edge.length / count; const result = []; result.push(...verticalZones(settings, tier, side, edge, count)); for (let floor = 0; floor < tier.floors; floor++) { for (let index = 0; index < count; index++) { if (isReservedVerticalZone(settings, side, index, count)) continue; const center = edgeBayCenter(edge, index, count); result.push(...shaftBay(settings, tier, side, edge, center, bay, index, count, floor)); } result.push(...floorTrim(tier, side, edge, floor)); } result.push(...verticalPilasters(settings, tier, side, edge, count)); result.push(...tierTrim(tier, side, edge)); return result; } function shaftBay(settings, tier, side, edge, center, bay, index, count, floor) { const id = shaftModule(settings, side, index, count, floor); const y = tier.y0 + floor * FLOOR_HEIGHT; const ornamental = id !== "corner-bay" && id !== "blank-bay" && settings.ornamentDensity > 0.52 && (floor + index) % 5 === 2; if (!ornamental) { return [place({ id, side, tier, center, y, width: bay, height: FLOOR_HEIGHT, depth: id === "corner-bay" ? 1.08 : 0.84, floorIndex: floor, bayIndex: index, edge })]; } const upperId = id === "double-window-bay" ? "double-window-bay" : "window-3m"; return [ place({ id: (floor + index) % 2 === 0 ? "carved-spandrel-vine" : "spandrel-panel", side, tier, center, y: y + 0.18, width: bay * 0.84, height: 0.82, depth: 0.56, floorIndex: floor, bayIndex: index, edge }), place({ id: upperId, side, tier, center, y: y + 0.92, width: bay, height: FLOOR_HEIGHT - 0.96, depth: 0.78, floorIndex: floor, bayIndex: index, edge }) ]; } function shaftModule(settings, side, index, count, floor) { const edge = index === 0 || index === count - 1; if (edge) return cornerPierId(settings); if (side === "back" && floor % 5 === 0 && index % 3 === 1) return "structural-blank-wall"; if (side !== "front" && index === Math.floor(count / 2)) return "recessed-window-slot"; if (settings.buildingArchetype === "terra-cotta-arcade") return floor % 4 === 0 ? "arcade-bay" : floor % 2 === 0 ? "arched-window-bay" : "brick-window-bay"; if (settings.buildingArchetype === "federal-fortress") return floor % 4 === 0 ? "deep-window-well" : "window-3m"; if (floor % 5 === 0 && index % 4 === 1) return "double-window-bay"; if (settings.shaftRhythm === "paired") return index % 2 === 0 ? "double-window-bay" : "window-3m"; if (settings.shaftRhythm === "chicago-grid") return index % 3 === 1 ? "window-4m" : "window-3m"; return floor % 4 === 0 && index % 3 === 0 ? "double-window-bay" : "window-4m"; } function floorTrim(tier, side, edge, floor) { const y = tier.y0 + floor * FLOOR_HEIGHT; const span = edge.length; return [ place({ id: "floor-band-strip", side, tier, center: edge.center, y: y - 0.16, width: span, height: 0.36, depth: 0.54, floorIndex: floor, bayIndex: -1, edge }), place({ id: "window-sill-strip", side, tier, center: edge.center, y: y + 0.56, width: span, height: 0.24, depth: 0.58, floorIndex: floor, bayIndex: -2, edge }), place({ id: "lintel-strip", side, tier, center: edge.center, y: y + FLOOR_HEIGHT - 0.48, width: span, height: 0.26, depth: 0.54, floorIndex: floor, bayIndex: -3, edge }), ...cornerTrimJoints3(tier, side, edge, y - 0.16, "belt-corner-joint", 0.58) ]; } function verticalPilasters(settings, tier, side, edge, count) { const result = []; const cadence = settings.shaftRhythm === "paired" ? 2 : 3; for (let index = 1; index < count; index++) { if (index % cadence !== 0) continue; result.push(place({ id: tier.floors > 4 ? "pilaster-bundle" : "vertical-pilaster-strip", side, tier, center: edgeSeamCenter(edge, index, count), y: tier.y0, width: 0.58, height: tier.height, depth: 0.72, floorIndex: -1, bayIndex: index, edge })); } return result; } function tierTrim(tier, side, edge) { const span = edge.length; return [ place({ id: "belt-course-large", side, tier, center: edge.center, y: tier.y0 - 0.34, width: span, height: 0.54, depth: 0.74, floorIndex: -1, bayIndex: -4, edge }), place({ id: "dentil-corbel-course", side, tier, center: edge.center, y: tier.y0 + tier.height - 0.3, width: span, height: 0.36, depth: 0.58, floorIndex: tier.floors, bayIndex: -5, edge }), place({ id: "belt-course-small", side, tier, center: edge.center, y: tier.y0 + Math.max(FLOOR_HEIGHT, tier.height * 0.5), width: span, height: 0.32, depth: 0.52, floorIndex: Math.floor(tier.floors / 2), bayIndex: -6, edge }), ...cornerTrimJoints3(tier, side, edge, tier.y0 - 0.34, "belt-corner-joint", 0.74), ...cornerTrimJoints3(tier, side, edge, tier.y0 + tier.height - 0.3, "cornice-corner-joint", 0.88) ]; } function cornerTrimJoints3(tier, side, edge, y, id, depth) { return [-1, 1].map((edgeSign) => place({ id, side, tier, center: edge.center + edgeSign * (edge.length / 2 - 0.32), y, width: 0.86, height: id === "cornice-corner-joint" ? 0.88 : 0.42, depth, floorIndex: -9, bayIndex: edgeSign < 0 ? -90 : -91, edge })); } function verticalZones(settings, tier, side, edge, count) { const result = []; const center = Math.floor(count / 2); const bay = edge.length / count; if (side === "front" && (settings.buildingArchetype === "board-of-trade-tower" || settings.buildingArchetype === "high-rise-pyramid")) { const axisBays = clampedOddAxisBays(settings.centralAxisBays, count); const halfAxis = Math.floor(axisBays / 2); result.push(place({ id: "central-glass-shaft", side, tier, center: edgeBayCenter(edge, center, count), y: tier.y0, width: bay * axisBays, height: tier.height, depth: 1.02, floorIndex: -1, bayIndex: center, edge })); for (const pierIndex of [center - halfAxis - 1, center + halfAxis + 1]) { if (pierIndex <= 0 || pierIndex >= count - 1) continue; result.push(place({ id: "solid-side-pier", side, tier, center: edgeBayCenter(edge, pierIndex, count), y: tier.y0, width: bay, height: tier.height, depth: 0.86, floorIndex: -1, bayIndex: pierIndex, edge })); } } if (side !== "front") { const serviceIndex = side === "back" ? center : Math.max(1, Math.floor(count * 0.35)); const slotIndex = Math.min(count - 2, serviceIndex + 1); result.push(place({ id: "structural-blank-wall", side, tier, center: edgeBayCenter(edge, serviceIndex, count), y: tier.y0, width: bay, height: tier.height, depth: 0.62, floorIndex: -1, bayIndex: serviceIndex, edge })); if (slotIndex !== serviceIndex) { result.push(place({ id: "recessed-window-slot", side, tier, center: edgeBayCenter(edge, slotIndex, count), y: tier.y0, width: bay, height: tier.height, depth: 0.95, floorIndex: -1, bayIndex: slotIndex, edge })); } } for (const cornerIndex of [0, count - 1]) { result.push(place({ id: cornerPierId(settings), side, tier, center: edgeBayCenter(edge, cornerIndex, count), y: tier.y0, width: bay, height: tier.height, depth: 1.1, floorIndex: -1, bayIndex: cornerIndex, edge, moduleVariant: cornerIndex === 0 ? "start" : "end" })); } return result; } function isReservedVerticalZone(settings, side, index, count) { const center = Math.floor(count / 2); if (side === "front" && (settings.buildingArchetype === "board-of-trade-tower" || settings.buildingArchetype === "high-rise-pyramid")) { const axisBays = clampedOddAxisBays(settings.centralAxisBays, count); const halfAxis = Math.floor(axisBays / 2); return Math.abs(index - center) <= halfAxis || index === center - halfAxis - 1 || index === center + halfAxis + 1 || index === 0 || index === count - 1; } if (side !== "front") { const serviceIndex = side === "back" ? center : Math.max(1, Math.floor(count * 0.35)); const slotIndex = Math.min(count - 2, serviceIndex + 1); return index === serviceIndex || slotIndex !== serviceIndex && index === slotIndex || index === 0 || index === count - 1; } return index === 0 || index === count - 1; } function cornerPierId(settings) { return settings.cornerTreatment === "rounded-piers" ? "rounded-corner-pier" : "wrapped-corner-pier"; } function clampedOddAxisBays(value, count) { const max = Math.max(1, Math.min(5, count - 4)); const oddMax = max % 2 === 0 ? max - 1 : max; const requested = Math.max(1, Math.round(value)); const oddRequested = requested % 2 === 0 ? requested + 1 : requested; return Math.max(1, Math.min(oddRequested, Math.max(1, oddMax))); } function createKitPlacements(settings, tiers) { const placements = tiers.flatMap( (tier) => facadeSides.flatMap((side) => { if (tier.role === "podium") return createPodiumPlacements(settings, tier, side); if (tier.role === "crown") return createCrownPlacements(settings, tier, side); return createShaftPlacements(settings, tier, side); }) ); for (const tier of roofHostTiers(tiers)) placements.push(...createRoofPlacements(settings, tier)); return placements; } function roofHostTiers(tiers) { const crowns = topTiers(tiers.filter((tier) => tier.role === "crown")); if (crowns.length > 0) return crowns; return topTiers(tiers.filter((tier) => tier.role === "shaft" || tier.role === "bridge")); } function topTiers(tiers) { if (tiers.length === 0) return []; const maxTop = Math.max(...tiers.map((tier) => tier.y0 + tier.height)); return tiers.filter((tier) => Math.abs(tier.y0 + tier.height - maxTop) < 1e-3); } var financialCoreModules = [ module("round-column", "Round column", "base", "Fluted column with stacked torus/plinth rings and cap blocks.", [1.3, 6.2, 1.3]), module("square-column", "Square column", "base", "Panelized square pier with bevelled base and capital courses.", [1.35, 6.1, 1.1]), module("paired-column", "Paired column", "base", "Two fluted columns carrying a shared classical entablature.", [4.4, 5.8, 1.5]), module("tall-lobby-window", "Tall lobby window", "base", "Tall bronze storefront window with deep limestone surround.", [3.2, 5.3, 0.9]), module("lobby-door", "Lobby door", "base", "Bronze double door with transom, lintel, and carved stone portal.", [3.4, 5.4, 1]), module("revolving-door-bay", "Revolving door bay", "base", "Curved bronze/glass revolving entry set inside stone side columns.", [4.2, 5.6, 1.4]), module("service-door", "Service door", "base", "Narrow black-metal service door with transom grille and stone jambs.", [2.8, 4.8, 0.9]), module("loading-dock-bay", "Loading dock bay", "base", "Roll-up loading shutter with heavy industrial lintel and side bollards.", [4.2, 4.9, 1]), module("granite-plinth", "Granite plinth", "base", "Dark granite foundation block with limestone upper cap.", [3.2, 1.15, 0.7]), module("corner-entrance", "Corner entrance", "base", "Wrapped corner portal with two street-facing bronze doors.", [4.4, 5.6, 1.6]), module("colossal-column", "Colossal column", "base", "Ten-meter fluted entry-order column with separate plinth, shaft, and ionic capital.", [1.7, 10.4, 1.7]), module("square-corner-pylon", "Square corner pylon", "base", "Massive square portico end pylon paired with a round column order.", [1.8, 10.2, 1.5]), module("triangular-pediment", "Triangular pediment", "base", "Full-depth temple-bank pediment with raking cornice and tympanum face.", [10, 3, 1.5]), module("pediment-eagle", "Pediment eagle/seal", "base", "Raised institutional eagle and shield relief for pediment centers.", [2.2, 1.1, 0.35]), module("acroterion-scroll", "Acroterion scroll", "base", "Corner scroll ornament for pediment and parapet ends.", [0.9, 1, 0.45]), module("company-frieze", "Company frieze", "base", "Long carved company-name frieze tablet below a bank pediment.", [8, 0.9, 0.45]), module("clock-medallion", "Clock medallion", "base", "Large round clock/medallion centerpiece with stone surround.", [2, 2, 0.45]), module("rusticated-base-block", "Rusticated base block", "base", "Deep rusticated stone block with horizontal courses for fortress bases.", [3.2, 2.4, 0.9]), module("bank-grille", "Bank grille", "base", "Large barred banking hall grille set into a heavy stone surround.", [3.4, 4.4, 0.9]), module("barred-window", "Metal-barred window", "base", "Small secure ground-floor window with bronze/black security bars.", [2.6, 2.8, 0.72]), module("storefront-curtain-wall", "Storefront curtain wall", "base", "Tall black bronze/glass wall set behind a column portico.", [4.6, 7, 0.7]), module("security-door", "Security door", "base", "Heavy institutional security door with grille transom and side stone reveals.", [2.8, 4.4, 0.9]), module("wall-plaque", "Wall plaque", "base", "Mounted bronze/stone bank plaque integrated into the facade.", [1.2, 1.4, 0.22]), module("address-plaque", "Address plaque", "base", "Small address number plaque with raised bronze numerals.", [0.9, 0.55, 0.18]), module("flag-mount", "Flag/banner mount", "base", "Angled flag/banner bracket mounted to stone facade.", [1.4, 1.6, 0.5]), module("bollard", "Bollard", "base", "Short black street bollard for bank frontage security.", [0.45, 1.1, 0.45]), module("wall-camera", "Wall camera", "base", "Small security camera pod and wall arm.", [0.55, 0.45, 0.7]), module("wall-lamp", "Wall lamp", "base", "Classical wall lamp with bracket and glass globe.", [0.55, 1.4, 0.6]), module("sidewalk-entry", "Sidewalk plinth/entry", "base", "Stone sidewalk threshold with curb, stair lip, and door landing.", [4.2, 0.45, 1.4]), module("window-3m", "3m window bay", "shaft", "Narrow office bay with bronze sash, sill, spandrel, and pilaster sides.", [3, 3.35, 0.72]), module("window-4m", "4m window bay", "shaft", "Wide office bay with paired vertical bronze sash and carved spandrel.", [4, 3.35, 0.74]), module("double-window-bay", "Double-window bay", "shaft", "Twin office windows divided by a central stone mullion and rosette spandrels.", [4.2, 3.35, 0.78]), module("blank-bay", "Blank bay", "shaft", "Stone service bay with framed slab panels and vertical reveal grooves.", [3.2, 3.35, 0.5]), module("corner-bay", "Corner bay", "shaft", "Wrapped corner bay with return-facing window and shared corner pilaster.", [3.2, 3.35, 1.2]), module("vertical-pilaster-strip", "Vertical pilaster strip", "shaft", "Reusable fluted pilaster with base block, cap, and inset channels.", [0.72, 3.35, 0.52]), module("spandrel-panel", "Spandrel panel", "shaft", "Carved floral medallion panel for between-window courses.", [3.4, 1, 0.42]), module("floor-band-strip", "Floor-band strip", "shaft", "Greek-key floor divider band with cap and base fillets.", [4, 0.58, 0.42]), module("central-glass-shaft", "Central glass shaft", "shaft", "Multi-floor recessed black-glass vertical slot framed by stone piers.", [5.6, 13.4, 1]), module("solid-side-pier", "Solid side pier", "shaft", "Tall blank structural pier flanking a central vertical shaft.", [1.4, 13.4, 0.86]), module("buttress-pier", "Buttress pier", "shaft", "Projecting multi-floor buttress pier with stepped cap blocks.", [1.1, 10, 1]), module("pilaster-bundle", "Pilaster bundle", "shaft", "Grouped vertical pilasters spanning multiple floors.", [1.2, 10, 0.74]), module("recessed-window-slot", "Recessed window slot", "shaft", "Deep continuous vertical window slot with narrow stacked panes.", [2.2, 10, 0.95]), module("arched-window-bay", "Arched window bay", "shaft", "Arched stone window bay with voussoirs and bronze sash.", [3.2, 4, 0.85]), module("arcade-bay", "Romanesque arcade bay", "shaft", "Tall arched arcade bay with cylindrical colonnettes and deep reveal.", [3.6, 5.2, 1.1]), module("brick-window-bay", "Terra-cotta window bay", "shaft", "Red masonry/terra-cotta bay with black sash and brick coursing.", [3.2, 3.35, 0.7]), module("deep-window-well", "Deep window well", "shaft", "Deeply recessed window surround with stepped sill/lintel hierarchy.", [3.2, 3.35, 1]), module("carved-spandrel-vine", "Carved vine spandrel", "shaft", "Decorative carved spandrel panel with vine/leaf relief.", [3.4, 1, 0.42]), module("wrapped-corner-pier", "Wrapped corner pier", "shaft", "Structural corner pier wrapping both facade directions without stretching.", [1.2, 10, 1.2]), module("rounded-corner-pier", "Rounded corner pier", "shaft", "Rounded/chamfered structural corner pier for Chicago financial tower corners.", [1.4, 10, 1.35]), module("structural-blank-wall", "Structural blank wall", "shaft", "Tall blank wall zone for party walls, service cores, and side facades.", [3.2, 10, 0.62]), module("belt-course-small", "Belt course small", "trim", "Slim belt course with bevelled upper/lower beads.", [4, 0.34, 0.42]), module("belt-course-large", "Belt course large", "trim", "Large ornamented belt course with dentils and carved frieze panels.", [4, 0.72, 0.62]), module("window-sill-strip", "Window sill strip", "trim", "Projecting sill strip with underside drip return.", [3.4, 0.28, 0.5]), module("lintel-strip", "Lintel strip", "trim", "Horizontal lintel strip with panel seams and raised edge fillets.", [3.4, 0.38, 0.48]), module("small-cornice", "Small cornice", "trim", "Short projecting cornice with dentil underside.", [4, 0.72, 0.85]), module("large-cornice", "Large cornice", "trim", "Deep classical cornice with bracket blocks and carved frieze.", [4.6, 1.1, 1.1]), module("corner-cornice", "Corner cornice", "trim", "L-shaped cornice section wrapping around a building corner.", [4.2, 1.1, 1.4]), module("belt-corner-joint", "Belt corner joint", "trim", "Authored L/corner connector for horizontal belt and sill courses.", [1.3, 0.52, 1.3]), module("cornice-corner-joint", "Cornice corner joint", "trim", "Heavy projecting corner block that joins cornice runs without open slab ends.", [1.6, 0.95, 1.6]), module("dentil-corbel-course", "Dentil corbel course", "trim", "Course-aligned dentil/corbel band with bracket rhythm.", [4, 0.95, 0.9]), module("crown-window-bay", "Crown window bay", "crown", "Short attic/crown bay with heavy pilasters and parapet rail blocks.", [3.6, 3, 0.85]), module("parapet-section", "Parapet section", "crown", "Ornamented parapet wall with raised piers and inset panels.", [4, 2, 0.72]), module("corner-parapet", "Corner parapet", "crown", "L-shaped parapet corner with cap blocks and round finial bases.", [3.6, 2.1, 1.2]), module("attic-crest-panel", "Attic crest panel", "crown", "Raised top crest with small rosette tablets and a projecting cap course.", [4, 1, 0.82]), module("crown-pediment", "Crown pediment", "crown", "Stepped central pediment block for bank tower crowns.", [4.8, 1.45, 1]), module("corner-finial", "Corner finial", "crown", "Small limestone urn/obelisk finial for parapet corners.", [0.8, 1.6, 0.8]), module("crown-urn-finial", "Crown urn finial", "crown", "Rounded classical urn finial for dense crown decoration rhythms.", [0.9, 1.4, 0.9]), module("crown-obelisk-finial", "Crown obelisk finial", "crown", "Tall stepped obelisk finial for skyline crown silhouettes.", [0.85, 2.2, 0.85]), module("crown-pillar-finial", "Crown pillar finial", "crown", "Small repeated parapet pillar/mini-pylon for edge finial rhythms.", [0.7, 1.2, 0.7]), module("crown-cartouche-panel", "Crown cartouche panel", "crown", "Raised cartouche/parapet relief panel used as a crown decoration module.", [3, 1.25, 0.74]), module("sloped-metal-roof", "Sloped metal roof", "roof", "Pyramidal/hipped standing-seam metal roof with ribs, hips, and ridge cap.", [10, 4, 10]), module("roof-statue-mast", "Roof statue mast", "roof", "Board-of-trade style roof sculpture mast on a stone/metal pedestal.", [1.6, 5.8, 1.6]), module("roof-lantern", "Roof lantern", "roof", "Open steel lantern/platform frame below a sculpture mast.", [2.4, 1.8, 2.4]), module("roof-crest", "Roof crest", "roof", "Ornamental roof crest/flagpole base for skyline silhouettes.", [1.4, 2.4, 1.4]), module("roof-mech-box", "Roof mech box", "roof", "Panelized rooftop mechanical box with vent grilles and cap slab.", [3.6, 2, 2.8]), module("hvac-cluster", "HVAC cluster", "roof", "Grouped cylindrical fans and rectangular condenser boxes.", [4.2, 2.1, 2.8]), module("roof-railing", "Roof railing", "roof", "Bronze roof safety railing with posts and horizontal bars.", [4.4, 1.15, 0.42]), module("antenna", "Antenna", "roof", "Stacked mast antenna with collars and small equipment nodes.", [0.8, 4.8, 0.8]) ]; function module(id, label, group, description, size) { return { id, label, group, description, implemented: true, quality: "kit-ready", defaultSize: { width: size[0], height: size[1], depth: size[2] } }; } var stone = [0.9, 0.84, 0.73]; var stoneDark = [0.58, 0.54, 0.47]; var stoneWarm = [0.78, 0.7, 0.6]; var granite = [0.25, 0.24, 0.23]; var glass = [0.018, 0.026, 0.03]; var bronze = [0.7, 0.45, 0.2]; var bronzeDark = [0.24, 0.15, 0.08]; var blackMetal = [0.035, 0.034, 0.032]; var roofMetal = [0.18, 0.19, 0.18]; function profiledCylinder(input) { const segments = input.segments ?? 40; for (let index = 0; index < segments; index++) { const a0 = index / segments * Math.PI * 2; const a1 = (index + 1) / segments * Math.PI * 2; const r0 = fluteRadius(a0, input.radius, input.flutes); const r1 = fluteRadius(a1, input.radius, input.flutes); quad(input.ctx, input.slot, [ point(input, a1, r1, input.y0), point(input, a0, r0, input.y0), point(input, a0, r0, input.y1), point(input, a1, r1, input.y1) ], input.color); } } function stackedColumn(input) { const h = input.y1 - input.y0; const bands = [ [0, 0.06, input.radius * 1.38], [0.06, 0.12, input.radius * 1.22], [0.12, 0.18, input.radius * 1.02], [0.18, 0.84, input.radius], [0.84, 0.9, input.radius * 1.06], [0.9, 0.95, input.radius * 1.24], [0.95, 1, input.radius * 1.42] ]; for (const [a, b, radius] of bands) { profiledCylinder({ ctx: input.ctx, slot: "limestone", x: input.x, z: input.z, y0: input.y0 + h * a, y1: input.y0 + h * b, radius, color: input.color, segments: radius === input.radius ? 56 : 44, flutes: radius === input.radius ? 18 : void 0 }); } } function point(input, angle, radius, y) { return [ input.x + Math.cos(angle) * radius, y, input.z + Math.sin(angle) * radius ]; } function quad(ctx, slot, points, color) { ctx.writer.appendQuad(slot, points.map(ctx.transform), color); } function fluteRadius(angle, radius, flutes) { if (!flutes) return radius; return radius * (0.93 + Math.max(0, Math.sin(angle * flutes)) * 0.07); } function slab(ctx, slot, rect, color) { const { x0, x1, y0, y1, z0, z1 } = rect; quad2(ctx, slot, [[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]], color); quad2(ctx, slot, [[x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0]], color); quad2(ctx, slot, [[x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1]], color); quad2(ctx, slot, [[x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1]], color); quad2(ctx, slot, [[x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0]], color); } function solidBox(ctx, slot, min, max, color) { const [x0, y0, z0] = min; const [x1, y1, z1] = max; quad2(ctx, slot, [[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]], color); quad2(ctx, slot, [[x1, y0, z0], [x0, y0, z0], [x0, y1, z0], [x1, y1, z0]], color); quad2(ctx, slot, [[x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1]], color); quad2(ctx, slot, [[x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0]], color); quad2(ctx, slot, [[x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0]], color); quad2(ctx, slot, [[x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1]], color); } function bevelBlock(ctx, slot, min, max, bevel, color) { solidBox(ctx, slot, [min[0] + bevel, min[1] + bevel, min[2]], [max[0] - bevel, max[1] - bevel, max[2]], color); slab(ctx, slot, { x0: min[0], x1: min[0] + bevel, y0: min[1] + bevel, y1: max[1] - bevel, z0: min[2], z1: max[2] }, color); slab(ctx, slot, { x0: max[0] - bevel, x1: max[0], y0: min[1] + bevel, y1: max[1] - bevel, z0: min[2], z1: max[2] }, color); slab(ctx, slot, { x0: min[0] + bevel, x1: max[0] - bevel, y0: min[1], y1: min[1] + bevel, z0: min[2], z1: max[2] }, color); slab(ctx, slot, { x0: min[0] + bevel, x1: max[0] - bevel, y0: max[1] - bevel, y1: max[1], z0: min[2], z1: max[2] }, color); } function quad2(ctx, slot, points, color) { ctx.writer.appendQuad( slot, points.map(ctx.transform), color ); } var roundColumnModule = (ctx) => { stackedColumn({ ctx, x: 0, z: 0.18, y0: 0, y1: ctx.height, radius: Math.min(ctx.width, ctx.depth) * 0.28, color: stone }); ctx.anchors.capital = [0, ctx.height * 0.92, 0.18]; }; var squareColumnModule = (ctx) => { const w = ctx.width / 2; bevelBlock(ctx, "limestone", [-w, 0, -0.1], [w, ctx.height, 0.42], 0.08, stone); for (let index = 1; index < 6; index++) { const y = ctx.height / 6 * index; slab(ctx, "limestone", { x0: -w + 0.08, x1: w - 0.08, y0: y - 0.025, y1: y + 0.025, z0: 0.43, z1: 0.5 }, stoneDark); } slab(ctx, "limestone", { x0: -w - 0.12, x1: w + 0.12, y0: 0, y1: 0.34, z0: -0.16, z1: 0.58 }, stoneWarm); slab(ctx, "limestone", { x0: -w - 0.14, x1: w + 0.14, y0: ctx.height - 0.48, y1: ctx.height, z0: -0.16, z1: 0.6 }, stoneWarm); }; var pairedColumnModule = (ctx) => { const columnY0 = 0.15; const columnY1 = ctx.height - 0.62; for (const x of [-ctx.width * 0.28, ctx.width * 0.28]) { stackedColumn({ ctx, x, z: 0.22, y0: columnY0, y1: columnY1, radius: 0.3, color: stone }); solidBox(ctx, "granite", [x - 0.48, 0, -0.22], [x + 0.48, 0.24, 0.68], stoneDark); } slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: ctx.height - 0.72, y1: ctx.height - 0.28, z0: -0.12, z1: 0.72 }, stoneWarm); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.12, x1: ctx.width / 2 + 0.12, y0: ctx.height - 0.28, y1: ctx.height, z0: -0.18, z1: 0.84 }, stone); solidBox(ctx, "bronze", [-0.08, 0.35, 0], [0.08, ctx.height - 0.95, 0.08], bronze); }; function dentils(ctx, x0, x1, y0, y1, z0, z1, count) { const step = (x1 - x0) / count; for (let index = 0; index < count; index += 2) { slab(ctx, "ornament", { x0: x0 + step * index + step * 0.18, x1: x0 + step * (index + 1) - step * 0.18, y0, y1, z0, z1 }, stone); } } function greekKey(ctx, x0, x1, y, z0, z1, count) { const step = (x1 - x0) / count; for (let index = 0; index < count; index++) { const left = x0 + index * step; slab(ctx, "ornament", { x0: left + step * 0.08, x1: left + step * 0.9, y0: y, y1: y + 0.08, z0, z1 }, stoneDark); slab(ctx, "ornament", { x0: left + step * 0.08, x1: left + step * 0.18, y0: y, y1: y + 0.32, z0, z1 }, stoneDark); slab(ctx, "ornament", { x0: left + step * 0.18, x1: left + step * 0.72, y0: y + 0.24, y1: y + 0.32, z0, z1 }, stoneDark); slab(ctx, "ornament", { x0: left + step * 0.62, x1: left + step * 0.72, y0: y + 0.12, y1: y + 0.32, z0, z1 }, stoneDark); } } function rosette(ctx, x, y, z0, z1, radius) { for (let index = 0; index < 12; index++) { const angle = index / 12 * Math.PI * 2; const cx = x + Math.cos(angle) * radius * 0.42; const cy = y + Math.sin(angle) * radius * 0.42; slab(ctx, "ornament", { x0: cx - radius * 0.06, x1: cx + radius * 0.06, y0: cy - radius * 0.18, y1: cy + radius * 0.18, z0, z1 }, stone); } slab(ctx, "ornament", { x0: x - radius * 0.14, x1: x + radius * 0.14, y0: y - radius * 0.14, y1: y + radius * 0.14, z0, z1 }, stoneDark); } function bronzeMullions(ctx, x0, x1, y0, y1, z0, z1, splits) { for (let index = 1; index < splits; index++) { const x = x0 + (x1 - x0) / splits * index; slab(ctx, "bronze", { x0: x - 0.035, x1: x + 0.035, y0, y1, z0, z1 }, bronze); } const mid = y0 + (y1 - y0) * 0.58; slab(ctx, "bronze", { x0, x1, y0: mid - 0.035, y1: mid + 0.035, z0, z1 }, bronzeDark); } function framedWindow(input) { const { ctx, x0, x1, y0, y1, side, top, bottom } = input; slab(ctx, "limestone", { x0, x1: x0 + side, y0, y1, z0: -0.08, z1: 0.28 }, stone); slab(ctx, "limestone", { x0: x1 - side, x1, y0, y1, z0: -0.08, z1: 0.28 }, stone); slab(ctx, "limestone", { x0: x0 + side, x1: x1 - side, y0: y1 - top, y1, z0: -0.08, z1: 0.26 }, stone); slab(ctx, "limestone", { x0: x0 + side, x1: x1 - side, y0, y1: y0 + bottom, z0: -0.08, z1: 0.22 }, stoneWarm); const apertureX0 = x0 + side; const apertureX1 = x1 - side; const apertureY0 = y0 + bottom; const apertureY1 = y1 - top; slab(ctx, "black-metal", { x0: apertureX0 - 0.04, x1: apertureX1 + 0.04, y0: apertureY0 - 0.04, y1: apertureY1 + 0.04, z0: -0.1, z1: 0.04 }, blackMetal); slab(ctx, "glass", { x0: apertureX0 - 0.01, x1: apertureX1 + 0.01, y0: apertureY0 - 0.01, y1: apertureY1 + 0.01, z0: 0.02, z1: 0.08 }, glass); bronzeMullions(ctx, apertureX0 + 0.04, apertureX1 - 0.04, apertureY0 + 0.04, apertureY1 - 0.04, 0.08, 0.18, input.splits); if (input.spandrel) { const sy0 = y0 + 0.12; const sy1 = y0 + bottom - 0.14; slab(ctx, "ornament", { x0: apertureX0, x1: apertureX1, y0: sy0, y1: sy1, z0: 0.1, z1: 0.22 }, stone); rosette(ctx, (apertureX0 + apertureX1) / 2, (sy0 + sy1) / 2, 0.22, 0.34, Math.min(apertureX1 - apertureX0, sy1 - sy0) * 0.32); } } function bronzeDoor(input) { const { ctx, x0, x1, y0, y1 } = input; slab(ctx, "black-metal", { x0: x0 - 0.03, x1: x1 + 0.03, y0: y0 - 0.03, y1: y1 + 0.03, z0: -0.1, z1: 0.02 }, blackMetal); slab(ctx, input.shutter ? "black-metal" : "glass", { x0, x1, y0, y1, z0: 0.02, z1: 0.08 }, input.shutter ? blackMetal : glass); if (input.shutter) { for (let index = 1; index < 8; index++) { const y = y0 + (y1 - y0) / 8 * index; slab(ctx, "bronze", { x0: x0 + 0.1, x1: x1 - 0.1, y0: y - 0.02, y1: y + 0.02, z0: -0.1, z1: 0.02 }, bronze); } return; } bronzeMullions(ctx, x0, x1, y0, y1, 0.02, 0.16, input.revolving ? 3 : 2); slab(ctx, "bronze", { x0, x1, y0: y0 + 0.14, y1: y0 + 0.28, z0: -0.12, z1: 0.08 }, bronze); } var tallLobbyWindowModule = (ctx) => { portal(ctx, 0.46, 0.72); framedWindow({ ctx, x0: -ctx.width / 2 + 0.34, x1: ctx.width / 2 - 0.34, y0: 0.34, y1: ctx.height - 0.48, side: 0.22, top: 0.28, bottom: 0.58, splits: 3, spandrel: true }); }; var lobbyDoorModule = (ctx) => { portal(ctx, 0.52, 0.86); bronzeDoor({ ctx, x0: -ctx.width / 2 + 0.64, x1: ctx.width / 2 - 0.64, y0: 0.34, y1: ctx.height - 1 }); slab(ctx, "bronze", { x0: -0.55, x1: 0.55, y0: ctx.height - 0.88, y1: ctx.height - 0.72, z0: 0.04, z1: 0.18 }, bronze); }; var revolvingDoorBayModule = (ctx) => { portal(ctx, 0.46, 0.94); for (const x of [-ctx.width * 0.34, ctx.width * 0.34]) { profiledCylinder({ ctx, slot: "limestone", x, z: 0.38, y0: 0.2, y1: ctx.height - 0.52, radius: 0.22, color: stone, segments: 32, flutes: 14 }); } bronzeDoor({ ctx, x0: -ctx.width * 0.22, x1: ctx.width * 0.22, y0: 0.28, y1: ctx.height - 0.92, revolving: true }); profiledCylinder({ ctx, slot: "bronze", x: 0, z: 0.14, y0: 0.24, y1: ctx.height - 0.92, radius: ctx.width * 0.22, color: bronze, segments: 32 }); }; var cornerEntranceModule = (ctx) => { lobbyDoorModule(ctx); solidBox(ctx, "limestone", [ctx.width / 2 - 0.24, 0, -ctx.depth * 0.42], [ctx.width / 2 + 0.22, ctx.height, 0.44], stone); bronzeDoor({ ctx, x0: ctx.width / 2 - 0.18, x1: ctx.width / 2 + 0.1, y0: 0.4, y1: ctx.height - 1.2 }); }; function portal(ctx, jamb, projection) { const x0 = -ctx.width / 2; const x1 = ctx.width / 2; slab(ctx, "limestone", { x0, x1: x0 + jamb, y0: 0, y1: ctx.height, z0: -0.12, z1: projection }, stone); slab(ctx, "limestone", { x0: x1 - jamb, x1, y0: 0, y1: ctx.height, z0: -0.12, z1: projection }, stone); slab(ctx, "limestone", { x0: x0 + jamb, x1: x1 - jamb, y0: ctx.height - 0.72, y1: ctx.height, z0: -0.12, z1: projection }, stoneWarm); slab(ctx, "limestone", { x0, x1, y0: 0, y1: 0.34, z0: -0.1, z1: projection * 0.72 }, stoneDark); dentils(ctx, x0 + 0.22, x1 - 0.22, ctx.height - 0.56, ctx.height - 0.38, projection, projection + 0.18, 11); } function archedOpening(input) { const { ctx, x0, x1, y0, y1, depth } = input; const width = x1 - x0; const spring = y0 + (y1 - y0) * 0.58; const radius = width * 0.5; slab(ctx, "limestone", { x0, x1, y0, y1, z0: -0.18, z1: -0.04 }, stoneWarm); slab(ctx, "black-metal", { x0: x0 + 0.34, x1: x1 - 0.34, y0: y0 + 0.22, y1: spring + radius * 0.88, z0: -0.04, z1: 0.06 }, stoneDark); slab(ctx, "limestone", { x0, x1: x0 + 0.34, y0, y1, z0: -0.1, z1: depth }, stone); slab(ctx, "limestone", { x0: x1 - 0.34, x1, y0, y1, z0: -0.1, z1: depth }, stone); slab(ctx, "limestone", { x0: x0 + 0.34, x1: x1 - 0.34, y0: y1 - 0.34, y1, z0: -0.1, z1: depth * 0.72 }, stoneWarm); archRing(ctx, x0 + width / 2, spring, radius, 0.34, -0.1, depth); slab(ctx, "glass", { x0: x0 + 0.42, x1: x1 - 0.42, y0: y0 + 0.3, y1: spring + radius * 0.72, z0: 0.06, z1: 0.12 }, glass); bronzeMullions(ctx, x0 + 0.36, x1 - 0.36, y0 + 0.3, spring + radius * 0.72, 0.06, 0.18, 2); if (input.bars) { const count = 5; for (let index = 0; index < count; index++) { const x = x0 + 0.5 + (width - 1) / (count - 1) * index; slab(ctx, "black-metal", { x0: x - 0.035, x1: x + 0.035, y0: y0 + 0.2, y1: spring + radius * 0.65, z0: 0.18, z1: 0.26 }, stoneDark); } } } function triangularPedimentProfile(input) { const { ctx, width, height, depth } = input; const w = width / 2; const zBack = -depth * 0.62; solidBox(ctx, "limestone", [-w, 0, zBack], [w, height * 0.28, depth * 0.62], stoneWarm); triangleFace(ctx, [-w, height * 0.28, depth * 0.55], [0, height, depth * 0.72], [w, height * 0.28, depth * 0.55], stone); triangleFace(ctx, [-w, height * 0.28, zBack], [0, height, zBack], [w, height * 0.28, zBack], stone); triangleFace(ctx, [-w * 0.82, height * 0.34, depth * 0.74], [0, height * 0.82, depth * 0.92], [w * 0.82, height * 0.34, depth * 0.74], stoneWarm); dentils(ctx, -w + 0.35, w - 0.35, height * 0.16, height * 0.26, depth * 0.62, depth * 0.84, Math.max(8, Math.floor(width * 1.1))); } function carvedSeal(ctx, x, y, z0, z1, radius) { rosette(ctx, x, y, z0, z1, radius); slab(ctx, "ornament", { x0: x - radius * 0.5, x1: x + radius * 0.5, y0: y - radius * 0.08, y1: y + radius * 0.08, z0, z1 }, stoneDark); slab(ctx, "ornament", { x0: x - radius * 0.08, x1: x + radius * 0.08, y0: y - radius * 0.46, y1: y + radius * 0.46, z0, z1 }, stoneDark); } function archRing(ctx, cx, cy, radius, thickness, z0, z1) { const segments = 18; for (let index = 0; index < segments; index++) { const a0 = Math.PI - Math.PI * index / segments; const a1 = Math.PI - Math.PI * (index + 1) / segments; const outer0 = point2(cx, cy, radius, a0, z1); const outer1 = point2(cx, cy, radius, a1, z1); const inner1 = point2(cx, cy, radius - thickness, a1, z1); const inner0 = point2(cx, cy, radius - thickness, a0, z1); const outerBack0 = point2(cx, cy, radius, a0, z0); const outerBack1 = point2(cx, cy, radius, a1, z0); const innerBack1 = point2(cx, cy, radius - thickness, a1, z0); const innerBack0 = point2(cx, cy, radius - thickness, a0, z0); ctx.writer.appendQuad("limestone", [ ctx.transform(outer0), ctx.transform(inner0), ctx.transform(inner1), ctx.transform(outer1) ], stone); ctx.writer.appendQuad("limestone", [ ctx.transform(outerBack1), ctx.transform(innerBack1), ctx.transform(innerBack0), ctx.transform(outerBack0) ], stone); ctx.writer.appendQuad("limestone", [ ctx.transform(outerBack0), ctx.transform(outer0), ctx.transform(outer1), ctx.transform(outerBack1) ], stone); ctx.writer.appendQuad("limestone", [ ctx.transform(inner0), ctx.transform(innerBack0), ctx.transform(innerBack1), ctx.transform(inner1) ], stone); } } function triangleFace(ctx, a, b, c, color) { ctx.writer.appendQuad("limestone", [ ctx.transform(a), ctx.transform(b), ctx.transform(c), ctx.transform(c) ], color); } function point2(cx, cy, radius, angle, z) { return [cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius, z]; } var colossalColumnModule = (ctx) => { const capitalScale = ctx.width * 0.44; const capitalY = ctx.height - capitalScale * 0.28; stackedColumn({ ctx, x: 0, z: 0.22, y0: 0, y1: capitalY, radius: ctx.width * 0.24, color: stone }); ionicCapital(ctx, 0, capitalY, capitalScale); solidBox(ctx, "limestone", [-ctx.width * 0.56, ctx.height - 0.18, -0.12], [ctx.width * 0.56, ctx.height, 0.78], stoneWarm); solidBox(ctx, "granite", [-ctx.width * 0.46, 0, -0.32], [ctx.width * 0.46, 0.42, 0.72], granite); }; var squareCornerPylonModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -0.18], [ctx.width / 2, ctx.height, 0.72], stone); for (let index = 1; index < 9; index++) { const y = ctx.height / 9 * index; slab(ctx, "limestone", { x0: -ctx.width / 2 + 0.12, x1: ctx.width / 2 - 0.12, y0: y - 0.035, y1: y + 0.035, z0: 0.72, z1: 0.86 }, stoneDark); } slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.18, x1: ctx.width / 2 + 0.18, y0: ctx.height - 0.72, y1: ctx.height, z0: -0.18, z1: 0.94 }, stoneWarm); }; var triangularPedimentModule = (ctx) => { triangularPedimentProfile({ ctx, width: ctx.width, height: ctx.height, depth: ctx.depth }); dentils(ctx, -ctx.width / 2 + 0.35, ctx.width / 2 - 0.35, 0.12, 0.28, ctx.depth * 0.72, ctx.depth * 0.96, Math.max(10, Math.floor(ctx.width))); }; var pedimentEagleModule = (ctx) => { carvedSeal(ctx, 0, ctx.height * 0.56, 0.08, 0.34, Math.min(ctx.width, ctx.height) * 0.32); for (const side of [-1, 1]) { slab(ctx, "ornament", { x0: side * 0.12, x1: side * ctx.width * 0.42, y0: ctx.height * 0.48, y1: ctx.height * 0.64, z0: 0.12, z1: 0.34 }, stoneWarm); } }; var acroterionScrollModule = (ctx) => { profiledCylinder({ ctx, slot: "limestone", x: 0, z: 0.18, y0: 0.08, y1: ctx.height * 0.72, radius: ctx.width * 0.2, color: stone, segments: 24 }); rosette(ctx, 0, ctx.height * 0.54, 0.26, 0.48, ctx.width * 0.34); solidBox(ctx, "limestone", [-ctx.width * 0.32, 0, -0.08], [ctx.width * 0.32, 0.16, 0.42], stoneDark); }; var companyFriezeModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -ctx.depth * 0.68, z1: 0.42 }, stoneWarm); const letters = Math.max(8, Math.floor(ctx.width / 0.42)); for (let index = 0; index < letters; index++) { const x = -ctx.width / 2 + 0.35 + index * ((ctx.width - 0.7) / letters); slab(ctx, "ornament", { x0: x - 0.055, x1: x + 0.055, y0: ctx.height * 0.28, y1: ctx.height * 0.72, z0: 0.42, z1: 0.54 }, stoneDark); } }; var clockMedallionModule = (ctx) => { rosette(ctx, 0, ctx.height / 2, 0.08, 0.32, ctx.width * 0.42); profiledCylinder({ ctx, slot: "bronze", x: 0, z: 0.34, y0: ctx.height * 0.46, y1: ctx.height * 0.54, radius: ctx.width * 0.38, color: bronze, segments: 48 }); slab(ctx, "black-metal", { x0: -0.035, x1: 0.035, y0: ctx.height * 0.5, y1: ctx.height * 0.78, z0: 0.48, z1: 0.58 }, blackMetal); slab(ctx, "black-metal", { x0: 0, x1: ctx.width * 0.24, y0: ctx.height * 0.49, y1: ctx.height * 0.55, z0: 0.48, z1: 0.58 }, blackMetal); }; var rusticatedBaseBlockModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -0.12], [ctx.width / 2, ctx.height, 0.58], stoneWarm); const rows = Math.max(3, Math.floor(ctx.height / 0.45)); for (let row = 1; row < rows; row++) { const y = ctx.height / rows * row; slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: y - 0.025, y1: y + 0.025, z0: 0.58, z1: 0.74 }, stoneDark); } }; var bankGrilleModule = (ctx) => { framedWindow({ ctx, x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, side: 0.42, top: 0.46, bottom: 0.42, splits: 3 }); grilleBars(ctx, -ctx.width * 0.32, ctx.width * 0.32, 0.56, ctx.height - 0.62, 7); }; var barredWindowModule = (ctx) => { framedWindow({ ctx, x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, side: 0.34, top: 0.38, bottom: 0.42, splits: 2 }); grilleBars(ctx, -ctx.width * 0.26, ctx.width * 0.26, 0.48, ctx.height - 0.44, 5); }; var storefrontCurtainWallModule = (ctx) => { slab(ctx, "black-metal", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.06, z1: 0.12 }, blackMetal); slab(ctx, "glass", { x0: -ctx.width / 2 + 0.12, x1: ctx.width / 2 - 0.12, y0: 0.18, y1: ctx.height - 0.18, z0: 0, z1: 0.08 }, glass); grilleBars(ctx, -ctx.width / 2 + 0.2, ctx.width / 2 - 0.2, 0.28, ctx.height - 0.28, 4); }; var securityDoorModule = (ctx) => { bronzeDoor({ ctx, x0: -ctx.width * 0.28, x1: ctx.width * 0.28, y0: 0.32, y1: ctx.height - 0.52, shutter: true }); slab(ctx, "limestone", { x0: -ctx.width / 2, x1: -ctx.width * 0.32, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.55 }, stone); slab(ctx, "limestone", { x0: ctx.width * 0.32, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.55 }, stone); slab(ctx, "black-metal", { x0: -ctx.width * 0.24, x1: ctx.width * 0.24, y0: ctx.height - 0.82, y1: ctx.height - 0.46, z0: 0.08, z1: 0.24 }, blackMetal); }; var wallPlaqueModule = (ctx) => { slab(ctx, "bronze", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: 0.04, z1: 0.18 }, bronzeDark); slab(ctx, "bronze", { x0: -ctx.width / 2 + 0.08, x1: ctx.width / 2 - 0.08, y0: 0.08, y1: ctx.height - 0.08, z0: 0.18, z1: 0.26 }, bronze); }; var addressPlaqueModule = (ctx) => { wallPlaqueModule(ctx); for (const x of [-0.18, 0, 0.18]) { slab(ctx, "black-metal", { x0: x - 0.035, x1: x + 0.035, y0: ctx.height * 0.25, y1: ctx.height * 0.75, z0: 0.28, z1: 0.36 }, blackMetal); } }; var flagMountModule = (ctx) => { solidBox(ctx, "bronze", [-0.08, 0.1, 0], [0.08, 0.36, 0.22], bronze); solidBox(ctx, "bronze", [0, 0.24, 0.16], [ctx.width * 0.5, 0.32, ctx.depth], bronze); slab(ctx, "ornament", { x0: ctx.width * 0.2, x1: ctx.width * 0.5, y0: 0.32, y1: ctx.height, z0: ctx.depth * 0.72, z1: ctx.depth }, stoneWarm); }; var bollardModule = (ctx) => { profiledCylinder({ ctx, slot: "black-metal", x: 0, z: 0, y0: 0, y1: ctx.height, radius: ctx.width * 0.22, color: blackMetal, segments: 24 }); profiledCylinder({ ctx, slot: "bronze", x: 0, z: 0, y0: ctx.height * 0.78, y1: ctx.height, radius: ctx.width * 0.26, color: bronze, segments: 24 }); }; var wallCameraModule = (ctx) => { solidBox(ctx, "black-metal", [-0.08, 0.1, 0], [0.08, ctx.height * 0.7, 0.24], blackMetal); solidBox(ctx, "black-metal", [-ctx.width * 0.35, ctx.height * 0.34, 0.18], [ctx.width * 0.35, ctx.height * 0.62, ctx.depth], blackMetal); }; var wallLampModule = (ctx) => { solidBox(ctx, "bronze", [-0.05, 0, 0], [0.05, ctx.height * 0.68, 0.18], bronze); profiledCylinder({ ctx, slot: "glass", x: 0, z: ctx.depth * 0.55, y0: ctx.height * 0.45, y1: ctx.height, radius: ctx.width * 0.22, color: glass, segments: 20 }); }; var sidewalkEntryModule = (ctx) => { solidBox(ctx, "granite", [-ctx.width / 2, 0, -ctx.depth / 2], [ctx.width / 2, ctx.height * 0.45, ctx.depth / 2], granite); solidBox(ctx, "limestone", [-ctx.width * 0.42, ctx.height * 0.45, -ctx.depth * 0.28], [ctx.width * 0.42, ctx.height, ctx.depth * 0.48], stone); }; function ionicCapital(ctx, x, y, scale) { slab(ctx, "limestone", { x0: x - scale, x1: x + scale, y0: y, y1: y + scale * 0.26, z0: -0.16, z1: 0.8 }, stoneWarm); for (const side of [-1, 1]) { rosette(ctx, x + side * scale * 0.55, y + scale * 0.28, 0.62, 0.88, scale * 0.32); } } function grilleBars(ctx, x0, x1, y0, y1, count) { for (let index = 0; index < count; index++) { const x = x0 + (x1 - x0) / Math.max(1, count - 1) * index; slab(ctx, "black-metal", { x0: x - 0.035, x1: x + 0.035, y0, y1, z0: 0.18, z1: 0.3 }, blackMetal); } } var serviceDoorModule = (ctx) => { frame(ctx, 0.42); bronzeDoor({ ctx, x0: -ctx.width * 0.23, x1: ctx.width * 0.23, y0: 0.35, y1: ctx.height - 0.85 }); slab(ctx, "black-metal", { x0: -0.42, x1: 0.42, y0: ctx.height - 0.72, y1: ctx.height - 0.46, z0: -0.08, z1: 0.12 }, blackMetal); }; var loadingDockBayModule = (ctx) => { frame(ctx, 0.52); bronzeDoor({ ctx, x0: -ctx.width * 0.34, x1: ctx.width * 0.34, y0: 0.36, y1: ctx.height - 0.7, shutter: true }); solidBox(ctx, "bronze", [-ctx.width * 0.46, 0, 0.12], [-ctx.width * 0.38, 0.82, 0.28], bronze); solidBox(ctx, "bronze", [ctx.width * 0.38, 0, 0.12], [ctx.width * 0.46, 0.82, 0.28], bronze); }; var granitePlinthModule = (ctx) => { solidBox(ctx, "granite", [-ctx.width / 2, 0, -ctx.depth / 2], [ctx.width / 2, ctx.height * 0.72, ctx.depth / 2], granite); slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: ctx.height * 0.72, y1: ctx.height, z0: -ctx.depth / 2, z1: ctx.depth / 2 + 0.08 }, stone); }; function frame(ctx, jamb) { const x0 = -ctx.width / 2; const x1 = ctx.width / 2; slab(ctx, "limestone", { x0, x1: x0 + jamb, y0: 0, y1: ctx.height, z0: -0.1, z1: 0.64 }, stone); slab(ctx, "limestone", { x0: x1 - jamb, x1, y0: 0, y1: ctx.height, z0: -0.1, z1: 0.64 }, stone); slab(ctx, "limestone", { x0: x0 + jamb, x1: x1 - jamb, y0: ctx.height - 0.72, y1: ctx.height, z0: -0.1, z1: 0.64 }, stoneWarm); slab(ctx, "limestone", { x0, x1, y0: 0, y1: 0.32, z0: -0.1, z1: 0.42 }, stoneDark); } var baseModuleRuntimes = [ { id: "round-column", builder: roundColumnModule, slots: ["limestone"] }, { id: "square-column", builder: squareColumnModule, slots: ["limestone"] }, { id: "paired-column", builder: pairedColumnModule, slots: ["limestone", "granite", "bronze"] }, { id: "tall-lobby-window", builder: tallLobbyWindowModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "lobby-door", builder: lobbyDoorModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "revolving-door-bay", builder: revolvingDoorBayModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "service-door", builder: serviceDoorModule, slots: ["limestone", "glass", "bronze", "black-metal"] }, { id: "loading-dock-bay", builder: loadingDockBayModule, slots: ["limestone", "bronze", "black-metal"] }, { id: "granite-plinth", builder: granitePlinthModule, slots: ["granite", "limestone"] }, { id: "corner-entrance", builder: cornerEntranceModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "colossal-column", builder: colossalColumnModule, slots: ["limestone", "granite", "ornament"] }, { id: "square-corner-pylon", builder: squareCornerPylonModule, slots: ["limestone"] }, { id: "triangular-pediment", builder: triangularPedimentModule, slots: ["limestone", "ornament"] }, { id: "pediment-eagle", builder: pedimentEagleModule, slots: ["ornament"] }, { id: "acroterion-scroll", builder: acroterionScrollModule, slots: ["limestone", "ornament"] }, { id: "company-frieze", builder: companyFriezeModule, slots: ["limestone", "ornament"] }, { id: "clock-medallion", builder: clockMedallionModule, slots: ["limestone", "bronze", "black-metal", "ornament"] }, { id: "rusticated-base-block", builder: rusticatedBaseBlockModule, slots: ["limestone"] }, { id: "bank-grille", builder: bankGrilleModule, slots: ["limestone", "glass", "bronze", "black-metal", "ornament"] }, { id: "barred-window", builder: barredWindowModule, slots: ["limestone", "glass", "bronze", "black-metal", "ornament"] }, { id: "storefront-curtain-wall", builder: storefrontCurtainWallModule, slots: ["glass", "black-metal", "bronze"] }, { id: "security-door", builder: securityDoorModule, slots: ["limestone", "black-metal", "bronze"] }, { id: "wall-plaque", builder: wallPlaqueModule, slots: ["bronze"] }, { id: "address-plaque", builder: addressPlaqueModule, slots: ["bronze", "black-metal"] }, { id: "flag-mount", builder: flagMountModule, slots: ["bronze", "ornament"] }, { id: "bollard", builder: bollardModule, slots: ["black-metal", "bronze"] }, { id: "wall-camera", builder: wallCameraModule, slots: ["black-metal"] }, { id: "wall-lamp", builder: wallLampModule, slots: ["bronze", "glass"] }, { id: "sidewalk-entry", builder: sidewalkEntryModule, slots: ["granite", "limestone"] } ]; var crownWindowBayModule = (ctx) => { framedWindow({ ctx, x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, side: 0.42, top: 0.48, bottom: 0.56, splits: 2 }); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.08, x1: ctx.width / 2 + 0.08, y0: ctx.height - 0.42, y1: ctx.height, z0: -0.08, z1: 0.58 }, stone); }; var parapetSectionModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.42 }, stoneWarm); for (const x of [-ctx.width * 0.34, 0, ctx.width * 0.34]) { slab(ctx, "limestone", { x0: x - 0.12, x1: x + 0.12, y0: 0.18, y1: ctx.height - 0.22, z0: 0.42, z1: 0.58 }, stoneDark); rosette(ctx, x, ctx.height * 0.55, 0.58, 0.72, 0.22); } slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.12, x1: ctx.width / 2 + 0.12, y0: ctx.height - 0.22, y1: ctx.height, z0: -0.08, z1: 0.58 }, stone); }; var cornerParapetModule = (ctx) => { parapetSectionModule(ctx); solidBox(ctx, "limestone", [ctx.width / 2 - 0.28, 0, -ctx.depth * 0.5], [ctx.width / 2 + 0.28, ctx.height, 0.52], stone); for (const z of [-ctx.depth * 0.38, 0.38]) { profiledCylinder({ ctx, slot: "limestone", x: ctx.width / 2, z, y0: ctx.height - 0.02, y1: ctx.height + 0.42, radius: 0.13, color: stone, segments: 20 }); } }; function louverBox(ctx, width, height, depth) { solidBox(ctx, "roof", [-width / 2, 0, -depth / 2], [width / 2, height, depth / 2], roofMetal); for (let index = 0; index < 7; index++) { const y = 0.35 + index * 0.18; slab(ctx, "black-metal", { x0: -width / 2 - 0.02, x1: width / 2 + 0.02, y0: y, y1: y + 0.045, z0: depth / 2 + 0.02, z1: depth / 2 + 0.08 }, blackMetal); } } function fanCylinder(ctx, x, z, radius, height) { profiledCylinder({ ctx, slot: "black-metal", x, z, y0: 0, y1: height, radius, color: blackMetal, segments: 28 }); profiledCylinder({ ctx, slot: "roof", x, z, y0: height, y1: height + 0.16, radius: radius * 1.12, color: roofMetal, segments: 28 }); } function railing(ctx, width, height) { for (let index = 0; index <= 6; index++) { const x = -width / 2 + width / 6 * index; solidBox(ctx, "bronze", [x - 0.035, 0, -0.035], [x + 0.035, height, 0.035], bronze); } for (const y of [height * 0.46, height * 0.9]) { solidBox(ctx, "bronze", [-width / 2, y - 0.03, -0.035], [width / 2, y + 0.03, 0.035], bronze); } } var roofMechBoxModule = (ctx) => { louverBox(ctx, ctx.width, ctx.height, ctx.depth); }; var hvacClusterModule = (ctx) => { solidBox(ctx, "roof", [-ctx.width * 0.45, 0, -ctx.depth * 0.35], [-ctx.width * 0.1, ctx.height * 0.55, ctx.depth * 0.25], roofMetal); fanCylinder(ctx, ctx.width * 0.16, -ctx.depth * 0.2, 0.45, ctx.height * 0.62); fanCylinder(ctx, ctx.width * 0.36, ctx.depth * 0.18, 0.36, ctx.height * 0.48); solidBox(ctx, "black-metal", [-ctx.width * 0.38, ctx.height * 0.16, ctx.depth * 0.28], [-ctx.width * 0.08, ctx.height * 0.42, ctx.depth * 0.36], blackMetal); }; var roofRailingModule = (ctx) => { railing(ctx, ctx.width, ctx.height); }; var antennaModule = (ctx) => { profiledCylinder({ ctx, slot: "black-metal", x: 0, z: 0, y0: 0, y1: ctx.height, radius: 0.035, color: blackMetal, segments: 14 }); for (const y of [0.7, 1.5, 2.4, 3.3]) { profiledCylinder({ ctx, slot: "bronze", x: 0, z: 0, y0: y, y1: y + 0.08, radius: 0.16, color: bronze, segments: 16 }); } solidBox(ctx, "black-metal", [-0.25, 1.1, -0.025], [0.25, 1.16, 0.025], blackMetal); solidBox(ctx, "black-metal", [-0.18, 2.1, -0.025], [0.18, 2.16, 0.025], blackMetal); }; var slopedMetalRoofModule = (ctx) => { const x0 = -ctx.width / 2; const x1 = ctx.width / 2; const z0 = -ctx.depth / 2; const z1 = ctx.depth / 2; const ridge = [0, ctx.height, 0]; roofTriangle(ctx, [x0, 0, z1], [x1, 0, z1], ridge); roofTriangle(ctx, [x1, 0, z0], [x0, 0, z0], ridge); roofTriangle(ctx, [x1, 0, z1], [x1, 0, z0], ridge); roofTriangle(ctx, [x0, 0, z0], [x0, 0, z1], ridge); for (let index = -5; index <= 5; index++) { if (index === 0) continue; const x = ctx.width / 12 * index; roofSeam(ctx, [x, 0.04, z1], ridge, 0.035); roofSeam(ctx, [x, 0.04, z0], ridge, 0.035); } for (let index = -3; index <= 3; index++) { if (index === 0) continue; const z = ctx.depth / 8 * index; roofSeam(ctx, [x1, 0.04, z], ridge, 0.035); roofSeam(ctx, [x0, 0.04, z], ridge, 0.035); } solidBox(ctx, "roof", [-0.08, ctx.height * 0.95, -0.18], [0.08, ctx.height + 0.12, 0.18], roofMetal); }; var roofLanternModule = (ctx) => { solidBox(ctx, "black-metal", [-ctx.width / 2, 0, -ctx.depth / 2], [ctx.width / 2, 0.12, ctx.depth / 2], blackMetal); for (const x of [-ctx.width * 0.42, ctx.width * 0.42]) { for (const z of [-ctx.depth * 0.42, ctx.depth * 0.42]) { solidBox(ctx, "black-metal", [x - 0.05, 0, z - 0.05], [x + 0.05, ctx.height, z + 0.05], blackMetal); } } solidBox(ctx, "black-metal", [-ctx.width / 2, ctx.height - 0.12, -ctx.depth / 2], [ctx.width / 2, ctx.height, ctx.depth / 2], blackMetal); }; var roofStatueMastModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width * 0.32, 0, -ctx.depth * 0.32], [ctx.width * 0.32, ctx.height * 0.14, ctx.depth * 0.32], stoneDark); profiledCylinder({ ctx, slot: "limestone", x: 0, z: 0, y0: ctx.height * 0.14, y1: ctx.height * 0.88, radius: ctx.width * 0.12, color: stone, segments: 32, flutes: 8 }); for (const x of [-ctx.width * 0.18, ctx.width * 0.18]) { solidBox(ctx, "limestone", [x - 0.08, ctx.height * 0.28, -0.05], [x + 0.08, ctx.height * 0.86, 0.05], stone); } profiledCylinder({ ctx, slot: "bronze", x: 0, z: 0, y0: ctx.height * 0.88, y1: ctx.height, radius: ctx.width * 0.16, color: bronze, segments: 24 }); }; var roofCrestModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width * 0.35, 0, -ctx.depth * 0.3], [ctx.width * 0.35, ctx.height * 0.2, ctx.depth * 0.3], stoneDark); profiledCylinder({ ctx, slot: "bronze", x: 0, z: 0, y0: ctx.height * 0.2, y1: ctx.height, radius: 0.04, color: bronze, segments: 12 }); slab(ctx, "ornament", { x0: 0.04, x1: ctx.width * 0.48, y0: ctx.height * 0.56, y1: ctx.height * 0.82, z0: 0, z1: ctx.depth * 0.18 }, stone); }; function roofTriangle(ctx, a, b, c) { ctx.writer.appendQuad("roof", [ ctx.transform(a), ctx.transform(b), ctx.transform(c), ctx.transform(c) ], roofMetal); } function roofSeam(ctx, a, b, halfWidth) { const dx = b[0] - a[0]; const dz = b[2] - a[2]; const length = Math.hypot(dx, dz) || 1; const nx = -dz / length * halfWidth; const nz = dx / length * halfWidth; ctx.writer.appendQuad("roof", [ ctx.transform([a[0] - nx, a[1], a[2] - nz]), ctx.transform([a[0] + nx, a[1], a[2] + nz]), ctx.transform([b[0] + nx * 0.18, b[1] + 0.04, b[2] + nz * 0.18]), ctx.transform([b[0] - nx * 0.18, b[1] + 0.04, b[2] - nz * 0.18]) ], roofMetal); } var atticCrestPanelModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height * 0.48, z0: -0.08, z1: 0.42 }, stoneWarm); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.12, x1: ctx.width / 2 + 0.12, y0: ctx.height * 0.48, y1: ctx.height, z0: -0.08, z1: 0.62 }, stone); dentils(ctx, -ctx.width / 2 + 0.2, ctx.width / 2 - 0.2, ctx.height * 0.12, ctx.height * 0.28, 0.42, 0.62, Math.max(5, Math.floor(ctx.width * 1.2))); for (const x of [-ctx.width * 0.28, 0, ctx.width * 0.28]) { rosette(ctx, x, ctx.height * 0.64, 0.62, 0.78, 0.18); } }; var crownPedimentModule = (ctx) => { const w = ctx.width / 2; solidBox(ctx, "limestone", [-w, 0, -0.08], [w, ctx.height * 0.42, 0.62], stoneWarm); solidBox(ctx, "limestone", [-w * 0.72, ctx.height * 0.42, -0.08], [w * 0.72, ctx.height * 0.72, 0.72], stone); solidBox(ctx, "limestone", [-w * 0.42, ctx.height * 0.72, -0.08], [w * 0.42, ctx.height, 0.86], stone); slab(ctx, "ornament", { x0: -w * 0.46, x1: w * 0.46, y0: ctx.height * 0.14, y1: ctx.height * 0.36, z0: 0.62, z1: 0.82 }, stoneDark); rosette(ctx, 0, ctx.height * 0.56, 0.72, 0.94, 0.28); }; var cornerFinialModule = (ctx) => { solidBox(ctx, "limestone", [-0.28, 0, -0.28], [0.28, ctx.height * 0.18, 0.28], stoneDark); profiledCylinder({ ctx, slot: "limestone", x: 0, z: 0, y0: ctx.height * 0.18, y1: ctx.height * 0.72, radius: 0.22, color: stone, segments: 28, flutes: 8 }); profiledCylinder({ ctx, slot: "limestone", x: 0, z: 0, y0: ctx.height * 0.72, y1: ctx.height, radius: 0.11, color: stoneWarm, segments: 24 }); }; var crownUrnFinialModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width * 0.28, 0, -ctx.depth * 0.28], [ctx.width * 0.28, ctx.height * 0.18, ctx.depth * 0.28], stoneDark); profiledCylinder({ ctx, slot: "limestone", x: 0, z: 0, y0: ctx.height * 0.18, y1: ctx.height * 0.62, radius: ctx.width * 0.25, color: stone, segments: 28 }); profiledCylinder({ ctx, slot: "limestone", x: 0, z: 0, y0: ctx.height * 0.62, y1: ctx.height * 0.84, radius: ctx.width * 0.16, color: stoneWarm, segments: 24 }); solidBox(ctx, "limestone", [-ctx.width * 0.2, ctx.height * 0.84, -ctx.depth * 0.2], [ctx.width * 0.2, ctx.height, ctx.depth * 0.2], stoneWarm); }; var crownObeliskFinialModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width * 0.34, 0, -ctx.depth * 0.34], [ctx.width * 0.34, ctx.height * 0.18, ctx.depth * 0.34], stoneDark); solidBox(ctx, "limestone", [-ctx.width * 0.22, ctx.height * 0.18, -ctx.depth * 0.22], [ctx.width * 0.22, ctx.height * 0.74, ctx.depth * 0.22], stone); solidBox(ctx, "limestone", [-ctx.width * 0.1, ctx.height * 0.74, -ctx.depth * 0.1], [ctx.width * 0.1, ctx.height, ctx.depth * 0.1], stoneWarm); }; var crownPillarFinialModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width * 0.32, 0, -ctx.depth * 0.32], [ctx.width * 0.32, ctx.height * 0.18, ctx.depth * 0.32], stoneDark); solidBox(ctx, "limestone", [-ctx.width * 0.22, ctx.height * 0.18, -ctx.depth * 0.22], [ctx.width * 0.22, ctx.height * 0.76, ctx.depth * 0.22], stone); solidBox(ctx, "limestone", [-ctx.width * 0.36, ctx.height * 0.76, -ctx.depth * 0.3], [ctx.width * 0.36, ctx.height * 0.9, ctx.depth * 0.3], stoneWarm); solidBox(ctx, "limestone", [-ctx.width * 0.18, ctx.height * 0.9, -ctx.depth * 0.18], [ctx.width * 0.18, ctx.height, ctx.depth * 0.18], stoneWarm); }; var crownCartouchePanelModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.46 }, stoneWarm); slab(ctx, "ornament", { x0: -ctx.width * 0.34, x1: ctx.width * 0.34, y0: ctx.height * 0.2, y1: ctx.height * 0.78, z0: 0.46, z1: 0.66 }, stoneDark); rosette(ctx, 0, ctx.height * 0.52, 0.66, 0.86, Math.min(ctx.width, ctx.height) * 0.2); }; var crownRoofRuntimes = [ { id: "crown-window-bay", builder: crownWindowBayModule, slots: ["limestone", "glass", "bronze"] }, { id: "parapet-section", builder: parapetSectionModule, slots: ["limestone", "ornament"] }, { id: "corner-parapet", builder: cornerParapetModule, slots: ["limestone", "ornament"] }, { id: "attic-crest-panel", builder: atticCrestPanelModule, slots: ["limestone", "ornament"] }, { id: "crown-pediment", builder: crownPedimentModule, slots: ["limestone", "ornament"] }, { id: "corner-finial", builder: cornerFinialModule, slots: ["limestone"] }, { id: "crown-urn-finial", builder: crownUrnFinialModule, slots: ["limestone"] }, { id: "crown-obelisk-finial", builder: crownObeliskFinialModule, slots: ["limestone"] }, { id: "crown-pillar-finial", builder: crownPillarFinialModule, slots: ["limestone"] }, { id: "crown-cartouche-panel", builder: crownCartouchePanelModule, slots: ["limestone", "ornament"] }, { id: "sloped-metal-roof", builder: slopedMetalRoofModule, slots: ["roof"] }, { id: "roof-statue-mast", builder: roofStatueMastModule, slots: ["limestone", "bronze"] }, { id: "roof-lantern", builder: roofLanternModule, slots: ["black-metal"] }, { id: "roof-crest", builder: roofCrestModule, slots: ["limestone", "bronze", "ornament"] }, { id: "roof-mech-box", builder: roofMechBoxModule, slots: ["roof", "black-metal"] }, { id: "hvac-cluster", builder: hvacClusterModule, slots: ["roof", "black-metal"] }, { id: "roof-railing", builder: roofRailingModule, slots: ["bronze"] }, { id: "antenna", builder: antennaModule, slots: ["black-metal", "bronze"] } ]; var window3mModule = (ctx) => { shaftFrame(ctx, 0.32); framedWindow({ ctx, x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, side: 0.32, top: 0.32, bottom: 0.74, splits: 2, spandrel: true }); }; var window4mModule = (ctx) => { shaftFrame(ctx, 0.34); framedWindow({ ctx, x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, side: 0.3, top: 0.32, bottom: 0.76, splits: 3, spandrel: true }); }; var doubleWindowBayModule = (ctx) => { shaftFrame(ctx, 0.3); const gap = 0.18; framedWindow({ ctx, x0: -ctx.width / 2, x1: -gap, y0: 0, y1: ctx.height, side: 0.24, top: 0.32, bottom: 0.78, splits: 2, spandrel: false }); framedWindow({ ctx, x0: gap, x1: ctx.width / 2, y0: 0, y1: ctx.height, side: 0.24, top: 0.32, bottom: 0.78, splits: 2, spandrel: false }); slab(ctx, "limestone", { x0: -gap, x1: gap, y0: 0.16, y1: ctx.height - 0.16, z0: -0.08, z1: 0.3 }, stone); for (const x of [-ctx.width * 0.23, ctx.width * 0.23]) { rosette(ctx, x, 0.46, 0.16, 0.3, 0.32); } }; function shaftFrame(ctx, pier) { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: -ctx.width / 2 + pier, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.28 }, stoneDark); slab(ctx, "limestone", { x0: ctx.width / 2 - pier, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.28 }, stoneDark); } var blankBayModule = (ctx) => { panelGrid(ctx, -ctx.width / 2, ctx.width / 2, 0, ctx.height); slab(ctx, "limestone", { x0: -ctx.width / 2 + 0.34, x1: ctx.width / 2 - 0.34, y0: 0.38, y1: ctx.height - 0.38, z0: -0.06, z1: 0.18 }, stoneWarm); }; var cornerBayModule = (ctx) => { window3mModule(ctx); slab(ctx, "limestone", { x0: ctx.width / 2 - 0.22, x1: ctx.width / 2 + 0.22, y0: 0, y1: ctx.height, z0: -ctx.depth * 0.5, z1: 0.42 }, stone); }; var verticalPilasterStripModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.42 }, stone); for (const x of [-0.18, 0, 0.18]) { slab(ctx, "limestone", { x0: x - 0.025, x1: x + 0.025, y0: 0.34, y1: ctx.height - 0.34, z0: 0.42, z1: 0.54 }, stoneDark); } slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.08, x1: ctx.width / 2 + 0.08, y0: 0, y1: 0.26, z0: -0.1, z1: 0.5 }, stoneWarm); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.08, x1: ctx.width / 2 + 0.08, y0: ctx.height - 0.34, y1: ctx.height, z0: -0.1, z1: 0.5 }, stoneWarm); }; var spandrelPanelModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.2 }, stoneWarm); slab(ctx, "ornament", { x0: -ctx.width / 2 + 0.25, x1: ctx.width / 2 - 0.25, y0: 0.16, y1: ctx.height - 0.16, z0: 0.2, z1: 0.36 }, stone); rosette(ctx, 0, ctx.height / 2, 0.36, 0.5, Math.min(ctx.width, ctx.height) * 0.32); }; var floorBandStripModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.26 }, stone); greekKey(ctx, -ctx.width / 2 + 0.15, ctx.width / 2 - 0.15, ctx.height * 0.22, 0.26, 0.44, Math.max(3, Math.floor(ctx.width))); }; function panelGrid(ctx, x0, x1, y0, y1) { slab(ctx, "limestone", { x0, x1, y0, y1, z0: -0.08, z1: 0.12 }, stone); const midX = (x0 + x1) / 2; const midY = (y0 + y1) / 2; slab(ctx, "limestone", { x0: midX - 0.025, x1: midX + 0.025, y0: y0 + 0.18, y1: y1 - 0.18, z0: 0.12, z1: 0.22 }, stoneDark); slab(ctx, "limestone", { x0: x0 + 0.18, x1: x1 - 0.18, y0: midY - 0.025, y1: midY + 0.025, z0: 0.12, z1: 0.22 }, stoneDark); } var centralGlassShaftModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: -ctx.width * 0.36, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.5 }, stoneDark); slab(ctx, "limestone", { x0: ctx.width * 0.36, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.5 }, stoneDark); slab(ctx, "black-metal", { x0: -ctx.width * 0.34, x1: ctx.width * 0.34, y0: 0.12, y1: ctx.height - 0.12, z0: -0.04, z1: 0.12 }, blackMetal); const floors = Math.max(3, Math.floor(ctx.height / 3.35)); for (let floor = 0; floor < floors; floor++) { const y0 = floor * (ctx.height / floors) + 0.18; const y1 = (floor + 1) * (ctx.height / floors) - 0.18; slab(ctx, "glass", { x0: -ctx.width * 0.28, x1: ctx.width * 0.28, y0, y1, z0: 0.12, z1: 0.2 }, glass); slab(ctx, "bronze", { x0: -ctx.width * 0.28, x1: ctx.width * 0.28, y0: y1 - 0.035, y1: y1 + 0.035, z0: 0.2, z1: 0.32 }, bronze); } }; var solidSidePierModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -0.1], [ctx.width / 2, ctx.height, 0.56], stone); slab(ctx, "limestone", { x0: -ctx.width / 2 + 0.14, x1: ctx.width / 2 - 0.14, y0: 0.3, y1: ctx.height - 0.3, z0: 0.56, z1: 0.7 }, stoneDark); }; var buttressPierModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -0.1], [ctx.width / 2, ctx.height, 0.72], stone); for (let index = 0; index < 4; index++) { const y = ctx.height / 4 * index; slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.08, x1: ctx.width / 2 + 0.08, y0: y, y1: y + 0.26, z0: -0.1, z1: 0.9 }, stoneWarm); } }; var pilasterBundleModule = (ctx) => { for (const x of [-0.32, 0, 0.32]) { slab(ctx, "limestone", { x0: x - 0.12, x1: x + 0.12, y0: 0, y1: ctx.height, z0: -0.06, z1: 0.58 }, stone); slab(ctx, "limestone", { x0: x - 0.035, x1: x + 0.035, y0: 0.42, y1: ctx.height - 0.42, z0: 0.58, z1: 0.72 }, stoneDark); } slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: ctx.height - 0.45, y1: ctx.height, z0: -0.08, z1: 0.78 }, stoneWarm); }; var recessedWindowSlotModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.12, z1: 0.68 }, stoneDark); slab(ctx, "black-metal", { x0: -ctx.width * 0.28, x1: ctx.width * 0.28, y0: 0.25, y1: ctx.height - 0.25, z0: -0.04, z1: 0.08 }, blackMetal); const panes = Math.max(3, Math.floor(ctx.height / 1.65)); for (let index = 0; index < panes; index++) { const y0 = 0.35 + index * ((ctx.height - 0.7) / panes); const y1 = 0.35 + (index + 0.72) * ((ctx.height - 0.7) / panes); slab(ctx, "glass", { x0: -ctx.width * 0.2, x1: ctx.width * 0.2, y0, y1, z0: 0.08, z1: 0.18 }, glass); } }; var archedWindowBayModule = (ctx) => { archedOpening({ ctx, x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, depth: 0.72 }); }; var arcadeBayModule = (ctx) => { archedOpening({ ctx, x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, depth: 0.94 }); for (const x of [-ctx.width * 0.42, ctx.width * 0.42]) { slab(ctx, "terra-cotta", { x0: x - 0.12, x1: x + 0.12, y0: 0, y1: ctx.height, z0: 0.66, z1: 0.94 }, stoneWarm); } }; var brickWindowBayModule = (ctx) => { solidBox(ctx, "terra-cotta", [-ctx.width / 2, 0, -0.08], [ctx.width / 2, ctx.height, 0.32], [1, 1, 1]); brickCourses(ctx); framedWindow({ ctx, x0: -ctx.width * 0.32, x1: ctx.width * 0.32, y0: 0.32, y1: ctx.height - 0.28, side: 0.18, top: 0.24, bottom: 0.42, splits: 2 }); }; var deepWindowWellModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.18, z1: 0.82 }, stone); framedWindow({ ctx, x0: -ctx.width * 0.34, x1: ctx.width * 0.34, y0: 0.28, y1: ctx.height - 0.26, side: 0.22, top: 0.28, bottom: 0.54, splits: 2 }); slab(ctx, "limestone", { x0: -ctx.width * 0.44, x1: ctx.width * 0.44, y0: 0.02, y1: 0.34, z0: 0.82, z1: 1.06 }, stoneWarm); slab(ctx, "limestone", { x0: -ctx.width * 0.44, x1: ctx.width * 0.44, y0: ctx.height - 0.42, y1: ctx.height, z0: 0.82, z1: 1.08 }, stoneWarm); }; var carvedSpandrelVineModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.06, z1: 0.24 }, stoneWarm); greekKey(ctx, -ctx.width / 2 + 0.15, ctx.width / 2 - 0.15, ctx.height * 0.16, 0.24, 0.42, Math.max(4, Math.floor(ctx.width))); for (const x of [-ctx.width * 0.25, 0, ctx.width * 0.25]) { rosette(ctx, x, ctx.height * 0.58, 0.42, 0.6, 0.22); } }; var wrappedCornerPierModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -ctx.depth / 2], [ctx.width / 2, ctx.height, 0.62], stone); solidBox(ctx, "limestone", [ctx.width / 2 - 0.28, 0, -ctx.depth / 2], [ctx.width / 2 + 0.32, ctx.height, ctx.depth / 2], stone); }; var roundedCornerPierModule = (ctx) => { const x0 = -ctx.width / 2; const x1 = ctx.width / 2; const chamfer = Math.min(ctx.width * 0.28, 0.62); const variant = ctx.moduleVariant ?? "both"; const bevelStart = variant === "start" || variant === "both"; const bevelEnd = variant === "end" || variant === "both"; const flatX0 = bevelStart ? x0 + chamfer : x0; const flatX1 = bevelEnd ? x1 - chamfer : x1; solidBox(ctx, "limestone", [flatX0, 0, -0.34], [flatX1, ctx.height, 0.48], stone); if (bevelStart) { solidBox(ctx, "limestone", [x0, 0, -0.34], [x0 + chamfer * 0.25, ctx.height, 0.1], stone); chamferFace(ctx, x0 + chamfer * 0.25, x0 + chamfer, 0.1, 0.48); } if (bevelEnd) { solidBox(ctx, "limestone", [x1 - chamfer * 0.25, 0, -0.34], [x1, ctx.height, 0.1], stone); chamferFace(ctx, x1 - chamfer, x1 - chamfer * 0.25, 0.48, 0.1); } for (let index = 0; index < 5; index++) { const y = ctx.height / 5 * index; slab(ctx, "limestone", { x0: flatX0 - 0.04, x1: flatX1 + 0.04, y0: y, y1: y + 0.18, z0: -0.34, z1: 0.6 }, stoneWarm); } }; var structuralBlankWallModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -0.08], [ctx.width / 2, ctx.height, 0.34], stoneWarm); const rows = Math.max(5, Math.floor(ctx.height / 1.2)); for (let index = 1; index < rows; index++) { const y = ctx.height / rows * index; slab(ctx, "limestone", { x0: -ctx.width / 2 + 0.08, x1: ctx.width / 2 - 0.08, y0: y - 0.02, y1: y + 0.02, z0: 0.34, z1: 0.44 }, stoneDark); } }; function chamferFace(ctx, x0, x1, z0, z1) { ctx.writer.appendQuad("limestone", [ ctx.transform([x0, 0, z0]), ctx.transform([x1, 0, z1]), ctx.transform([x1, ctx.height, z1]), ctx.transform([x0, ctx.height, z0]) ], stone); } function brickCourses(ctx) { const rows = Math.max(8, Math.floor(ctx.height / 0.28)); for (let row = 1; row < rows; row++) { const y = ctx.height / rows * row; slab(ctx, "terra-cotta", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: y - 0.01, y1: y + 0.01, z0: 0.32, z1: 0.42 }, stoneDark); } } var shaftModuleRuntimes = [ { id: "window-3m", builder: window3mModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "window-4m", builder: window4mModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "double-window-bay", builder: doubleWindowBayModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "blank-bay", builder: blankBayModule, slots: ["limestone"] }, { id: "corner-bay", builder: cornerBayModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "vertical-pilaster-strip", builder: verticalPilasterStripModule, slots: ["limestone"] }, { id: "spandrel-panel", builder: spandrelPanelModule, slots: ["limestone", "ornament"] }, { id: "floor-band-strip", builder: floorBandStripModule, slots: ["limestone", "ornament"] }, { id: "central-glass-shaft", builder: centralGlassShaftModule, slots: ["limestone", "glass", "bronze", "black-metal"] }, { id: "solid-side-pier", builder: solidSidePierModule, slots: ["limestone"] }, { id: "buttress-pier", builder: buttressPierModule, slots: ["limestone"] }, { id: "pilaster-bundle", builder: pilasterBundleModule, slots: ["limestone"] }, { id: "recessed-window-slot", builder: recessedWindowSlotModule, slots: ["limestone", "glass", "black-metal"] }, { id: "arched-window-bay", builder: archedWindowBayModule, slots: ["limestone", "glass", "bronze", "black-metal"] }, { id: "arcade-bay", builder: arcadeBayModule, slots: ["limestone", "terra-cotta", "glass", "bronze", "black-metal"] }, { id: "brick-window-bay", builder: brickWindowBayModule, slots: ["terra-cotta", "limestone", "glass", "bronze", "ornament"] }, { id: "deep-window-well", builder: deepWindowWellModule, slots: ["limestone", "glass", "bronze", "ornament"] }, { id: "carved-spandrel-vine", builder: carvedSpandrelVineModule, slots: ["limestone", "ornament"] }, { id: "wrapped-corner-pier", builder: wrappedCornerPierModule, slots: ["limestone"] }, { id: "rounded-corner-pier", builder: roundedCornerPierModule, slots: ["limestone"] }, { id: "structural-blank-wall", builder: structuralBlankWallModule, slots: ["limestone"] } ]; var smallCorniceModule = (ctx) => { cornice(ctx, 0.55, 0.28); }; var largeCorniceModule = (ctx) => { cornice(ctx, 0.82, 0.42); dentils(ctx, -ctx.width / 2 + 0.2, ctx.width / 2 - 0.2, 0.1, 0.32, 0.62, 0.9, Math.max(5, Math.floor(ctx.width * 1.4))); for (const x of [-ctx.width * 0.3, 0, ctx.width * 0.3]) { rosette(ctx, x, ctx.height * 0.5, 0.72, 0.95, 0.22); } }; var cornerCorniceModule = (ctx) => { largeCorniceModule(ctx); solidBox(ctx, "limestone", [ctx.width / 2 - 0.24, 0, -ctx.depth * 0.65], [ctx.width / 2 + 0.26, ctx.height, 0.82], stone); slab(ctx, "ornament", { x0: ctx.width / 2 - 0.18, x1: ctx.width / 2 + 0.18, y0: 0.18, y1: ctx.height - 0.18, z0: -ctx.depth * 0.55, z1: -ctx.depth * 0.12 }, stoneWarm); }; var beltCornerJointModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -ctx.depth / 2], [ctx.width / 2, ctx.height, ctx.depth / 2], stoneWarm); solidBox(ctx, "limestone", [-ctx.width / 2 - 0.08, ctx.height * 0.58, -ctx.depth / 2 - 0.08], [ctx.width / 2 + 0.08, ctx.height, ctx.depth / 2 + 0.08], stone); }; var corniceCornerJointModule = (ctx) => { solidBox(ctx, "limestone", [-ctx.width / 2, 0, -ctx.depth / 2], [ctx.width / 2, ctx.height * 0.34, ctx.depth / 2], stoneWarm); solidBox(ctx, "limestone", [-ctx.width / 2 - 0.14, ctx.height * 0.34, -ctx.depth / 2 - 0.14], [ctx.width / 2 + 0.14, ctx.height * 0.74, ctx.depth / 2 + 0.14], stone); solidBox(ctx, "limestone", [-ctx.width / 2 - 0.24, ctx.height * 0.74, -ctx.depth / 2 - 0.24], [ctx.width / 2 + 0.24, ctx.height, ctx.depth / 2 + 0.24], stone); }; function cornice(ctx, projection, friezeDepth) { const back = -Math.max(0.08, ctx.depth * 0.45); const front = Math.max(projection, ctx.depth * 0.55); slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height * 0.34, z0: back, z1: front * 0.7 }, stoneWarm); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.12, x1: ctx.width / 2 + 0.12, y0: ctx.height * 0.34, y1: ctx.height * 0.72, z0: back, z1: front }, stone); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.18, x1: ctx.width / 2 + 0.18, y0: ctx.height * 0.72, y1: ctx.height, z0: back, z1: front + friezeDepth }, stone); } var beltCourseSmallModule = (ctx) => { bead(ctx, 0, ctx.height, 0.32); }; var beltCourseLargeModule = (ctx) => { bead(ctx, 0, ctx.height, 0.48); slab(ctx, "ornament", { x0: -ctx.width / 2 + 0.16, x1: ctx.width / 2 - 0.16, y0: ctx.height * 0.34, y1: ctx.height * 0.76, z0: 0.26, z1: 0.48 }, stoneWarm); dentils(ctx, -ctx.width / 2 + 0.2, ctx.width / 2 - 0.2, ctx.height * 0.08, ctx.height * 0.24, 0.42, 0.66, Math.max(6, Math.floor(ctx.width * 1.5))); }; var windowSillStripModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: ctx.height * 0.32, y1: ctx.height, z0: -0.08, z1: 0.46 }, stone); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.1, x1: ctx.width / 2 + 0.1, y0: 0, y1: ctx.height * 0.34, z0: -0.08, z1: 0.6 }, stoneWarm); }; var lintelStripModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.42 }, stone); for (let index = 1; index < Math.max(2, Math.floor(ctx.width)); index++) { const x = -ctx.width / 2 + ctx.width / Math.floor(ctx.width) * index; slab(ctx, "limestone", { x0: x - 0.025, x1: x + 0.025, y0: 0.08, y1: ctx.height - 0.08, z0: 0.42, z1: 0.5 }, stoneDark); } }; var floorBandStripModule2 = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: 0, y1: ctx.height, z0: -0.08, z1: 0.32 }, stone); greekKey(ctx, -ctx.width / 2 + 0.18, ctx.width / 2 - 0.18, ctx.height * 0.2, 0.32, 0.54, Math.max(4, Math.floor(ctx.width * 1.25))); }; var dentilCorbelCourseModule = (ctx) => { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0: ctx.height * 0.36, y1: ctx.height, z0: -0.08, z1: 0.62 }, stone); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.1, x1: ctx.width / 2 + 0.1, y0: 0, y1: ctx.height * 0.28, z0: -0.08, z1: 0.8 }, stoneWarm); const count = Math.max(4, Math.floor(ctx.width * 1.2)); dentils(ctx, -ctx.width / 2 + 0.16, ctx.width / 2 - 0.16, ctx.height * 0.1, ctx.height * 0.36, 0.62, 0.92, count); }; function bead(ctx, y0, y1, z) { slab(ctx, "limestone", { x0: -ctx.width / 2, x1: ctx.width / 2, y0, y1, z0: -0.08, z1: z }, stone); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.08, x1: ctx.width / 2 + 0.08, y0, y1: y0 + 0.12, z0: -0.08, z1: z + 0.14 }, stoneWarm); slab(ctx, "limestone", { x0: -ctx.width / 2 - 0.08, x1: ctx.width / 2 + 0.08, y0: y1 - 0.12, y1, z0: -0.08, z1: z + 0.14 }, stoneWarm); } var trimCorniceRuntimes = [ { id: "belt-course-small", builder: beltCourseSmallModule, slots: ["limestone"] }, { id: "belt-course-large", builder: beltCourseLargeModule, slots: ["limestone", "ornament"] }, { id: "window-sill-strip", builder: windowSillStripModule, slots: ["limestone"] }, { id: "lintel-strip", builder: lintelStripModule, slots: ["limestone"] }, { id: "floor-band-strip", builder: floorBandStripModule2, slots: ["limestone", "ornament"] }, { id: "small-cornice", builder: smallCorniceModule, slots: ["limestone", "ornament"] }, { id: "large-cornice", builder: largeCorniceModule, slots: ["limestone", "ornament"] }, { id: "corner-cornice", builder: cornerCorniceModule, slots: ["limestone", "ornament"] }, { id: "belt-corner-joint", builder: beltCornerJointModule, slots: ["limestone"] }, { id: "cornice-corner-joint", builder: corniceCornerJointModule, slots: ["limestone"] }, { id: "dentil-corbel-course", builder: dentilCorbelCourseModule, slots: ["limestone", "ornament"] } ]; var runtimes = [ ...baseModuleRuntimes, ...shaftModuleRuntimes, ...trimCorniceRuntimes, ...crownRoofRuntimes ]; var kitModuleRuntimeById = new Map( runtimes.map((runtime) => [runtime.id, runtime]) ); function getKitModuleRuntime(id) { const runtime = kitModuleRuntimeById.get(id); if (!runtime) throw new Error(`Missing financial core module builder: ${id}`); return runtime; } function missingKitModuleIds() { return financialCoreModules.map((module2) => module2.id).filter((id) => !kitModuleRuntimeById.has(id)); } function assertCompleteKitRegistry() { const missing = missingKitModuleIds(); if (missing.length > 0) { throw new Error(`Financial core kit is missing module builders: ${missing.join(", ")}`); } } function assertGeneratorInvariants(placements) { const missing = missingKitModuleIds(); if (missing.length > 0) { throw new Error(`Financial core kit is missing module builders: ${missing.join(", ")}`); } const duplicates = duplicateSurfaceOwners(placements); if (duplicates.length > 0) { throw new Error(`Duplicate facade surface owner regions: ${duplicates.join(", ")}`); } } function duplicateSurfaceOwners(placements) { const owners = /* @__PURE__ */ new Set(); const duplicates = /* @__PURE__ */ new Set(); for (const placement of placements) { if (placement.side === "roof") continue; const key = surfaceKey(placement); if (owners.has(key)) duplicates.add(key); owners.add(key); } return [...duplicates]; } function unusedModuleIds(placements) { const used = new Set(placements.map((placement) => placement.id)); return financialCoreModules.map((module2) => module2.id).filter((id) => !used.has(id)); } function surfaceKey(placement) { return [ placement.side, placement.tierName, placement.edgeId ?? "whole-side", rounded(placement.xOffset ?? 0), rounded(placement.zOffset ?? 0), rounded(placement.center - placement.width / 2), rounded(placement.center + placement.width / 2), rounded(placement.y), rounded(placement.y + placement.height), rounded(placement.normalOffset ?? 0) ].join(":"); } function rounded(value) { return Math.round(value * 100) / 100; } function createFinancialBuildingPlan(settings) { const tiers = createMassTiers(settings); const placements = createKitPlacements(settings, tiers); assertGeneratorInvariants(placements); return { settings, bayWidth: BAY_WIDTH, floorHeight: FLOOR_HEIGHT, tiers, bays: legacyBays(placements), placements, diagnostics: { duplicateSurfaceOwners: duplicateSurfaceOwners(placements), missingModuleIds: missingKitModuleIds(), unusedModuleIds: unusedModuleIds(placements) } }; } function legacyBays(placements) { return placements.filter((placement) => placement.side !== "roof").map((placement) => ({ kind: "blank", side: placement.side, tierName: placement.tierName, alongStart: placement.center - placement.width / 2, alongEnd: placement.center + placement.width / 2, y0: placement.y, y1: placement.y + placement.height, floorIndex: placement.floorIndex, bayIndex: placement.bayIndex, isCorner: placement.id.includes("corner") })); } import * as THREE from "three/webgpu"; var materialSlots = [ "limestone", "granite", "terra-cotta", "glass", "bronze", "black-metal", "ornament", "roof" ]; var STONE_TILE_METERS = 1.45; var STONE_ATLAS_COLUMNS = 3; var STONE_ATLAS_ROWS = 2; var ATLAS_PADDING = 4e-3; var COHERENT_STONE_CELL = 4; var KitMeshWriter = class { buffers = Object.fromEntries( materialSlots.map((slot) => [slot, emptyBuffer()]) ); appendQuad(slot, corners, color, uvScale = 0.35) { if (slot === "limestone" || slot === "ornament") { this.appendAtlasQuad(slot, corners, color); return; } const width = distance(corners[0], corners[1]) * uvScale; const height = distance(corners[1], corners[2]) * uvScale; this.appendQuadRaw(slot, corners, color, [ [0, 0], [width, 0], [width, height], [0, height] ]); } appendAtlasQuad(slot, corners, color) { const width = distance(corners[0], corners[1]); const height = distance(corners[1], corners[2]); const uCuts = segmentCuts(width); const vCuts = segmentCuts(height); const atlasCell = chooseStoneAtlasCell(slot); for (let u = 0; u < uCuts.length - 1; u++) { for (let v = 0; v < vCuts.length - 1; v++) { const u0 = uCuts[u]; const u1 = uCuts[u + 1]; const v0 = vCuts[v]; const v1 = vCuts[v + 1]; const subCorners = [ bilerp(corners, u0, v0), bilerp(corners, u1, v0), bilerp(corners, u1, v1), bilerp(corners, u0, v1) ]; const uSpan = Math.min(1, width * (u1 - u0) / STONE_TILE_METERS); const vSpan = Math.min(1, height * (v1 - v0) / STONE_TILE_METERS); this.appendQuadRaw(slot, subCorners, color, atlasUvs(atlasCell, uSpan, vSpan)); } } } appendQuadRaw(slot, corners, color, uvs) { const buffer = this.buffers[slot]; const base = buffer.positions.length / 3; const normal = faceNormal(corners); for (let index = 0; index < 4; index++) { buffer.positions.push(...corners[index]); buffer.normals.push(...normal); buffer.uvs.push(...uvs[index]); buffer.colors.push(...color); } buffer.indices.push(base, base + 1, base + 2, base, base + 2, base + 3); } appendBox(slot, min, max, color, faces = {}) { const add = (face) => faces[face] !== false; const [x0, y0, z0] = min; const [x1, y1, z1] = max; if (add("front")) this.appendQuad(slot, [[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]], color); if (add("back")) this.appendQuad(slot, [[x1, y0, z0], [x0, y0, z0], [x0, y1, z0], [x1, y1, z0]], color); if (add("right")) this.appendQuad(slot, [[x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1]], color); if (add("left")) this.appendQuad(slot, [[x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0]], color); if (add("top")) this.appendQuad(slot, [[x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0]], color); if (add("bottom")) this.appendQuad(slot, [[x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1]], color); } appendFrom(other) { for (const slot of materialSlots) { const target = this.buffers[slot]; const source = other.buffers[slot]; const offset = target.positions.length / 3; target.positions.push(...source.positions); target.normals.push(...source.normals); target.uvs.push(...source.uvs); target.colors.push(...source.colors); target.indices.push(...source.indices.map((index) => index + offset)); } } toGeometries() { return Object.fromEntries( materialSlots.map((slot) => [slot, toGeometry(this.buffers[slot])]) ); } triangleCount() { return materialSlots.reduce( (sum, slot) => sum + this.buffers[slot].indices.length / 3, 0 ); } }; function emptyBuffer() { return { positions: [], normals: [], uvs: [], colors: [], indices: [] }; } function segmentCuts(length) { if (length <= 1e-3) return [0, 1]; const count = Math.max(1, Math.ceil(length / STONE_TILE_METERS)); return Array.from({ length: count + 1 }, (_, index) => Math.min(1, index * STONE_TILE_METERS / length)); } function chooseStoneAtlasCell(slot) { if (slot === "ornament") return COHERENT_STONE_CELL; return COHERENT_STONE_CELL; } function atlasUvs(cell, uSpan, vSpan) { const column = cell % STONE_ATLAS_COLUMNS; const rowFromTop = Math.floor(cell / STONE_ATLAS_COLUMNS); const cellWidth = 1 / STONE_ATLAS_COLUMNS; const cellHeight = 1 / STONE_ATLAS_ROWS; const u0 = column * cellWidth + ATLAS_PADDING; const v0 = 1 - (rowFromTop + 1) * cellHeight + ATLAS_PADDING; const u1 = u0 + (cellWidth - ATLAS_PADDING * 2) * uSpan; const v1 = v0 + (cellHeight - ATLAS_PADDING * 2) * vSpan; return [ [u0, v0], [u1, v0], [u1, v1], [u0, v1] ]; } function toGeometry(buffer) { if (buffer.positions.length === 0) return null; const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.Float32BufferAttribute(buffer.positions, 3)); geometry.setAttribute("normal", new THREE.Float32BufferAttribute(buffer.normals, 3)); geometry.setAttribute("uv", new THREE.Float32BufferAttribute(buffer.uvs, 2)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(buffer.colors, 3)); geometry.setIndex(buffer.indices); geometry.computeBoundingSphere(); return geometry; } function faceNormal(corners) { const a = subtract(corners[1], corners[0]); const b = subtract(corners[2], corners[0]); const n = cross(a, b); const length = Math.hypot(...n) || 1; return [n[0] / length, n[1] / length, n[2] / length]; } function subtract(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; } function bilerp(corners, u, v) { const bottom = lerp(corners[0], corners[1], u); const top = lerp(corners[3], corners[2], u); return lerp(bottom, top, v); } function lerp(a, b, t) { return [ a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t ]; } function cross(a, b) { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; } function distance(a, b) { return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); } function facadeTransform(input) { const offsetX = input.offsetX ?? 0; const offsetZ = input.offsetZ ?? 0; const normalOffset = input.normalOffset ?? 0; return ([x, y, z]) => { if (input.side === "front") return [offsetX + input.center + x, input.y + y, offsetZ + input.buildingDepth / 2 + normalOffset + z]; if (input.side === "back") return [offsetX + input.center - x, input.y + y, offsetZ - input.buildingDepth / 2 - normalOffset - z]; if (input.side === "right") return [offsetX + input.buildingWidth / 2 + normalOffset + z, input.y + y, offsetZ - input.center - x]; return [offsetX - input.buildingWidth / 2 - normalOffset - z, input.y + y, offsetZ + input.center + x]; }; } function roofTransform(input) { return ([x, y, z]) => [input.x + x, input.y + y, input.z + z]; } function appendMassCaps(writer, tiers) { for (const tier of tiers) appendTierSoffit(writer, tier); for (const tier of tiers) { if (tier.role === "shaft") continue; appendTierDeck(writer, tier, tier.role === "crown"); } appendSameLevelConnectors(writer, tiers); } function appendTierSoffit(writer, tier) { if (tier.y0 <= 1e-3) return; const y0 = tier.y0 - 0.16; const y1 = tier.y0; const x0 = tier.x - tier.width / 2; const x1 = tier.x + tier.width / 2; const z0 = tier.z - tier.depth / 2; const z1 = tier.z + tier.depth / 2; writer.appendBox("limestone", [x0, y0, z0], [x1, y1, z1], stoneDark); writer.appendBox("limestone", [x0, y0 - 0.16, z1 - 0.22], [x1, y1, z1 + 0.08], stoneDark); writer.appendBox("limestone", [x0, y0 - 0.16, z0 - 0.08], [x1, y1, z0 + 0.22], stoneDark); writer.appendBox("limestone", [x1 - 0.22, y0 - 0.16, z0], [x1 + 0.08, y1, z1], stoneDark); writer.appendBox("limestone", [x0 - 0.08, y0 - 0.16, z0], [x0 + 0.22, y1, z1], stoneDark); } function appendTierDeck(writer, tier, roof) { const y = tier.y0 + tier.height; const deckTop = y - 0.035; writer.appendBox( roof ? "roof" : "limestone", [tier.x - tier.width / 2, y - 0.14, tier.z - tier.depth / 2], [tier.x + tier.width / 2, deckTop, tier.z + tier.depth / 2], roof ? roofMetal : stoneDark ); appendDeckEdges(writer, tier, deckTop, roof); } function appendSameLevelConnectors(writer, tiers) { const levels = /* @__PURE__ */ new Map(); for (const tier of tiers) { const key = `${tier.role}:${tier.y0}:${tier.height}`; levels.set(key, [...levels.get(key) ?? [], tier]); } for (const group of levels.values()) { if (group.length < 2) continue; for (let a = 0; a < group.length; a++) { for (let b = a + 1; b < group.length; b++) appendConnector(writer, group[a], group[b]); } } } function appendConnector(writer, a, b) { const y = a.y0 + a.height; const slabY0 = y - 0.16; const slabY1 = y - 0.035; const ax0 = a.x - a.width / 2; const ax1 = a.x + a.width / 2; const az0 = a.z - a.depth / 2; const az1 = a.z + a.depth / 2; const bx0 = b.x - b.width / 2; const bx1 = b.x + b.width / 2; const bz0 = b.z - b.depth / 2; const bz1 = b.z + b.depth / 2; if (almostEqual(az0, bz1) || almostEqual(az1, bz0)) { const x0 = Math.max(ax0, bx0); const x1 = Math.min(ax1, bx1); if (x1 <= x0) return; const z = almostEqual(az0, bz1) ? az0 : az1; writer.appendBox("limestone", [x0, slabY0, z - 0.18], [x1, slabY1, z + 0.18], stoneDark); } if (almostEqual(ax0, bx1) || almostEqual(ax1, bx0)) { const z0 = Math.max(az0, bz0); const z1 = Math.min(az1, bz1); if (z1 <= z0) return; const x = almostEqual(ax0, bx1) ? ax0 : ax1; writer.appendBox("limestone", [x - 0.18, slabY0, z0], [x + 0.18, slabY1, z1], stoneDark); } } function almostEqual(a, b) { return Math.abs(a - b) < 1e-3; } function appendDeckEdges(writer, tier, y, roof) { const slot = roof ? "roof" : "limestone"; const color = roof ? roofMetal : stoneDark; const x0 = tier.x - tier.width / 2; const x1 = tier.x + tier.width / 2; const z0 = tier.z - tier.depth / 2; const z1 = tier.z + tier.depth / 2; writer.appendBox(slot, [x0, y, z1 - 0.18], [x1, y + 0.34, z1 + 0.12], color); writer.appendBox(slot, [x0, y, z0 - 0.12], [x1, y + 0.34, z0 + 0.18], color); writer.appendBox(slot, [x1 - 0.18, y, z0], [x1 + 0.12, y + 0.34, z1], color); writer.appendBox(slot, [x0 - 0.12, y, z0], [x0 + 0.18, y + 0.34, z1], color); writer.appendBox("granite", [x0 + 0.3, y + 0.02, z1 - 0.08], [x1 - 0.3, y + 0.08, z1 + 0.02], stoneDark); } var topologyColors = { podium: [0.46, 0.56, 0.68], shaft: [0.58, 0.62, 0.54], crown: [0.66, 0.55, 0.42], bridge: [0.5, 0.64, 0.66] }; function compileFinancialBuilding(plan) { assertCompleteKitRegistry(); const writer = new KitMeshWriter(); if (plan.settings.debugMode === "topology") { for (const tier of plan.tiers) appendTopologyBlock(writer, tier); } else { appendMassCaps(writer, plan.tiers); for (const placement of plan.placements) appendPlacement(writer, plan, placement); } const moduleUsage = moduleUsageFrom(plan.placements); return { geometries: writer.toGeometries(), triangleCount: writer.triangleCount(), moduleCount: plan.placements.length, plan, moduleUsage }; } function appendPlacement(writer, plan, placement) { const runtime = getKitModuleRuntime(placement.id); const tier = plan.tiers.find((candidate) => candidate.name === placement.tierName); const transform = placement.side === "roof" ? roofTransform({ x: placement.center, y: placement.y, z: placement.roofZ ?? 0 }) : facadeTransform({ side: placement.side, buildingWidth: tier?.width ?? plan.tiers[0].width, buildingDepth: tier?.depth ?? plan.tiers[0].depth, center: placement.center, y: placement.y, offsetX: placement.xOffset ?? tier?.x ?? 0, offsetZ: placement.zOffset ?? tier?.z ?? 0, normalOffset: placement.normalOffset ?? 0 }); runtime.builder({ writer, transform, moduleId: placement.id, width: placement.width, height: placement.height, depth: placement.depth, anchors: {}, moduleVariant: placement.moduleVariant }); } function appendTopologyBlock(writer, tier) { writer.appendBox( "limestone", [tier.x - tier.width / 2, tier.y0, tier.z - tier.depth / 2], [tier.x + tier.width / 2, tier.y0 + tier.height, tier.z + tier.depth / 2], topologyColors[tier.role] ); } function moduleUsageFrom(placements) { const usage = {}; for (const module2 of financialCoreModules) usage[module2.id] = 0; for (const placement of placements) usage[placement.id] = (usage[placement.id] ?? 0) + 1; return usage; } // .example-captures/audit-build/procedural-tower-building-entry.ts var PROCEDURAL_FINANCIAL_TOWER_SETTINGS = Object.freeze({ seed: 1042, variant: "setback-tower", widthBays: 9, depthBays: 7, floors: 17, podiumFloors: 3, setbackFloors: 2, towerScale: 0.82, ornamentDensity: 0.72, colonnade: true, cornerEntrance: true, crown: true, materialVariant: "light-limestone", debugMode: "beauty", activeTab: "building", selectedModuleId: "round-column", podiumStyle: "colonnade", entranceType: "center-revolving", shaftRhythm: "chicago-grid", crownStyle: "windowed-crown", roofEquipmentDensity: 0.7, massingPattern: "single-tower", footprintStyle: "rectangle", secondaryFootprintStyle: "rectangle", footprintHeightMode: "full-height", hardInsetSide: "none", hardInsetAmount: 0, innerCourtWidth: 0.38, innerCourtDepth: 0.42, innerCourtOffsetX: 0, innerCourtOffsetZ: 0, skybridgeEnabled: true, skybridgeFloor: 8, buildingArchetype: "board-of-trade-tower", roofStyle: "statue-tower", porticoProjection: 1.8, centralAxisBays: 3, cornerTreatment: "rounded-piers", crownDecorationStyle: "classical", crownDecorationDensity: 0.6, crownFinialRhythm: "edge-regular", crownFinialDensity: 0.55 }); var renderOrder = [ "limestone", "granite", "terra-cotta", "ornament", "glass", "bronze", "black-metal", "roof" ]; var debugModeMap = /* @__PURE__ */ new Map([ ["final", "beauty"], ["beauty", "beauty"], ["topology", "topology"], ["facade-ids", "facade-ids"] ]); function createBuildingPlan(overrides = {}) { return createFinancialBuildingPlan({ ...PROCEDURAL_FINANCIAL_TOWER_SETTINGS, ...overrides }); } function generateBuilding(settings = {}) { return compileFinancialBuilding(createBuildingPlan(settings)); } function compileBuilding(planOrSettings = createBuildingPlan(), materials = {}) { let settings = normalizeSettings(planOrSettings); let generated = compileFinancialBuilding(createFinancialBuildingPlan(settings)); const root = new THREE2.Group(); const meshes = /* @__PURE__ */ new Map(); applyGeneratedGeometry(generated); function applyGeneratedGeometry(nextGenerated) { generated = nextGenerated; for (const slot of renderOrder) { const geometry = generated.geometries[slot]; let mesh = meshes.get(slot); if (!geometry) { if (mesh) { root.remove(mesh); mesh.geometry.dispose(); meshes.delete(slot); } continue; } if (!mesh) { mesh = new THREE2.Mesh(geometry, materials[slot] ?? fallbackMaterial(slot)); mesh.name = `procedural-financial-tower:${slot}`; mesh.castShadow = true; mesh.receiveShadow = true; meshes.set(slot, mesh); root.add(mesh); } else { mesh.geometry.dispose(); mesh.geometry = geometry; } } } return { root, get moduleCount() { return generated.moduleCount; }, get triangleCount() { return generated.triangleCount; }, get plan() { return generated.plan; }, get moduleUsage() { return generated.moduleUsage; }, setDebugMode(modeName) { const debugMode = debugModeMap.get(modeName) ?? "beauty"; settings = { ...settings, debugMode }; applyGeneratedGeometry( compileFinancialBuilding(createFinancialBuildingPlan(settings)) ); }, dispose() { for (const mesh of meshes.values()) { mesh.geometry.dispose(); } } }; } function normalizeSettings(planOrSettings) { const sourceSettings = planOrSettings?.settings ?? planOrSettings; return { ...PROCEDURAL_FINANCIAL_TOWER_SETTINGS, ...sourceSettings }; } function fallbackMaterial(slot) { return new THREE2.MeshStandardMaterial({ color: slot === "glass" ? 462868 : 16052196, roughness: slot === "glass" ? 0.08 : 0.8, metalness: slot === "glass" ? 0.72 : 0.02, vertexColors: true }); } export { PROCEDURAL_FINANCIAL_TOWER_SETTINGS, compileBuilding, compileFinancialBuilding, createBuildingPlan, createFinancialBuildingPlan, generateBuilding };