{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "window",
  "type": "registry:component",
  "title": "Window (A60 / H120 / blast)",
  "description": "Fixed offshore window in a short lined wall stub with a real hole. Square outer frame filling the hole, rounded-corner lights, exterior bolting flange, double glazing (laminated panes for the blast type) with a spacer, EPDM gaskets both faces, bolted inside clamping ring, interior window board and exterior drip sill, rating plate and tag. Mullions split it into `panes` lights; `sunshade` adds an external louvred eyebrow. +Z is the outside face. Set `wall: none` to drop it into a room wall; `windowOpening()` gives the hole.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "interiors/window.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/interiors/window.tsx",
      "content": "// Offshore window set in a wall stub: fixed A60 / H120 or blast-rated, rounded-corner bolted frame, double glazing with gaskets, optional sunshade. IMO FTP Code, ISO 3903.\nimport { useEffect, useMemo } from 'react'\nimport * as THREE from 'three'\nimport { Kit, type MatKey } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { KitMesh, type GroupProps } from '../lib/mesh'\nimport { tagPlate } from '../lib/parts'\nimport { liningMaterial, ratingLabelMaterial } from './_textures'\nimport { wallWithHoles } from './_wall'\n\nexport type FireRating = 'A60' | 'H120' | 'none'\n\nexport interface WindowProps {\n  kind?: 'fixed' | 'blast'\n  fireRating?: FireRating\n  /** glazed width, m */\n  width?: number\n  /** glazed height, m */\n  height?: number\n  /** glazing bottom above the floor, m */\n  sill?: number\n  /** lights across, separated by mullions */\n  panes?: number\n  /** external louvred sunshade over the window (+Z is outside) */\n  sunshade?: boolean\n  wall?: 'stub' | 'none'\n  wallThickness?: number\n  tag?: string\n}\n\nexport const defaults = {\n  kind: 'fixed' as 'fixed' | 'blast',\n  fireRating: 'A60' as FireRating,\n  width: 1.2,\n  height: 0.9,\n  sill: 1.0,\n  panes: 1,\n  sunshade: false,\n  wall: 'stub' as 'stub' | 'none',\n  wallThickness: 0.15,\n  tag: 'W-101',\n}\n\nconst clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))\n\n/** Rough hole a window needs in a wall: centred on x = 0, from y0 to y1. */\nexport function windowOpening(p: Pick<WindowProps, 'kind' | 'width' | 'height' | 'sill'> = {}) {\n  const blast = (p.kind ?? defaults.kind) === 'blast'\n  const W = clamp(p.width ?? defaults.width, 0.3, 4)\n  const H = clamp(p.height ?? defaults.height, 0.3, 2.4)\n  const FR = blast ? 0.1 : 0.07\n  const sill = clamp(p.sill ?? defaults.sill, 0.1, 2.5)\n  return { W, H, FR, holeW: W + 2 * FR, y0: sill - FR, y1: sill + H + FR }\n}\n\nexport function Window(props: WindowProps & GroupProps) {\n  const { kind, fireRating, width, height, sill, panes, sunshade, wall, wallThickness, tag, ...group } = { ...defaults, ...props }\n  const b = useMemo(() => build({ kind, fireRating, width, height, sill, panes, sunshade, wall, wallThickness, tag }), [kind, fireRating, width, height, sill, panes, sunshade, wall, wallThickness, tag])\n  useEffect(() => () => b.geometry.dispose(), [b])\n  return (\n    <group {...group}>\n      <KitMesh data={b} />\n    </group>\n  )\n}\n\nexport default Window\n\nfunction rrect(w: number, h: number, r: number, cx = 0, cy = 0) {\n  const s = new THREE.Shape()\n  const x0 = cx - w / 2\n  const y0 = cy - h / 2\n  r = Math.min(r, w / 2 - 1e-3, h / 2 - 1e-3)\n  s.moveTo(x0 + r, y0)\n  s.lineTo(x0 + w - r, y0)\n  s.absarc(x0 + w - r, y0 + r, r, -Math.PI / 2, 0, false)\n  s.lineTo(x0 + w, y0 + h - r)\n  s.absarc(x0 + w - r, y0 + h - r, r, 0, Math.PI / 2, false)\n  s.lineTo(x0 + r, y0 + h)\n  s.absarc(x0 + r, y0 + h - r, r, Math.PI / 2, Math.PI, false)\n  s.lineTo(x0, y0 + r)\n  s.absarc(x0 + r, y0 + r, r, Math.PI, Math.PI * 1.5, false)\n  return s\n}\nfunction ring(k: Kit, mat: MatKey, ow: number, oh: number, orad: number, iw: number, ih: number, irad: number, z0: number, depth: number, cy: number) {\n  const s = rrect(ow, oh, orad, 0, cy)\n  s.holes.push(rrect(iw, ih, irad, 0, cy))\n  k.extrude(mat, s, depth, new THREE.Matrix4().makeTranslation(0, 0, z0), 12)\n}\n\nfunction build(p: typeof defaults) {\n  const k = new Kit()\n  const o = windowOpening(p)\n  const { W, H, FR } = o\n  const blast = p.kind === 'blast'\n  const T = clamp(p.wallThickness, 0.06, 0.4)\n  const FD = T + 0.04\n  const cy = p.sill + H / 2\n  const rIn = blast ? 0.06 : 0.08 // rounded glass corners, marine practice\n  const frameMat = paint(blast ? '#5d646a' : '#c9cdc9', { metalness: 0.35, roughness: 0.42, clearcoat: 0.2, weather: 0 })\n  const n = clamp(Math.round(p.panes), 1, 6)\n  const mull = 0.06\n  const lw = (W - (n - 1) * mull) / n\n\n  if (p.wall === 'stub') {\n    const len = o.holeW + 1.4\n    const hh = Math.max(2.9, o.y1 + 0.6)\n    wallWithHoles(k, len, hh, T, [{ u: 0, w: o.holeW, y0: o.y0, y1: o.y1 }], liningMaterial())\n    for (const s of [-1, 1]) k.box('rubberBlack', [len, 0.1, 0.008], [0, 0.05, s * (T / 2 + 0.004)])\n  }\n\n  // main frame: square outside to fill the hole, rounded light openings; exterior bolting flange\n  ring(k, frameMat, o.holeW, H + 2 * FR, 0.004, W, H, rIn, -FD / 2, FD, cy)\n  ring(k, frameMat, o.holeW + 0.1, H + 2 * FR + 0.1, 0.03, W + 0.02, H + 0.02, rIn + 0.01, FD / 2 - 0.012, 0.012, cy)\n  ring(k, frameMat, o.holeW + 0.04, H + 2 * FR + 0.04, 0.02, W + 0.02, H + 0.02, rIn + 0.01, -FD / 2 - 0.006, 0.006, cy) // interior trim\n  // mullions\n  for (let i = 1; i < n; i++) {\n    const x = -W / 2 + i * lw + (i - 0.5) * mull\n    k.box(frameMat, [mull, H, FD], [x, cy, 0])\n  }\n  // double glazing per light: outer pane, spacer, inner pane, gaskets, clamping ring with bolts\n  const tOut = blast ? 0.0128 : 0.01 // laminated outer for blast\n  const tIn = blast ? 0.0175 : 0.012\n  const gap = 0.016\n  const zG = 0.0\n  for (let i = 0; i < n; i++) {\n    const x = -W / 2 + i * (lw + mull) + lw / 2\n    const r = n > 1 ? Math.min(rIn, 0.03) : rIn\n    const pane = (z: number, t: number) => k.extrude('glass', rrect(lw - 0.004, H - 0.004, r, x, cy), t, new THREE.Matrix4().makeTranslation(0, 0, z), 12)\n    pane(zG + gap / 2, tOut)\n    pane(zG - gap / 2 - tIn, tIn)\n    const sp = rrect(lw, H, r, x, cy)\n    sp.holes.push(rrect(lw - 0.03, H - 0.03, Math.max(0.005, r - 0.015), x, cy))\n    k.extrude('nut', sp, gap, new THREE.Matrix4().makeTranslation(0, 0, zG - gap / 2), 12)\n    for (const [z, d] of [[zG + gap / 2 + tOut, 0.006], [zG - gap / 2 - tIn - 0.006, 0.006]] as const) {\n      const g = rrect(lw + 0.006, H + 0.006, r + 0.003, x, cy)\n      g.holes.push(rrect(lw - 0.024, H - 0.024, Math.max(0.005, r - 0.012), x, cy))\n      k.extrude('rubberBlack', g, d, new THREE.Matrix4().makeTranslation(0, 0, z), 12)\n    }\n    // clamping ring on the inside face, bolted\n    const cr = rrect(lw + 0.03, H + 0.03, r + 0.015, x, cy)\n    cr.holes.push(rrect(lw - 0.03, H - 0.03, Math.max(0.005, r - 0.015), x, cy))\n    k.extrude(frameMat, cr, 0.012, new THREE.Matrix4().makeTranslation(0, 0, -FD / 2 - 0.012), 12)\n    const per = 2 * (lw + H)\n    const nb = Math.max(8, Math.round(per / (blast ? 0.12 : 0.2)))\n    for (let b = 0; b < nb; b++) {\n      const q = boundaryPoint(lw + 0.004, H + 0.004, (b / nb) * per)\n      k.hex('nut', blast ? 0.022 : 0.016, 0.008, [x + q[0], cy + q[1], -FD / 2 - 0.016], 'z')\n    }\n  }\n  // exterior flange bolts (bolt-on frame)\n  {\n    const ow = o.holeW + 0.05\n    const oh = H + 2 * FR + 0.05\n    const per = 2 * (ow + oh)\n    const nb = Math.max(12, Math.round(per / (blast ? 0.1 : 0.15)))\n    for (let b = 0; b < nb; b++) {\n      const q = boundaryPoint(ow, oh, (b / nb) * per)\n      k.hex('nut', blast ? 0.026 : 0.02, 0.01, [q[0], cy + q[1], FD / 2 + 0.005], 'z')\n    }\n  }\n  // interior window board and exterior drip sill\n  k.box(frameMat, [o.holeW + 0.08, 0.025, 0.12], [0, o.y0 - 0.0125, -FD / 2 - 0.05])\n  k.box(frameMat, [o.holeW + 0.1, 0.012, 0.1], [0, o.y0 - 0.02, FD / 2 + 0.04], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.12))\n  // rating plate and tag inside\n  if (p.fireRating !== 'none') k.plane(ratingLabelMaterial(blast ? `${p.fireRating}` : p.fireRating), 0.09, 0.045, [o.holeW / 2 - 0.07, o.y0 + FR / 2, -FD / 2 - 0.0065], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI))\n  tagPlate(k, p.tag, [-o.holeW / 2 + 0.07, o.y0 + FR / 2, -FD / 2 - 0.0065], '-z', 0.08)\n\n  if (p.sunshade) {\n    // louvred eyebrow on cantilever brackets, outside\n    const sw = o.holeW + 0.3\n    const dep = Math.min(0.6, 0.25 + H * 0.3)\n    const y = o.y1 + 0.12\n    for (const s of [-1, 1]) {\n      const sx = s * (sw / 2 - 0.03)\n      k.box(frameMat, [0.012, 0.18, dep], [sx, y - 0.04, FD / 2 + dep / 2])\n      k.box(frameMat, [0.08, 0.22, 0.012], [sx, y - 0.03, FD / 2 + 0.006])\n      k.rod(frameMat, [sx, y - 0.13, FD / 2 + 0.01], [sx, y - 0.02, FD / 2 + dep - 0.02], 0.012, 8)\n    }\n    const nl = Math.max(4, Math.round(dep / 0.08))\n    for (let i = 0; i < nl; i++) k.box('aluminium', [sw - 0.06, 0.006, 0.07], [0, y + 0.02, FD / 2 + 0.05 + (i * (dep - 0.08)) / (nl - 1)], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -0.6))\n    k.box(frameMat, [sw, 0.05, 0.012], [0, y + 0.02, FD / 2 + dep])\n  }\n  return k.build()\n}\n\n/** Point at distance d around a rectangle (w × h) centred on the origin. */\nfunction boundaryPoint(w: number, h: number, d: number): [number, number] {\n  if (d < w) return [-w / 2 + d, -h / 2]\n  d -= w\n  if (d < h) return [w / 2, -h / 2 + d]\n  d -= h\n  if (d < w) return [w / 2 - d, h / 2]\n  d -= w\n  return [-w / 2, h / 2 - d]\n}\n"
    },
    {
      "path": "interiors/_textures.ts",
      "type": "registry:file",
      "target": "components/deck-twin/interiors/_textures.ts",
      "content": "// Canvas materials for control-room interiors: wall lining, raised floor, ceiling tiles, LED panels, perforated doors, shutter slats, labels.\nimport * as THREE from 'three'\n\ntype G = CanvasRenderingContext2D\nconst cache = new Map<string, THREE.MeshStandardMaterial>()\n\n/** Cached canvas-texture material; `draw` runs once, on first use. */\nexport function canvasMaterial(key: string, w: number, h: number, draw: (g: G, w: number, h: number) => void, o: { repeat?: boolean; roughness?: number; metalness?: number; emissive?: number; alphaTest?: number; transparent?: boolean; side?: THREE.Side } = {}) {\n  const hit = cache.get(key)\n  if (hit) return hit\n  const c = document.createElement('canvas')\n  c.width = w\n  c.height = h\n  draw(c.getContext('2d')!, w, h)\n  const tex = new THREE.CanvasTexture(c)\n  tex.colorSpace = THREE.SRGBColorSpace\n  tex.anisotropy = 8\n  if (o.repeat) tex.wrapS = tex.wrapT = THREE.RepeatWrapping\n  const m = new THREE.MeshStandardMaterial({ map: tex, metalness: o.metalness ?? 0.05, roughness: o.roughness ?? 0.6, alphaTest: o.alphaTest ?? 0, transparent: o.transparent ?? false, side: o.side ?? THREE.FrontSide })\n  if (o.emissive) {\n    m.emissive = new THREE.Color('#ffffff')\n    m.emissiveMap = tex\n    m.emissiveIntensity = o.emissive\n  }\n  m.name = `int-${key}`\n  cache.set(key, m)\n  return m\n}\n\nfunction rng(seed: number) {\n  let s = seed >>> 0\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296\n}\nfunction speckle(g: G, w: number, h: number, base: string, n: number, dark: string, light: string, seed = 1) {\n  g.fillStyle = base\n  g.fillRect(0, 0, w, h)\n  const r = rng(seed)\n  for (let i = 0; i < n; i++) {\n    g.fillStyle = r() < 0.5 ? dark : light\n    g.fillRect(r() * w, r() * h, 1 + r() * 2, 1 + r() * 2)\n  }\n}\n\n/** Steel-faced wall lining panel, 600 mm module with a shadow-gap joint. UV 1 = 0.6 m. */\nexport function liningMaterial(color = '#e3e4e0') {\n  return canvasMaterial(`lining-${color}`, 128, 128, (g, w, h) => {\n    speckle(g, w, h, color, 220, 'rgba(0,0,0,0.035)', 'rgba(255,255,255,0.05)', 3)\n    g.fillStyle = 'rgba(40,44,48,0.45)'\n    g.fillRect(0, 0, 2, h)\n    g.fillStyle = 'rgba(255,255,255,0.4)'\n    g.fillRect(2, 0, 1, h)\n  }, { repeat: true, roughness: 0.55, metalness: 0.15 })\n}\n\n/** 600 × 600 raised-access floor tile with an anti-static vinyl finish. UV 1 = 0.6 m. */\nexport function raisedFloorMaterial(color = '#8d9396') {\n  return canvasMaterial(`rf-${color}`, 256, 256, (g, w, h) => {\n    speckle(g, w, h, color, 2600, 'rgba(0,0,0,0.12)', 'rgba(255,255,255,0.1)', 7)\n    g.strokeStyle = 'rgba(30,32,34,0.75)'\n    g.lineWidth = 3\n    g.strokeRect(1.5, 1.5, w - 3, h - 3)\n    g.strokeStyle = 'rgba(255,255,255,0.18)'\n    g.lineWidth = 1\n    g.strokeRect(4, 4, w - 8, h - 8)\n  }, { repeat: true, roughness: 0.7 })\n}\n\n/** Carpet tile, 500 mm module. UV 1 = 0.5 m. */\nexport function carpetMaterial(color = '#4a5058') {\n  return canvasMaterial(`carpet-${color}`, 256, 256, (g, w, h) => {\n    speckle(g, w, h, color, 9000, 'rgba(0,0,0,0.16)', 'rgba(255,255,255,0.07)', 9)\n    g.strokeStyle = 'rgba(0,0,0,0.25)'\n    g.lineWidth = 2\n    g.strokeRect(1, 1, w - 2, h - 2)\n  }, { repeat: true, roughness: 0.95, metalness: 0 })\n}\n\n/** Mineral-fibre suspended ceiling tile on a 600 mm exposed grid. UV 1 = 0.6 m. */\nexport function ceilingMaterial() {\n  return canvasMaterial('ceiling', 256, 256, (g, w, h) => {\n    speckle(g, w, h, '#ecece8', 1800, 'rgba(0,0,0,0.08)', 'rgba(255,255,255,0.4)', 5)\n    g.fillStyle = '#f7f7f5'\n    g.fillRect(0, 0, w, 6)\n    g.fillRect(0, 0, 6, h)\n    g.fillStyle = 'rgba(0,0,0,0.12)'\n    g.fillRect(6, 6, w - 6, 2)\n    g.fillRect(6, 6, 2, h - 6)\n  }, { repeat: true, roughness: 0.9, metalness: 0, side: THREE.DoubleSide })\n}\n\n/** Flat 600 × 600 LED panel: opal diffuser with a slightly brighter edge-lit rim. */\nexport function ledPanelMaterial(intensity = 1.6) {\n  return canvasMaterial(`led-${intensity}`, 128, 128, (g, w, h) => {\n    const grd = g.createRadialGradient(w / 2, h / 2, 10, w / 2, h / 2, w * 0.7)\n    grd.addColorStop(0, '#fffdf6')\n    grd.addColorStop(1, '#fff6e6')\n    g.fillStyle = grd\n    g.fillRect(0, 0, w, h)\n  }, { emissive: intensity, roughness: 0.4, metalness: 0 })\n}\n\n/** Square-perforated steel (cabinet doors, ceiling diffusers). UV 1 = 0.1 m. */\nexport function perforatedMaterial(color = '#2f3337') {\n  return canvasMaterial(`perf-${color}`, 64, 64, (g, w, h) => {\n    g.clearRect(0, 0, w, h)\n    g.fillStyle = color\n    g.fillRect(0, 0, w, h)\n    for (let y = 0; y < 8; y++)\n      for (let x = 0; x < 8; x++) {\n        g.clearRect(x * 8 + 2 + (y % 2) * 4, y * 8 + 2, 4, 4)\n      }\n  }, { repeat: true, transparent: true, side: THREE.DoubleSide, metalness: 0.4, roughness: 0.5 })\n}\n\n/** Linear slot diffuser face. */\nexport function diffuserMaterial() {\n  return canvasMaterial('diffuser', 128, 32, (g, w, h) => {\n    g.fillStyle = '#f2f2f0'\n    g.fillRect(0, 0, w, h)\n    g.fillStyle = '#25282b'\n    for (let i = 0; i < 4; i++) g.fillRect(4, 4 + i * 7, w - 8, 3)\n  }, { roughness: 0.5 })\n}\n\n/** Interlocking roller-shutter slats, repeat along V. UV 1 = 0.075 m slat. */\nexport function slatMaterial(color = '#b9bec2') {\n  return canvasMaterial(`slat-${color}`, 32, 64, (g, w, h) => {\n    const grd = g.createLinearGradient(0, 0, 0, h)\n    grd.addColorStop(0, 'rgba(0,0,0,0.35)')\n    grd.addColorStop(0.15, color)\n    grd.addColorStop(0.55, '#e0e4e7')\n    grd.addColorStop(0.85, color)\n    grd.addColorStop(1, 'rgba(0,0,0,0.4)')\n    g.fillStyle = grd\n    g.fillRect(0, 0, w, h)\n  }, { repeat: true, metalness: 0.6, roughness: 0.4 })\n}\n\n/** Engraved traffolyte label (white on black, or black on white). */\nexport function labelMaterial(text: string, bg = '#1b1d1f', fg = '#f1f1ed', w = 512, h = 96) {\n  return canvasMaterial(`label-${text}-${bg}-${fg}-${w}x${h}`, w, h, (g) => {\n    g.fillStyle = bg\n    g.fillRect(0, 0, w, h)\n    g.fillStyle = fg\n    g.textAlign = 'center'\n    g.textBaseline = 'middle'\n    let size = Math.round(h * 0.6)\n    g.font = `bold ${size}px Arial, sans-serif`\n    while (g.measureText(text).width > w * 0.9 && size > 8) g.font = `bold ${(size -= 1)}px Arial, sans-serif`\n    g.fillText(text, w / 2, h / 2 + 1)\n  }, { roughness: 0.45 })\n}\n\n/** Door / window certification plate: rating, standard, serial. */\nexport function ratingLabelMaterial(rating: string) {\n  return canvasMaterial(`rating-${rating}`, 256, 128, (g, w, h) => {\n    const grd = g.createLinearGradient(0, 0, w, h)\n    grd.addColorStop(0, '#d8dcdf')\n    grd.addColorStop(1, '#b8bec2')\n    g.fillStyle = grd\n    g.fillRect(0, 0, w, h)\n    g.strokeStyle = '#555b60'\n    g.lineWidth = 4\n    g.strokeRect(3, 3, w - 6, h - 6)\n    g.fillStyle = '#b52a20'\n    g.fillRect(10, 10, 90, h - 20)\n    g.fillStyle = '#fff'\n    g.font = 'bold 34px Arial, sans-serif'\n    g.textAlign = 'center'\n    g.textBaseline = 'middle'\n    g.fillText(rating, 55, h / 2)\n    g.fillStyle = '#1b1e21'\n    g.textAlign = 'left'\n    g.font = 'bold 17px Arial, sans-serif'\n    g.fillText(rating.startsWith('H') ? 'H-CLASS DIVISION' : rating.startsWith('A') ? 'A-CLASS DIVISION' : 'FIRE DOOR', 110, 32)\n    g.font = '15px Arial, sans-serif'\n    g.fillText('IMO FTP CODE', 110, 58)\n    g.fillText('SN 000000', 110, 82)\n    g.fillText('TYPE APPROVED', 110, 104)\n  }, { metalness: 0.6, roughness: 0.35 })\n}\n\n/** ISO 7010 E001-style emergency exit sign (running man, door, arrow). */\nexport function exitSignMaterial(lit = true) {\n  return canvasMaterial(`exit-${lit}`, 512, 256, (g, w, h) => {\n    g.fillStyle = '#0f8a3d'\n    g.fillRect(0, 0, w, h)\n    g.fillStyle = '#fff'\n    // door frame\n    g.fillRect(300, 30, 12, 196)\n    g.fillRect(300, 30, 150, 12)\n    g.fillRect(438, 30, 12, 196)\n    // running figure\n    g.beginPath()\n    g.arc(240, 62, 22, 0, Math.PI * 2)\n    g.fill()\n    g.lineCap = 'round'\n    g.strokeStyle = '#fff'\n    g.lineWidth = 26\n    const ln = (a: [number, number][]) => {\n      g.beginPath()\n      a.forEach(([x, y], i) => (i ? g.lineTo(x, y) : g.moveTo(x, y)))\n      g.stroke()\n    }\n    ln([[226, 100], [196, 160]])\n    ln([[196, 160], [238, 186], [228, 230]])\n    ln([[196, 160], [160, 200], [128, 196]])\n    ln([[222, 104], [262, 130], [290, 116]])\n    ln([[222, 104], [182, 112], [164, 140]])\n    // arrow\n    g.beginPath()\n    g.moveTo(24, 128)\n    g.lineTo(78, 78)\n    g.lineTo(78, 108)\n    g.lineTo(124, 108)\n    g.lineTo(124, 148)\n    g.lineTo(78, 148)\n    g.lineTo(78, 178)\n    g.closePath()\n    g.fill()\n  }, { emissive: lit ? 0.9 : 0, roughness: 0.4 })\n}\n\n/** Room name sign: white on blue (mandatory-style) or black on white. */\nexport function roomSignMaterial(text: string) {\n  return canvasMaterial(`room-${text}`, 512, 128, (g, w, h) => {\n    g.fillStyle = '#f4f4f0'\n    g.fillRect(0, 0, w, h)\n    g.fillStyle = '#1f4e9a'\n    g.fillRect(0, 0, 18, h)\n    g.fillStyle = '#16191c'\n    g.textAlign = 'center'\n    g.textBaseline = 'middle'\n    let size = 52\n    g.font = `bold ${size}px Arial, sans-serif`\n    while (g.measureText(text).width > w - 60 && size > 12) g.font = `bold ${(size -= 2)}px Arial, sans-serif`\n    g.fillText(text, w / 2 + 9, h / 2 + 2)\n  }, { roughness: 0.4 })\n}\n\n/** Keyboard key field. */\nexport function keyboardMaterial() {\n  return canvasMaterial('keyboard', 256, 96, (g, w, h) => {\n    g.fillStyle = '#1b1c1e'\n    g.fillRect(0, 0, w, h)\n    const kw = 14\n    for (let r = 0; r < 5; r++)\n      for (let c = 0; c < 15; c++) {\n        const x = 4 + c * (kw + 2) + (r % 2) * 4\n        if (x + kw > 200) continue\n        g.fillStyle = '#2d2f32'\n        g.fillRect(x, 6 + r * 17, kw, 14)\n        g.fillStyle = 'rgba(255,255,255,0.35)'\n        g.fillRect(x + 4, 10 + r * 17, 4, 3)\n      }\n    g.fillStyle = '#2d2f32'\n    g.fillRect(60, 6 + 5 * 17 - 2, 90, 10)\n    for (let r = 0; r < 4; r++) for (let c = 0; c < 3; c++) g.fillRect(206 + c * 16, 6 + r * 20, 14, 16)\n  }, { roughness: 0.6 })\n}\n"
    },
    {
      "path": "interiors/_wall.ts",
      "type": "registry:file",
      "target": "components/deck-twin/interiors/_wall.ts",
      "content": "// Wall panel with real rectangular openings: built as boxes around each hole, lined both faces, for doors, windows and rooms.\nimport * as THREE from 'three'\nimport { mat4, uvScale, type Kit, type MatKey } from '../lib/kit'\n\n/** Hole in a wall: centre `u` along the wall, width `w`, sill `y0` and head `y1` above the floor. */\nexport interface Hole {\n  u: number\n  w: number\n  y0: number\n  y1: number\n}\n\n/**\n * Wall along X from -len/2 to len/2, thickness along Z centred on z = 0, floor y = 0 to `height`.\n * `face` is the finish on every face (UV 1 = 0.6 m, planar so the module lines up across pieces).\n */\nexport function wallWithHoles(k: Kit, len: number, height: number, t: number, holes: Hole[], face: MatKey, y0 = 0) {\n  const hs = holes\n    .map((h) => ({ a: Math.max(-len / 2, h.u - h.w / 2), b: Math.min(len / 2, h.u + h.w / 2), y0: Math.max(y0, h.y0), y1: Math.min(height, h.y1) }))\n    .filter((h) => h.b - h.a > 0.01 && h.y1 - h.y0 > 0.01)\n    .sort((p, q) => p.a - q.a)\n  const piece = (a: number, b: number, ya: number, yb: number) => {\n    if (b - a < 0.002 || yb - ya < 0.002) return\n    const g = new THREE.BoxGeometry(b - a, yb - ya, t)\n    const pos = g.getAttribute('position') as THREE.BufferAttribute\n    const nor = g.getAttribute('normal') as THREE.BufferAttribute\n    const uv = g.getAttribute('uv') as THREE.BufferAttribute\n    for (let i = 0; i < pos.count; i++) {\n      const x = pos.getX(i) + (a + b) / 2\n      const y = pos.getY(i) + (ya + yb) / 2\n      const z = pos.getZ(i)\n      const ax = Math.abs(nor.getX(i))\n      uv.setXY(i, (ax > 0.5 ? z : x) / 0.6, y / 0.6)\n    }\n    k.add(face, g, mat4([(a + b) / 2, (ya + yb) / 2, 0]))\n  }\n  let cur = -len / 2\n  for (const h of hs) {\n    if (h.a > cur) piece(cur, h.a, y0, height)\n    piece(h.a, h.b, y0, h.y0)\n    piece(h.a, h.b, h.y1, height)\n    cur = Math.max(cur, h.b)\n  }\n  piece(cur, len / 2, y0, height)\n}\n\n/** Plain box with UVs scaled so a 0.6 m module texture tiles at true size. */\nexport function tiledBox(k: Kit, mat: MatKey, size: [number, number, number], pos: [number, number, number], module = 0.6) {\n  const g = new THREE.BoxGeometry(...size)\n  uvScale(g, Math.max(size[0], size[2]) / module, Math.max(size[1], size[2]) / module)\n  k.add(mat, g, mat4(pos))\n}\n"
    }
  ],
  "categories": [
    "interiors"
  ],
  "meta": {
    "export": "Window",
    "anchor": "base",
    "standards": [
      "IMO FTP Code (A60 / H120)",
      "ISO 3903 (ship windows)",
      "EN 1363-2 (hydrocarbon curve)"
    ]
  }
}