{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gas-cylinder-quad",
  "type": "registry:component",
  "title": "Gas cylinder quad (bundle)",
  "description": "Offshore cylinder bundle (\"quad\") of 50 L seamless cylinders in a 2.2 m lifting frame. The frame has an RHS base with fork pockets, a checker-plate floor, SHS posts and top frame, angle clamp rails with rubber strips and guard bars over the valves. Each cylinder has an EN 1089-3 shoulder colour for the gas, a neck, valve and handwheel, and a pigtail to its row header. The headers feed a collector with a master valve, a gauge and an outlet in a guard. Marked with the gas name, a class 2 diamond, a DNV plate and a tag, with padeyes, shackles and a 4-leg sling. count sets the cylinders (default 16, 4 × 4).",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "lifting/gas-cylinder-quad.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/lifting/gas-cylinder-quad.tsx",
      "content": "// Offshore gas cylinder quad (cylinder bundle) after DNV 2.7-1 / ISO 10961: 50 L cylinders (EN 1089-3 shoulder colours) in a lifting frame with clamp bars, pigtails, manifold, master valve and 4-leg sling set.\nimport * as THREE from 'three'\nimport { Kit, type MatKey, type V3 } from '../lib/kit'\nimport { KitMesh, useKit, type GroupProps } from '../lib/mesh'\nimport { paint } from '../lib/materials'\nimport { checkerPlate, gauge, member, tagPlate } from '../lib/parts'\nimport { SECTIONS } from '../lib/standards'\nimport { textMaterial } from '../lib/textures'\nimport { boxMM, padeyeShackle, slingSet } from './_rigging'\n\nexport type QuadGas = 'nitrogen' | 'oxygen' | 'air' | 'helium' | 'argon' | 'co2'\n\nexport interface GasCylinderQuadProps {\n  /** number of cylinders */\n  count?: number\n  gas?: QuadGas\n  /** frame colour */\n  color?: string\n  tag?: string\n  liftingSet?: boolean\n}\n\nexport const defaults = {\n  count: 16,\n  gas: 'nitrogen' as QuadGas,\n  color: '#d9a21b',\n  tag: 'GQ-101',\n  liftingSet: true,\n}\n\n// EN 1089-3 shoulder colours (body grey for inert / industrial gases)\nconst SHOULDER: Record<QuadGas, string> = { nitrogen: '#1b1c1e', oxygen: '#f2f2ee', air: '#f2f2ee', helium: '#7a4a26', argon: '#2f6b3f', co2: '#7f8589' }\nconst LABEL: Record<QuadGas, string> = { nitrogen: 'NITROGEN', oxygen: 'OXYGEN', air: 'BREATHING AIR', helium: 'HELIUM', argon: 'ARGON', co2: 'CARBON DIOXIDE' }\n\nconst OD = 0.229 // 50 L, 200/300 bar seamless cylinder (typical)\nconst PITCH = 0.255\n\nexport function GasCylinderQuad({ count = defaults.count, gas = defaults.gas, color = defaults.color, tag = defaults.tag, liftingSet = defaults.liftingSet, ...group }: GasCylinderQuadProps & GroupProps) {\n  const data = useKit((k) => build(k, count, gas, color, tag, liftingSet), [count, gas, color, tag, liftingSet])\n  return (\n    <group {...group}>\n      <KitMesh data={data} />\n    </group>\n  )\n}\n\nexport default GasCylinderQuad\n\nfunction build(k: Kit, countIn: number, gas: QuadGas, color: string, tag: string, lifting: boolean) {\n  const n = Math.round(Math.min(Math.max(countIn, 4), 25))\n  const cols = Math.ceil(Math.sqrt(n))\n  const rows = Math.ceil(n / cols)\n  const P = paint(color, { weather: 1.1 })\n  const body = paint('#6d7276', { weather: 0.5 })\n  const shoulder = paint(SHOULDER[gas] ?? SHOULDER.nitrogen, { weather: 0.3 })\n  const sq = 'SHS 100x100x6.3'\n  const s = SECTIONS[sq].b\n  const hx = (cols * PITCH) / 2 + s + 0.14\n  const hz = (rows * PITCH) / 2 + s + 0.02\n  const yF = 0.12\n  const H = 2.2\n  const ex = hx - s / 2\n  const ez = hz - s / 2\n\n  // base with fork pockets, checker-plate floor, corner posts, top frame, cylinder clamp bars\n  for (const sz of [-1, 1]) member(k, [-hx, yF / 2, sz * (hz - 0.05)], [hx, yF / 2, sz * (hz - 0.05)], 'RHS 200x100x8', P, [0, 0, 1])\n  for (const sx of [-1, 1]) member(k, [sx * (hx - 0.05), yF / 2, -hz + 0.1], [sx * (hx - 0.05), yF / 2, hz - 0.1], 'RHS 200x100x8', P, [1, 0, 0])\n  for (const sx of [-1, 1]) {\n    const x = sx * Math.min(0.3, hx * 0.4)\n    boxMM(k, P, [x - 0.11, yF - 0.008, -hz + 0.1], [x + 0.11, yF, hz - 0.1])\n    for (const sz of [-1, 1]) boxMM(k, 'rubberBlack', [x - 0.1, 0.012, sz * (hz + 0.001) - 0.001], [x + 0.1, 0.09, sz * (hz + 0.001) + 0.001])\n  }\n  checkerPlate(k, [0, yF + 0.006, 0], 2 * hx - 0.2, 2 * hz - 0.2)\n  for (const sx of [-1, 1]) for (const sz of [-1, 1]) member(k, [sx * ex, yF, sz * ez], [sx * ex, H - s, sz * ez], sq, P, [1, 0, 0])\n  for (const sz of [-1, 1]) member(k, [-hx, H - s / 2, sz * ez], [hx, H - s / 2, sz * ez], sq, P)\n  for (const sx of [-1, 1]) member(k, [sx * ex, H - s / 2, -ez + s / 2], [sx * ex, H - s / 2, ez - s / 2], sq, P)\n  const yC = yF + 0.006\n  const yS = yC + 1.42\n  for (const y of [yC + 0.45, yC + 1.1]) {\n    for (const sz of [-1, 1]) member(k, [-ex + s / 2, y, sz * ez], [ex - s / 2, y, sz * ez], 'L75x75x8', P, [0, 1, 0])\n    for (const sx of [-1, 1]) member(k, [sx * ex, y, -ez + s / 2], [sx * ex, y, ez - s / 2], 'L75x75x8', P, [0, 1, 0])\n    // internal clamp bars between cylinder rows\n    for (let r = 1; r < rows; r++) {\n      const z = -((rows * PITCH) / 2) + r * PITCH\n      boxMM(k, P, [-ex + s / 2, y - 0.03, z - 0.006], [ex - s / 2, y + 0.03, z + 0.006])\n    }\n    for (let r = 0; r < rows; r++)\n      for (const sz of [-1, 1]) {\n        const z = -((rows * PITCH) / 2) + (r + 0.5) * PITCH + sz * (OD / 2 + 0.01)\n        if (Math.abs(z) > ez - s / 2) continue\n        boxMM(k, 'rubberBlack', [-ex + s / 2, y - 0.025, z - 0.004], [ex - s / 2, y + 0.025, z + 0.004])\n      }\n  }\n\n  // cylinders: body, shoulder colour band, neck, valve with guard handwheel, pigtail to the row header\n  const cyls: { x: number; z: number }[] = []\n  for (let i = 0; i < n; i++) {\n    const c = i % cols\n    const r = Math.floor(i / cols)\n    cyls.push({ x: -((cols * PITCH) / 2) + (c + 0.5) * PITCH, z: -((rows * PITCH) / 2) + (r + 0.5) * PITCH })\n  }\n  const yM = yS + 0.36\n  for (const { x, z } of cyls) {\n    k.lathe(body, [[0.06, 0], [OD / 2 - 0.01, 0], [OD / 2, 0.015], [OD / 2, yS - yC - 0.12]], [x, yC, z], 'y', 28)\n    k.lathe(shoulder, [[OD / 2, yS - yC - 0.12], [OD / 2 * 0.93, yS - yC - 0.04], [OD / 2 * 0.62, yS - yC + 0.02], [0.03, yS - yC + 0.045]], [x, yC, z], 'y', 28)\n    k.cyl('brass', 0.022, 0.08, [x, yS + 0.08, z], 'y', 12)\n    k.box('brass', [0.06, 0.05, 0.04], [x, yS + 0.14, z])\n    k.cyl('brass', 0.012, 0.06, [x + 0.04, yS + 0.14, z], 'x', 8)\n    k.handwheel('actuatorBlue', 0.055, [x, yS + 0.2, z], 'y', 3)\n    k.tube('tubing', [[x + 0.07, yS + 0.14, z], [x + 0.1, yS + 0.14, z], [x + 0.1, yM - 0.02, z], [x + 0.1, yM - 0.02, z + 0.04]], 0.005, 6, 0.03)\n  }\n  // row headers → collector along X at the front, master valve, gauge, outlet in a guard\n  const zc = ez - 0.09\n  for (let r = 0; r < rows; r++) {\n    const z = -((rows * PITCH) / 2) + (r + 0.5) * PITCH + 0.04\n    k.rod('stainless', [-(cols * PITCH) / 2 + 0.05, yM, z], [(cols * PITCH) / 2 + 0.08, yM, z], 0.012, 8)\n  }\n  const xo = (cols * PITCH) / 2 + 0.08\n  k.rod('stainless', [xo, yM, -((rows * PITCH) / 2) + 0.15], [xo, yM, zc], 0.016, 10)\n  k.rod('stainless', [xo, yM, zc], [xo, yM - 0.35, zc], 0.016, 10)\n  k.box('stainless', [0.07, 0.07, 0.07], [xo, yM - 0.2, zc])\n  k.rod('actuatorRed', [xo, yM - 0.2, zc + 0.035], [xo, yM - 0.2, zc + 0.11], 0.008, 6)\n  k.box('actuatorRed', [0.12, 0.02, 0.025], [xo + 0.05, yM - 0.2, zc + 0.11])\n  gauge(k, [xo - 0.09, yM - 0.05, zc + 0.02], 'z', 0.08, 'bar', 400)\n  k.rod('stainless', [xo - 0.09, yM - 0.05, zc], [xo, yM - 0.05, zc], 0.006, 6)\n  k.cyl('brass', 0.02, 0.06, [xo, yM - 0.38, zc], 'y', 12)\n  k.hex('brass', 0.05, 0.025, [xo, yM - 0.42, zc])\n  for (const y of [yM - 0.5, yM + 0.08]) boxMM(k, P, [xo - 0.17, y - 0.01, zc - 0.02], [xo + 0.1, y + 0.01, ez - s / 2])\n\n  // top guard bars over the valves\n  for (let c = 0; c <= cols; c++) {\n    const x = -((cols * PITCH) / 2) + c * PITCH\n    boxMM(k, P, [x - 0.02, H - s - 0.02, -ez + s / 2], [x + 0.02, H - s + 0.02, ez - s / 2])\n  }\n  // labels: gas name, UN 2.2 diamond, DNV plate, tag\n  k.plane(textMaterial(LABEL[gas] ?? 'GAS', '#1d6b3a', '#ffffff', 768, 112), Math.min(1.1, 2 * hx - 0.3), 0.15, [0, H - s / 2, hz + 0.002])\n  const dq = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 4)\n  k.box(P, [0.26, 0.26, 0.006], [ex, yC + 0.75, hz + 0.003])\n  k.plane(textMaterial('2', '#1d8a46', '#ffffff', 128, 128), 0.17, 0.17, [ex, yC + 0.75, hz + 0.0065], dq)\n  k.box('nameplate', [0.22, 0.14, 0.003], [-hx * 0.4, 0.06, hz + 0.002])\n  k.plane(textMaterial('DNV 2.7-1', '#e9eae6', '#222222', 256, 64), 0.2, 0.05, [-hx * 0.4, 0.09, hz + 0.0036])\n  if (tag) tagPlate(k, tag, [hx * 0.45, 0.06, hz + 0.002], 'z', 0.22)\n\n  if (lifting) {\n    const eyes: V3[] = []\n    for (const sx of [-1, 1]) for (const sz of [-1, 1]) eyes.push(padeyeShackle(k, [sx * ex, H, sz * ez], 'z', 1, P as MatKey))\n    slingSet(k, eyes, [0, H + Math.hypot(ex, ez) * 1.05, 0], 0.22, 0.011)\n  }\n}\n"
    },
    {
      "path": "lifting/_rigging.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/lifting/_rigging.tsx",
      "content": "// Shared rigging for the lifting category: wire rope, shackles, master links, 4-leg sling sets, hooks, hazard panels and oriented bars.\nimport * as THREE from 'three'\nimport { useMemo } from 'react'\nimport { Kit, mat4, uvScale, type MatKey, type V3 } from '../lib/kit'\nimport { paint } from '../lib/materials'\nimport { liftingLug } from '../lib/parts'\nimport { hazardStripeMaterial } from '../lib/textures'\n\nconst UNIT_ROPE = new THREE.CylinderGeometry(1, 1, 1, 8, 1, false)\n\n/** Galvanised wire rope (dark, oily). */\nexport const ropeMaterial = () => paint('#3a3d40', { metalness: 0.75, roughness: 0.42, clearcoat: 0, weather: 0 })\n/** Offshore crane / lifting-gear yellow (RAL 1003). */\nexport const craneYellow = () => paint('#eab308', { weather: 0.9 })\n\n/** Axis-aligned box from min to max corners. */\nexport function boxMM(k: Kit, mat: MatKey, a: V3, b: V3) {\n  k.box(mat, [Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]), Math.abs(b[2] - a[2])], [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2])\n}\n\n/** Rectangular bar from a to b, w across (perpendicular to up), h along `up`. */\nexport function bar(k: Kit, mat: MatKey, a: V3, b: V3, w: number, h: number, up: V3 = [0, 1, 0]) {\n  const A = new THREE.Vector3(...a)\n  const d = new THREE.Vector3(...b).sub(A)\n  const L = d.length()\n  if (L < 1e-6) return\n  const x = d.normalize()\n  let u = new THREE.Vector3(...up).normalize()\n  if (Math.abs(u.dot(x)) > 0.99) u = Math.abs(x.y) > 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0)\n  const z = new THREE.Vector3().crossVectors(x, u).normalize()\n  const y = new THREE.Vector3().crossVectors(z, x).normalize()\n  const m = new THREE.Matrix4().makeBasis(x, y, z).setPosition(A.addScaledVector(x, L / 2))\n  k.add(mat, new THREE.BoxGeometry(L, h, w), m)\n}\n\n/** Black/yellow hazard chevron panel of w × h, centred at pos, facing `normal`, laid 4 mm proud. */\nexport function hazardPanel(k: Kit, pos: V3, w: number, h: number, normal: V3, stripe = 0.3) {\n  const n = new THREE.Vector3(...normal).normalize()\n  const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), n)\n  const g = new THREE.PlaneGeometry(w, h)\n  uvScale(g, w / (stripe * 2), h / (stripe * 2))\n  const p = new THREE.Vector3(...pos).addScaledVector(n, 0.004)\n  k.add(hazardStripeMaterial(), g, mat4(p.toArray() as V3, q))\n}\n\n/** Bow shackle: bow and pin in the plane of `along` and +Y, pin centre at pos (pairs with a padeye plate across `along`). s = jaw width. */\nexport function shackle(k: Kit, pos: V3, s = 0.06, along: 'x' | 'z' = 'x', mat: MatKey = 'galvanised') {\n  const r = s * 0.22\n  const q = along === 'x' ? new THREE.Quaternion() : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2)\n  const m = (p: V3) => new THREE.Matrix4().compose(new THREE.Vector3(...p).applyQuaternion(q).add(new THREE.Vector3(...pos)), q, new THREE.Vector3(1, 1, 1))\n  const bow = new THREE.TorusGeometry(s / 2, r, 6, 16, Math.PI)\n  k.add(mat, bow, m([0, s * 0.9, 0]))\n  for (const sx of [-1, 1]) k.add(mat, new THREE.CylinderGeometry(r, r, s * 0.9, 6), m([sx * (s / 2), s * 0.45, 0]))\n  k.add('nut', new THREE.CylinderGeometry(r * 0.95, r * 0.95, s + r * 6, 8).rotateZ(Math.PI / 2), m([0, 0, 0]))\n  for (const sx of [-1, 1]) k.add(mat, new THREE.SphereGeometry(r * 1.25, 8, 6), m([sx * (s / 2), 0, 0]))\n}\n\n/** Top of a shackle's bow (where a sling eye sits). */\nexport const shackleTop = (pos: V3, s = 0.06): V3 => [pos[0], pos[1] + s * 1.4, pos[2]]\n\n/** Oblong master link, long axis vertical, centre at pos. */\nexport function masterLink(k: Kit, pos: V3, s = 0.25, mat: MatKey = 'galvanised') {\n  const g = new THREE.TorusGeometry(s * 0.32, s * 0.08, 8, 24)\n  g.scale(1, 1.55, 1)\n  k.add(mat, g, mat4(pos))\n}\n\n/** Four-leg sling set: legs from each attachment point to a master link at `apex`, with thimble eyes and ferrules. */\nexport function slingSet(k: Kit, eyes: V3[], apex: V3, s = 0.25, rope = 0.012) {\n  const rm = ropeMaterial()\n  const link = s * 0.32 * 1.55\n  const bottom: V3 = [apex[0], apex[1] - link + s * 0.04, apex[2]]\n  masterLink(k, apex, s)\n  // intermediate links (one per pair of legs)\n  for (const sx of [-1, 1]) {\n    const g = new THREE.TorusGeometry(s * 0.16, s * 0.05, 6, 18)\n    g.scale(1, 1.4, 1)\n    k.add('galvanised', g, mat4([bottom[0] + sx * s * 0.12, bottom[1] - s * 0.12, bottom[2]], new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2)))\n  }\n  const hang: V3 = [bottom[0], bottom[1] - s * 0.3, bottom[2]]\n  for (const e of eyes) {\n    const top: V3 = [hang[0] + Math.sign(e[0] - hang[0]) * s * 0.12, hang[1], hang[2] + Math.sign(e[2] - hang[2]) * s * 0.02]\n    const E = new THREE.Vector3(...e)\n    const T = new THREE.Vector3(...top)\n    const dir = T.clone().sub(E).normalize()\n    // thimble eye at the shackle, ferrule a little up the leg\n    const eyeC = E.clone().addScaledVector(dir, 0.06)\n    k.rod(rm, eyeC.toArray() as V3, T.toArray() as V3, rope, 6)\n    k.rod('aluminium', eyeC.clone().addScaledVector(dir, 0.12).toArray() as V3, eyeC.clone().addScaledVector(dir, 0.22).toArray() as V3, rope * 1.9, 8)\n    k.torus('galvanised', 0.035, rope * 0.9, eyeC.toArray() as V3, new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), new THREE.Vector3(-dir.z, 0, dir.x).normalize()), 6, 14)\n    k.rod(rm, T.toArray() as V3, [top[0], top[1] + 0.04, top[2]], rope, 6)\n  }\n}\n\n/** Forged single hook (DIN 15401 shape): shank up from pos, bowl below, latch. size = bowl inner radius. */\nexport function hook(k: Kit, pos: V3, size = 0.12, mat: MatKey = 'castIron', latch: MatKey = 'safetyYellow') {\n  const [x, y, z] = pos\n  const r = size\n  const t = size * 0.42\n  k.cyl(mat, t * 0.55, r * 1.3, [x, y + r * 0.65, z], 'y', 12)\n  k.hex('nut', t * 1.6, t * 0.6, [x, y + r * 1.35, z])\n  // bowl: 300° arc of a tapered section, open towards +X\n  const arc = new THREE.TorusGeometry(r, t * 0.6, 10, 24, (5 * Math.PI) / 3)\n  arc.rotateZ(Math.PI / 2 + Math.PI / 6)\n  k.add(mat, arc, mat4([x, y - r * 1.0, z], undefined, [1, 1.05, 1.3]))\n  k.sphere(mat, t * 0.55, [x + r * Math.cos(Math.PI / 6 + Math.PI / 2 + (5 * Math.PI) / 3), y - r + r * Math.sin(Math.PI / 6 + Math.PI / 2 + (5 * Math.PI) / 3), z], undefined, 10)\n  k.rod(latch, [x + t * 0.5, y + r * 0.1, z], [x + r * 0.95, y - r * 0.45, z], t * 0.12, 6)\n}\n\n/** Stretched-rope mesh between two points (moves with props without rebuilding geometry). */\nexport function Rope({ a, b, r = 0.022, material }: { a: V3; b: V3; r?: number; material?: THREE.Material }) {\n  const t = useMemo(() => {\n    const A = new THREE.Vector3(...a)\n    const d = new THREE.Vector3(...b).sub(A)\n    const L = Math.max(d.length(), 1e-4)\n    const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), d.normalize())\n    return { p: A.addScaledVector(d, L / 2), q, s: new THREE.Vector3(r, L, r) }\n  }, [a[0], a[1], a[2], b[0], b[1], b[2], r]) // eslint-disable-line react-hooks/exhaustive-deps\n  const m = material ?? ropeMaterial()\n  return <mesh geometry={UNIT_ROPE} material={m} position={t.p} quaternion={t.q} scale={t.s} castShadow />\n}\n\n/** Padeye plate (across `across`) with a bow shackle through its hole; returns the bow top for the sling eye. */\nexport function padeyeShackle(k: Kit, pos: V3, across: 'x' | 'z' = 'x', s = 1, mat: MatKey = 'structSteel'): V3 {\n  liftingLug(k, pos, across === 'x' ? 'z' : 'x', s, mat)\n  const pin: V3 = [pos[0], pos[1] + 0.12 * s * 0.65, pos[2]]\n  shackle(k, pin, 0.065 * s, across)\n  return shackleTop(pin, 0.065 * s)\n}\n"
    }
  ],
  "categories": [
    "lifting"
  ],
  "meta": {
    "export": "GasCylinderQuad",
    "anchor": "base",
    "standards": [
      "DNV 2.7-1",
      "EN 12079-1",
      "ISO 10961",
      "EN 1089-3",
      "IMCA D 043"
    ]
  }
}