import React, { useMemo, useState } from "react"; // GenieClip RST Load Calculator (pure React / JS) // - Computes recommended furring-channel spacing (OC) and clip spacing (OC) // - Treats clouds either as distributed average psf or as dedicated clips (4/each) // - Clip capacity fixed at 36 lb per GenieClip RST // - No TypeScript; ASCII-only strings to avoid parser quirks // ----------------------------- // Pure calculation helpers // ----------------------------- function calcBaseAssemblyPsf(opts) { const osb = opts.includeOSB ? opts.osbPsf : 0; const gyp = opts.drywallLayers * opts.drywallPsf; return osb + gyp + opts.insulPsf; // excludes misc; that's handled separately as another distributed psf } function calcCloudAvgPsf(mountMode, areaFt2, totalCloudWeightLb) { if (mountMode === "distributed" && areaFt2 > 0) return totalCloudWeightLb / areaFt2; return 0; } function calcCombos(params) { const { gridPsf, allowedChannelSpacings, allowedClipSpacings, constrainToStructure, structureSpacing, clipCap } = params; const channels = Array.from(new Set(allowedChannelSpacings)).sort((a, b) => a - b); const clips = constrainToStructure ? [structureSpacing] : Array.from(new Set(allowedClipSpacings)).sort((a, b) => a - b); const out = []; for (let i = 0; i < channels.length; i++) { const ch = channels[i]; for (let j = 0; j < clips.length; j++) { const cl = clips[j]; const tribAreaFt2 = (ch * cl) / 144.0; // in^2 -> ft^2 const loadPerClip = tribAreaFt2 * gridPsf; const pass = isFinite(loadPerClip) && loadPerClip <= clipCap; const safety = isFinite(loadPerClip) && loadPerClip > 0 ? (clipCap / loadPerClip) : Infinity; out.push({ channelOC: ch, clipOC: cl, tribAreaFt2, loadPerClip, pass, safety }); } } // sort by tributary product (wider first) out.sort((a, b) => (b.channelOC * b.clipOC) - (a.channelOC * a.clipOC)); return out; } function firstPassing(combos) { for (let k = 0; k < combos.length; k++) if (combos[k].pass) return combos[k]; return null; } function round2(x) { return Math.round(x * 100) / 100; } // ----------------------------- // Component // ----------------------------- const App = () => { // Assembly const [area, setArea] = useState(400); // ft^2 const [includeOSB, setIncludeOSB] = useState(true); const [osbPsf, setOsbPsf] = useState(2.7); const [drywallLayers, setDrywallLayers] = useState(2); // 0..3 const [drywallPsf, setDrywallPsf] = useState(2.5); const [insulPsf, setInsulPsf] = useState(0.2); const [miscPsf, setMiscPsf] = useState(0); // NEW: miscellaneous distributed psf (lights, speakers, etc.) // Clouds const [mountMode, setMountMode] = useState("distributed"); // distributed | dedicated const [c4x1, setC4x1] = useState(0); // 15 lb const [c4x2, setC4x2] = useState(0); // 30 lb const [c4x3, setC4x3] = useState(0); // 45 lb const [c4x4, setC4x4] = useState(0); // 60 lb // Spacing constraints const [allowedChannelSpacings, setAllowedChannelSpacings] = useState([12, 16, 24]); const [allowedClipSpacings, setAllowedClipSpacings] = useState([24, 32, 40, 48]); const [constrainToStructure, setConstrainToStructure] = useState(false); const [structureSpacing, setStructureSpacing] = useState(48); const CLIP_CAP = 36; const totalCloudWeight = (c4x1 * 15) + (c4x2 * 30) + (c4x3 * 45) + (c4x4 * 60); const baseAssemblyPsf = useMemo(() => calcBaseAssemblyPsf({ includeOSB, osbPsf, drywallLayers, drywallPsf, insulPsf }), [includeOSB, osbPsf, drywallLayers, drywallPsf, insulPsf]); const cloudAvgPsf = useMemo(() => calcCloudAvgPsf(mountMode, area, totalCloudWeight), [mountMode, area, totalCloudWeight]); const gridPsf = useMemo(() => baseAssemblyPsf + cloudAvgPsf + miscPsf, [baseAssemblyPsf, cloudAvgPsf, miscPsf]); const maxAreaPerClip = useMemo(() => (gridPsf > 0 ? (CLIP_CAP / gridPsf) : Infinity), [gridPsf]); const maxSpacingProduct = useMemo(() => maxAreaPerClip * 144.0, [maxAreaPerClip]); const combos = useMemo(() => calcCombos({ gridPsf, allowedChannelSpacings, allowedClipSpacings, constrainToStructure, structureSpacing, clipCap: CLIP_CAP }), [gridPsf, allowedChannelSpacings, allowedClipSpacings, constrainToStructure, structureSpacing]); const rec = useMemo(() => firstPassing(combos), [combos]); const estimatedClipsOnGrid = useMemo(() => { if (!rec || area <= 0) return 0; const tribFt2 = rec.tribAreaFt2; return Math.ceil(area / Math.max(tribFt2, 1e-6)); }, [rec, area]); const dedicatedCloudClips = useMemo(() => { if (mountMode !== "dedicated") return 0; const totalClouds = c4x1 + c4x2 + c4x3 + c4x4; return totalClouds * 4; }, [mountMode, c4x1, c4x2, c4x3, c4x4]); const totalClips = useMemo(() => estimatedClipsOnGrid + dedicatedCloudClips, [estimatedClipsOnGrid, dedicatedCloudClips]); // Dedicated check rows (per-clip loads) const dedicatedRows = useMemo(() => { if (mountMode !== "dedicated") return []; const items = [ { name: "4x1 (15 lb)", load: 15 / 4 }, { name: "4x2 (30 lb)", load: 30 / 4 }, { name: "4x3 (45 lb)", load: 45 / 4 }, { name: "4x4 (60 lb)", load: 60 / 4 } ]; return items.map(x => ({ name: x.name, load: x.load, pass: x.load <= CLIP_CAP, safety: x.load > 0 ? (CLIP_CAP / x.load) : Infinity })); }, [mountMode]); // UI helpers const Pill = ({ children, tone }) => { const styles = { success: "bg-green-100 text-green-700", danger: "bg-rose-100 text-rose-700", neutral: "bg-gray-100 text-gray-800" }; const cls = styles[tone || "neutral"]; return {children}; }; const NumberField = ({ label, value, setValue, min, step, suffix, title }) => ( ); const Toggle = ({ label, checked, onChange }) => ( ); // ----------------------------- // Render // ----------------------------- return (

