import type { ConnectorDef, PartDefinition, PartType } from '../types'; /** * Part catalog — the single source of truth for every placeable part: * default parameters, inspector metadata, and connector layouts. * * To add a new part type: * 1. Add its name to `PartType` in types.ts. * 2. Add an entry here (defaults, params, connectors). * 3. Add a mesh case in components/PartMesh.tsx. * 4. (Optional) tune its resistance in simulation/flowSimulator.ts. */ /** Bend radius used by elbows (ft). */ export const ELBOW_BEND_RADIUS = 0.5; /** Supported nominal pipe sizes (in) used across plumbing parts. */ export const STANDARD_PIPE_SIZES = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4] as const; export const DEFAULT_PIPE_SIZE = 1; /** Visual radius (ft) for a pipe of `d` inches diameter. */ export const pipeRadius = (d: number) => 0.05 + d * 0.055; const deg = (d: number) => (d * Math.PI) / 180; /** Connector layout for an elbow with a configurable sweep angle. */ function elbowConnectors(params: Record): ConnectorDef[] { const B = ELBOW_BEND_RADIUS; // Arc starts at local origin pointing -X and sweeps toward +Y. const t = deg(params.angle ?? 90) - Math.PI / 2; return [ { id: 'a', pos: [0, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [B * Math.cos(t), B * Math.sin(t) + B, 0], dir: [-Math.sin(t), Math.cos(t), 0], }, ]; } export function snapPipeSize(value: number): number { let best: number = STANDARD_PIPE_SIZES[0]; let bestDist = Math.abs(value - best); for (const size of STANDARD_PIPE_SIZES) { const dist = Math.abs(value - size); if (dist < bestDist) { best = size; bestDist = dist; } } return best; } export const PART_DEFS: Record = { pipe: { type: 'pipe', label: 'Pipe', category: 'Plumbing', description: 'Straight pipe segment', color: '#b8bfc9', defaults: { length: 2, diameter: DEFAULT_PIPE_SIZE }, params: { length: { label: 'Length', min: 0.5, max: 12, step: 0.5, unit: 'ft' }, diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: (p) => [ { id: 'a', pos: [-p.length / 2, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [p.length / 2, 0, 0], dir: [1, 0, 0] }, ], }, coupling: { type: 'coupling', label: 'Coupling', category: 'Plumbing', description: 'Straight fitting for joining two pipe ends', color: '#aeb7c2', defaults: { diameter: DEFAULT_PIPE_SIZE }, params: { diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [-0.3, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.3, 0, 0], dir: [1, 0, 0] }, ], }, reducer: { type: 'reducer', label: 'Reducer', category: 'Plumbing', description: 'Inline reducer fitting between two pipe diameters', color: '#a8b2bf', defaults: { diameterA: 1.5, diameterB: DEFAULT_PIPE_SIZE }, params: { diameterA: { label: 'Diameter A', min: 0.5, max: 4, step: 0.25, unit: 'in' }, diameterB: { label: 'Diameter B', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [-0.35, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.35, 0, 0], dir: [1, 0, 0] }, ], }, cap: { type: 'cap', label: 'Cap', category: 'Plumbing', description: 'End cap that terminates a pipe run', color: '#9aa3b0', defaults: { diameter: DEFAULT_PIPE_SIZE }, params: { diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [{ id: 'in', pos: [-0.22, 0, 0], dir: [-1, 0, 0] }], }, elbow: { type: 'elbow', label: 'Elbow', category: 'Plumbing', description: 'Bend fitting (15–180°)', color: '#9aa3b0', defaults: { angle: 90, diameter: DEFAULT_PIPE_SIZE }, params: { angle: { label: 'Bend angle', min: 15, max: 180, step: 15, unit: '°' }, diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: elbowConnectors, }, tee: { type: 'tee', label: 'T-Joint', category: 'Plumbing', description: '3-way splitter fitting', color: '#9aa3b0', defaults: { diameter: DEFAULT_PIPE_SIZE }, params: { diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [-0.6, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.6, 0, 0], dir: [1, 0, 0] }, { id: 'c', pos: [0, 0, 0.6], dir: [0, 0, 1] }, ], }, valve: { type: 'valve', label: 'Valve', category: 'Flow Control', description: 'Inline ball valve (0–100% open)', color: '#e0564f', defaults: { diameter: DEFAULT_PIPE_SIZE, open: 100 }, params: { open: { label: 'Open', min: 0, max: 100, step: 5, unit: '%' }, diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [-0.35, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.35, 0, 0], dir: [1, 0, 0] }, ], }, pump: { type: 'pump', label: 'Pump', category: 'Flow Control', description: 'Submersible/inline pump — pressure source', color: '#3d7bfa', // `on` (0|1) is intentionally NOT in params meta — the inspector renders a // custom power toggle for it. The simulator treats on === 0 as "switched off". defaults: { gph: 400, maxHeadFt: 8, on: 1 }, params: { gph: { label: 'Rated flow', min: 50, max: 2000, step: 25, unit: 'GPH' }, maxHeadFt: { label: 'Max head', min: 2, max: 30, step: 1, unit: 'ft' }, }, getConnectors: () => [ { id: 'in', pos: [-0.45, 0.25, 0], dir: [-1, 0, 0] }, { id: 'out', pos: [0.45, 0.25, 0], dir: [1, 0, 0] }, ], }, reservoir: { type: 'reservoir', label: 'Reservoir', category: 'Flow Control', description: 'Water tank — source & return sink', color: '#2a3950', defaults: { width: 2, depth: 1.4, height: 1, level: 70, ph: 6.0, ec: 1.2 }, params: { width: { label: 'Width', min: 1, max: 6, step: 0.5, unit: 'ft' }, depth: { label: 'Depth', min: 1, max: 6, step: 0.5, unit: 'ft' }, height: { label: 'Height', min: 0.5, max: 4, step: 0.25, unit: 'ft' }, level: { label: 'Fill level', min: 0, max: 100, step: 5, unit: '%' }, ph: { label: 'Water pH', min: 4.0, max: 8.5, step: 0.1, unit: '' }, ec: { label: 'Water EC', min: 0.0, max: 3.0, step: 0.1, unit: 'mS/cm' }, }, getConnectors: (p) => [ { id: 'a', pos: [-p.width / 2, 0.25, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [p.width / 2, 0.25, 0], dir: [1, 0, 0] }, { id: 'c', pos: [0, 0.25, -p.depth / 2], dir: [0, 0, -1] }, { id: 'd', pos: [0, 0.25, p.depth / 2], dir: [0, 0, 1] }, ], }, tower: { type: 'tower', label: 'Grow Tower', category: 'Growing', description: 'Vertical NFT tower with plant sites', color: '#e8eaed', defaults: { height: 4, sites: 6 }, params: { height: { label: 'Height', min: 2, max: 10, step: 0.5, unit: 'ft' }, sites: { label: 'Plant sites', min: 2, max: 16, step: 1 }, }, getConnectors: (p) => [ { id: 'in', pos: [0, p.height, 0], dir: [0, 1, 0] }, { id: 'out', pos: [0.45, 0.15, 0], dir: [1, 0, 0] }, ], }, tray: { type: 'tray', label: 'Grow Tray', category: 'Growing', description: 'Flood/NFT channel tray', color: '#d7dce2', defaults: { length: 3, width: 1.2 }, params: { length: { label: 'Length', min: 1, max: 8, step: 0.5, unit: 'ft' }, width: { label: 'Width', min: 0.5, max: 4, step: 0.25, unit: 'ft' }, }, getConnectors: (p) => [ { id: 'in', pos: [-p.length / 2, 0.15, 0], dir: [-1, 0, 0] }, { id: 'out', pos: [p.length / 2, 0.15, 0], dir: [1, 0, 0] }, ], }, wallPanel: { type: 'wallPanel', label: 'Wall Panel', category: 'Growing', description: 'Vertical grow wall with pockets', color: '#3b7a57', defaults: { width: 3, height: 4, rows: 4, cols: 5 }, params: { width: { label: 'Width', min: 1, max: 8, step: 0.5, unit: 'ft' }, height: { label: 'Height', min: 1, max: 8, step: 0.5, unit: 'ft' }, rows: { label: 'Pocket rows', min: 1, max: 8, step: 1 }, cols: { label: 'Pocket cols', min: 1, max: 10, step: 1 }, }, getConnectors: (p) => [ { id: 'in', pos: [0, p.height, 0], dir: [0, 1, 0] }, { id: 'out', pos: [0, 0.05, 0], dir: [0, -1, 0] }, ], }, lattice: { type: 'lattice', label: 'Lattice', category: 'Structure', description: 'Structural lattice frame (no flow)', color: '#8a7a5c', defaults: { width: 2.5, height: 3 }, params: { width: { label: 'Width', min: 1, max: 8, step: 0.5, unit: 'ft' }, height: { label: 'Height', min: 1, max: 8, step: 0.5, unit: 'ft' }, }, getConnectors: () => [], }, netPot: { type: 'netPot', label: 'Net Pot', category: 'Growing', description: 'Plant cup with seedling (decorative)', color: '#222a33', defaults: { size: 1 }, params: { size: { label: 'Size', min: 0.5, max: 2, step: 0.25 }, }, getConnectors: () => [], }, emitter: { type: 'emitter', label: 'Emitter', category: 'Flow Control', description: 'Drip/spray outlet — terminates a line', color: '#46c0e8', defaults: {}, params: {}, getConnectors: () => [{ id: 'in', pos: [0.2, 0, 0], dir: [1, 0, 0] }], }, drain: { type: 'drain', label: 'Drain', category: 'Flow Control', description: 'Floor drain — limited-capacity exit point', color: '#64748b', defaults: { capacityGph: 250 }, params: { capacityGph: { label: 'Capacity', min: 50, max: 2000, step: 25, unit: 'GPH' }, }, getConnectors: () => [{ id: 'in', pos: [-0.35, 0.25, 0], dir: [-1, 0, 0] }], }, growTent: { type: 'growTent', label: 'Grow Tent', category: 'Environment', description: 'Room/tent enclosure for planning air exchange and equipment', color: '#475569', defaults: { width: 7, depth: 5, height: 4, sealed: 0, ambientTempF: 75, humidity: 60, ambientCo2: 420, leakageAch: 0.15 }, params: { width: { label: 'Width', min: 2, max: 20, step: 0.5, unit: 'ft' }, depth: { label: 'Depth', min: 2, max: 20, step: 0.5, unit: 'ft' }, height: { label: 'Height', min: 2, max: 12, step: 0.5, unit: 'ft' }, sealed: { label: 'Sealed room', min: 0, max: 1, step: 1 }, ambientTempF: { label: 'Ambient Temp', min: 45, max: 100, step: 1, unit: '°F' }, humidity: { label: 'Ambient RH', min: 20, max: 90, step: 1, unit: '%' }, ambientCo2: { label: 'Ambient CO₂', min: 250, max: 1200, step: 25, unit: 'PPM' }, leakageAch: { label: 'Leakage ACH', min: 0, max: 3, step: 0.05, unit: 'ACH' }, }, getConnectors: () => [], }, inlineFan: { type: 'inlineFan', label: 'Inline Fan', category: 'Environment', description: 'Duct fan for room exhaust/intake calculations', color: '#0f766e', defaults: { cfm: 600, diameter: 6, exhaust: 1 }, params: { cfm: { label: 'Airflow', min: 50, max: 4000, step: 25, unit: 'CFM' }, diameter: { label: 'Duct size', min: 4, max: 14, step: 2, unit: 'in' }, exhaust: { label: 'Exhaust mode', min: 0, max: 1, step: 1 }, }, getConnectors: () => [], }, circulationFan: { type: 'circulationFan', label: 'Circulation Fan', category: 'Environment', description: 'Internal airflow fan for canopy mixing', color: '#2563eb', defaults: { cfm: 250, sweep: 90 }, params: { cfm: { label: 'Airflow', min: 50, max: 1500, step: 25, unit: 'CFM' }, sweep: { label: 'Sweep', min: 0, max: 180, step: 15, unit: '°' }, }, getConnectors: () => [], }, growLight: { type: 'growLight', label: 'Grow Light', category: 'Environment', description: 'Overhead grow fixture for room planning', color: '#fbbf24', defaults: { width: 2, depth: 2, watts: 650, hangingHeight: 1.5, photoperiod: 18 }, params: { width: { label: 'Width', min: 0.5, max: 6, step: 0.25, unit: 'ft' }, depth: { label: 'Depth', min: 0.5, max: 6, step: 0.25, unit: 'ft' }, watts: { label: 'Power', min: 50, max: 1500, step: 25, unit: 'W' }, hangingHeight: { label: 'Hang height', min: 0.25, max: 6, step: 0.25, unit: 'ft' }, photoperiod: { label: 'Hours On', min: 0, max: 24, step: 1, unit: 'h' }, }, getConnectors: () => [], }, co2Tank: { type: 'co2Tank', label: 'CO₂ Tank', category: 'Environment', description: 'CO₂ cylinder for sealed-room enrichment planning', color: '#22c55e', defaults: { pounds: 20, regulatorCfh: 20 }, params: { pounds: { label: 'Tank size', min: 5, max: 100, step: 5, unit: 'lb' }, regulatorCfh: { label: 'Release rate', min: 1, max: 100, step: 1, unit: 'CFH' }, }, getConnectors: () => [], }, airCompressor: { type: 'airCompressor', label: 'Air Compressor', category: 'Flow Control', description: 'Aerate reservoir water with an attached airstone', color: '#0ea5e9', defaults: { gph: 120, frequency: 5, on: 1 }, params: { gph: { label: 'Air Flow', min: 20, max: 400, step: 10, unit: 'GPH' }, frequency: { label: 'Bubble Freq', min: 1, max: 10, step: 1, unit: 'Hz' }, }, getConnectors: () => [], }, reducingTee: { type: 'reducingTee', label: 'Reducing Tee', category: 'Plumbing', description: 'Tee branch with a reduced secondary outlet size', color: '#8b9bb4', defaults: { diameterA: 1.5, diameterB: 1.0 }, params: { diameterA: { label: 'Main Run Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, diameterB: { label: 'Branch Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [-0.6, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.6, 0, 0], dir: [1, 0, 0] }, { id: 'c', pos: [0, 0, 0.6], dir: [0, 0, 1] }, ], }, reducingElbow: { type: 'reducingElbow', label: 'Reducing Elbow', category: 'Plumbing', description: '90-degree elbow transitioning between two diameters', color: '#9aa3b0', defaults: { diameterA: 1.5, diameterB: 1.0 }, params: { diameterA: { label: 'Diameter A', min: 0.5, max: 4, step: 0.25, unit: 'in' }, diameterB: { label: 'Diameter B', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [0, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.18, 0.18, 0], dir: [0, 1, 0] }, ], }, bulkhead: { type: 'bulkhead', label: 'Bulkhead Fitting', category: 'Plumbing', description: 'Tank wall pass-through connector', color: '#27272a', defaults: { diameter: 1.0 }, params: { diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [-0.2, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.2, 0, 0], dir: [1, 0, 0] }, ], }, waterChiller: { type: 'waterChiller', label: 'Water Chiller', category: 'Flow Control', description: 'Thermoelectric water chiller to keep reservoirs cool', color: '#0891b2', defaults: { targetTemp: 68, on: 1, diameter: 1.0 }, params: { targetTemp: { label: 'Target Temp', min: 45, max: 80, step: 1, unit: '°F' }, diameter: { label: 'Port Size', min: 0.5, max: 2.0, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'in', pos: [-0.2, 1.05, 0], dir: [0, 1, 0] }, { id: 'out', pos: [0.2, 1.05, 0], dir: [0, 1, 0] }, ], }, reverseOsmosis: { type: 'reverseOsmosis', label: 'RO Filter', category: 'Plumbing', description: 'Reverse osmosis filter to purify source water', color: '#3b82f6', defaults: { diameter: 1.0 }, params: { diameter: { label: 'Diameter', min: 0.5, max: 4, step: 0.25, unit: 'in' }, }, getConnectors: () => [ { id: 'a', pos: [-0.3, 0, 0], dir: [-1, 0, 0] }, { id: 'b', pos: [0.3, 0, 0], dir: [1, 0, 0] }, ], }, }; export const CATEGORIES: { name: string; types: PartType[] }[] = [ { name: 'Plumbing', types: ['pipe', 'coupling', 'reducer', 'cap', 'elbow', 'tee', 'reducingTee', 'reducingElbow', 'bulkhead', 'reverseOsmosis'] }, { name: 'Flow Control', types: ['pump', 'reservoir', 'valve', 'emitter', 'drain', 'airCompressor', 'waterChiller'] }, { name: 'Growing', types: ['tower', 'tray', 'wallPanel', 'netPot'] }, { name: 'Structure', types: ['lattice'] }, { name: 'Environment', types: ['growTent', 'inlineFan', 'circulationFan', 'growLight', 'co2Tank'] }, ]; /** Parts that carry water (participate in the flow graph). */ export const FLOW_PARTS = new Set([ 'pipe', 'coupling', 'reducer', 'cap', 'elbow', 'tee', 'valve', 'pump', 'reservoir', 'tower', 'tray', 'wallPanel', 'emitter', 'drain', 'reducingTee', 'reducingElbow', 'bulkhead', 'waterChiller', 'reverseOsmosis', ]); /** Plumbing parts whose unconnected ends count as leaks. */ export const OPEN_END_WARN_PARTS = new Set([ 'pipe', 'coupling', 'reducer', 'elbow', 'tee', 'valve', 'pump', 'reducingTee', 'reducingElbow', 'bulkhead', 'waterChiller', 'reverseOsmosis', ]);