Files
editor/packages/nodes/src/duct-fitting/definition.ts
T
Sudhir YadavandGitHub 5551500d98 feat: HVAC ductwork + DWV plumbing systems (#402)
Adds two new MEP node families (HVAC ductwork, DWV plumbing) built on a shared port-connectivity model. Co-authored by @sudhir9297.
2026-06-16 15:30:39 -04:00

135 lines
4.2 KiB
TypeScript

import type { NodeDefinition } from '@pascal-app/core'
import { rotateFittingNode } from '../shared/fitting-rotation'
import { buildDuctFittingFloorplan } from './floorplan'
import { buildDuctFittingGeometry } from './geometry'
import { ductFittingParametrics } from './parametrics'
import { getDuctFittingPorts } from './ports'
import { DuctFittingNode } from './schema'
/**
* Phase 2 of the HVAC node system — duct fittings (elbow / tee / reducer)
* and the first kind to expose typed ports (`def.ports`).
*
* Composition: `def.geometry` only, same as duct-segment. Ports are the
* architectural payload: placement tools snap onto them, and a later
* slice walks them to build the supply/return system graph.
*/
export const ductFittingDefinition: NodeDefinition<typeof DuctFittingNode> = {
kind: 'duct-fitting',
schemaVersion: 1,
schema: DuctFittingNode,
category: 'utility',
distributionRole: 'fitting',
defaults: () => ({
object: 'node',
parentId: null,
visible: true,
metadata: {},
position: [0, 0, 0],
rotation: [0, 0, 0],
fittingType: 'elbow',
shape: 'round',
width: 14,
height: 8,
shape2: 'round',
width2: 14,
height2: 8,
angle: 90,
branchAngle: 90,
diameter: 6,
diameter2: 6,
ductMaterial: 'sheet-metal',
system: 'supply',
}),
capabilities: {
selectable: { hitVolume: 'bbox' },
// `cursorAttached`: a fitting is a small connector — an offset-
// preserving drag reads as the mesh trailing the mouse, so pin its
// origin to the cursor instead.
movable: { axes: ['x', 'y', 'z'], gridSnap: true, cursorAttached: true },
duplicable: true,
deletable: true,
},
parametrics: ductFittingParametrics,
geometry: buildDuctFittingGeometry,
geometryKey: (n) =>
JSON.stringify([
n.fittingType,
// The mitered elbow + flange profiles swap width/height roles based
// on where world-up sits in the local frame, so orientation is a
// geometry input.
n.rotation,
n.shape,
n.width,
n.height,
n.shape2,
n.width2,
n.height2,
n.angle,
n.branchAngle,
n.diameter,
n.diameter2,
n.ductMaterial,
n.system,
]),
ports: getDuctFittingPorts,
floorplan: buildDuctFittingFloorplan,
// R/T rotate a selected fitting ±45° around the shared active axis.
// The default editor rotate only knows Y; fittings need X/Z for
// risers, so this overrides it. Alt-cycling of the axis + the axis
// badge live in `./selection.tsx`.
keyboardActions: {
r: {
appliesTo: (node) => node.type === 'duct-fitting',
run: (node) => rotateFittingNode(node, 1),
},
t: {
appliesTo: (node) => node.type === 'duct-fitting',
run: (node) => rotateFittingNode(node, -1),
},
axisCycling: true,
},
// Alt-cycles the active rotation axis while a fitting is selected.
// Editor-only (drives `useEditor.rotationAxis`), so it mounts via the
// editor's SelectionAffordanceManager rather than `def.system`.
affordanceTools: {
selection: () => import('./selection'),
// Ghost-preview duplicate / move. Duplicate is pure drag-to-place: a
// translucent copy of the fitting (built from its real geometry, at its
// own rotation, so an elbow / riser stays properly aligned) follows the
// cursor and only lands on the commit click. Takes priority over
// `capabilities.movable` in the MoveTool dispatcher.
move: () => import('./move-tool'),
},
tool: () => import('./tool'),
toolHints: [
{ key: 'Click', label: 'Place fitting' },
{ key: 'Hover a duct end', label: 'Snap onto the run' },
{ key: 'R / T', label: 'Rotate ±45°' },
{ key: 'Alt', label: 'Switch rotation axis (Y → X → Z)' },
{ key: 'Esc', label: 'Exit' },
],
presentation: {
label: 'Duct Fitting',
description: 'Elbow, tee, reducer, or square-to-round transition connecting duct runs.',
icon: { kind: 'url', src: '/icons/duct-fitting.png' },
paletteSection: 'structure',
paletteOrder: 91,
},
mcp: {
description:
'A duct fitting (elbow, tee, reducer, or square-to-round transition) with typed connection ports. Position is level-local meters; rotation is an XYZ euler in radians.',
},
}