GenieClip RST Load Calculator

Compute recommended channel and clip spacing from uniform loads (36 lb/clip limit).

Assembly

{includeOSB ? ( ) : null}
Base assembly load{round2(baseAssemblyPsf)} psf
Misc distributed load{round2(miscPsf)} psf

Clouds

Total cloud weight
{round2(totalCloudWeight)} lb
Total grid load
{round2(gridPsf)} psf
{mountMode === "dedicated" && dedicatedRows.length > 0 ? (

Dedicated cloud clip check (per clip)

{dedicatedRows.map((r, i) => ( ))}
TypeLoad/clipStatus
{r.name} {round2(r.load)} lb {r.pass ? PASS x{round2(r.safety)} : FAIL}
Assumes 4 clips per cloud.
) : null}

Spacing options

Calculator picks the widest spacing that still passes.
Channel spacing (in)
{[12,16,24].map((v) => ( ))}
Clip spacing (in)
{[24,32,40,48].map((v) => ( ))}
Max spacing product
{isFinite(maxSpacingProduct) ? Math.round(maxSpacingProduct) : "-"} in^2
Max tributary area/clip
{isFinite(maxAreaPerClip) ? round2(maxAreaPerClip) : "-"} ft^2

Recommendation

{rec ? (
Recommended spacing
Channels: {rec.channelOC}" OC ยท Clips: {rec.clipOC}" OC
Load/clip: {round2(rec.loadPerClip)} lb
Safety factor: x{round2(rec.safety)}
Estimated clips on grid{estimatedClipsOnGrid}
{mountMode === "dedicated" ? (
+ Cloud clips{dedicatedCloudClips}
) : null}
Total estimated clips{totalClips}
Grid load{round2(gridPsf)} psf
Clip capacity{CLIP_CAP} lb
) : (
No passing spacing combination with current constraints.
)}

