Files
hydro-builder-app/src/parts/catalog.ts

509 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, number>): 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<PartType, PartDefinition> = {
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 (15180°)',
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 (0100% 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<PartType>([
'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<PartType>([
'pipe', 'coupling', 'reducer', 'elbow', 'tee', 'valve', 'pump',
'reducingTee', 'reducingElbow', 'bulkhead', 'waterChiller', 'reverseOsmosis',
]);