Initial project import

This commit is contained in:
drjones
2026-06-13 17:36:44 -07:00
commit ad2a18cc8d
18471 changed files with 4497570 additions and 0 deletions

504
src/parts/catalog.ts Normal file
View File

@@ -0,0 +1,504 @@
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 },
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 },
},
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',
]);