All evaluated combos

{combos.map((c, i) => ( ))}
Channels (OC)Clips (OC)Trib. areaLoad/clipStatus
{c.channelOC}" {c.clipOC}" {round2(c.tribAreaFt2)} ft^2 {isFinite(c.loadPerClip) ? round2(c.loadPerClip) : "-"} lb {c.pass ? PASS x{round2(c.safety)} : FAIL}
Sorted from widest to densest; the first PASS is recommended.
{/* Self-tests panel to validate core math (in lieu of a test runner) */}

Built-in tests

Assumptions: uniform grid loads; capacity 36 lb/clip. Always verify with manufacturer data and structure.
); }; // ----------------------------- // Tiny test harness (renders results) // ----------------------------- function TestPanel() { // Share calc logic here const run = (cfg) => { const base = calcBaseAssemblyPsf(cfg); const cloud = calcCloudAvgPsf(cfg.mountMode, cfg.area, cfg.totalCloudWeight || 0); const grid = base + cloud + (cfg.miscPsf || 0); const combos = calcCombos({ gridPsf: grid, allowedChannelSpacings: cfg.allowedChannelSpacings, allowedClipSpacings: cfg.allowedClipSpacings, constrainToStructure: cfg.constrainToStructure || false, structureSpacing: cfg.structureSpacing || 48, clipCap: 36 }); return { base, cloud, grid, combos, rec: firstPassing(combos) }; }; // Test cases const tests = [ { name: "No clouds, OSB + 2x gyp; only (16,48) and (24,48) allowed -> no pass", cfg: { includeOSB: true, osbPsf: 2.7, drywallLayers: 2, drywallPsf: 2.5, insulPsf: 0.2, mountMode: "distributed", area: 400, totalCloudWeight: 0, allowedChannelSpacings: [16,24], allowedClipSpacings: [48] }, expect: (r) => r.rec === null }, { name: "Same assembly; allow (16,24) -> recommend 16/24 (pass)", cfg: { includeOSB: true, osbPsf: 2.7, drywallLayers: 2, drywallPsf: 2.5, insulPsf: 0.2, mountMode: "distributed", area: 400, totalCloudWeight: 0, allowedChannelSpacings: [16], allowedClipSpacings: [24] }, expect: (r) => r.rec && r.rec.channelOC === 16 && r.rec.clipOC === 24 }, { name: "Add distributed clouds (4x 60 lb), wide menu -> 24/24 should pass", cfg: { includeOSB: true, osbPsf: 2.7, drywallLayers: 2, drywallPsf: 2.5, insulPsf: 0.2, mountMode: "distributed", area: 400, totalCloudWeight: 240, allowedChannelSpacings: [12,16,24], allowedClipSpacings: [24,32,40,48] }, expect: (r) => r.rec && r.rec.channelOC === 24 && r.rec.clipOC === 24 }, { name: "Same as above but +1.0 psf misc makes 24/24 fail; expect 16/32", cfg: { includeOSB: true, osbPsf: 2.7, drywallLayers: 2, drywallPsf: 2.5, insulPsf: 0.2, miscPsf: 1.0, mountMode: "distributed", area: 400, totalCloudWeight: 240, allowedChannelSpacings: [12,16,24], allowedClipSpacings: [24,32,40,48] }, expect: (r) => r.rec && r.rec.channelOC === 16 && r.rec.clipOC === 32 } ]; const results = tests.map((t) => { const r = run(t.cfg); const ok = !!t.expect(r); return { name: t.name, ok, details: r.rec ? ("rec="+r.rec.channelOC+"/"+r.rec.clipOC+" load="+round2(r.rec.loadPerClip)) : "rec=null" }; }); return (
    {results.map((x, i) => (
  • {x.ok ? "PASS" : "FAIL"} - {x.name} ({x.details})
  • ))}
); } export default App;