509 lines
17 KiB
TypeScript
509 lines
17 KiB
TypeScript
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 (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<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',
|
||
]);
|