Files
editor/packages/nodes/src/duct-terminal/definition.ts
T
Wassim SAMADandClaude Opus 4.8 267dac7b1a perf(editor): convert UI icons from PNG to WebP
The editor's /icons assets were ~10MB of oversized PNGs (a single
toolbar icon up to 1.4MB). Convert every non-PWA icon to WebP (quality
92) and repoint all /icons/*.png references to .webp across
packages/editor, packages/nodes, and apps/editor. PWA/platform icons
(apple-touch-icon, icon-192, icon-512) stay PNG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:41:50 -04:00

102 lines
3.2 KiB
TypeScript

import type { NodeDefinition } from '@pascal-app/core'
import { buildDuctTerminalFloorplan } from './floorplan'
import { buildDuctTerminalGeometry } from './geometry'
import { ductTerminalParametrics } from './parametrics'
import { getDuctTerminalPorts } from './ports'
import { DuctTerminalNode } from './schema'
/**
* Phase 3 of the HVAC node system — duct terminals: supply registers,
* ceiling diffusers, return grilles. The end of the air loop. One typed
* port at the collar (mount-aware direction) so duct runs end onto a
* terminal like any other port.
*
* Composition: `def.geometry` only. Yaw-only rotation — the editor's
* default R-rotate works on a selected terminal.
*/
export const ductTerminalDefinition: NodeDefinition<typeof DuctTerminalNode> = {
kind: 'duct-terminal',
schemaVersion: 1,
schema: DuctTerminalNode,
category: 'utility',
distributionRole: 'terminal',
defaults: () => ({
object: 'node',
parentId: null,
visible: true,
metadata: {},
position: [0, 0, 0],
rotation: 0,
terminalType: 'supply-register',
mount: 'floor',
width: 0.3,
depth: 0.15,
collarShape: 'round',
collarDiameter: 6,
collarWidth: 10,
collarHeight: 6,
}),
capabilities: {
selectable: { hitVolume: 'bbox' },
movable: { axes: ['x', 'z'], gridSnap: true, portSnap: { systems: ['supply', 'return'] } },
rotatable: { axes: ['y'], snapAngles: [Math.PI / 4] },
duplicable: true,
deletable: true,
// A floor register rests on top of whatever slab is under it — the
// generic FloorElevationSystem lifts its mesh Y by the slab's elevation
// so the face sits on the slab surface instead of sinking into it.
// Ceiling / wall mounts derive their Y elsewhere, so `applies` skips them.
floorPlaced: {
footprint: (node) => {
const t = node as DuctTerminalNode
return { dimensions: [t.width, 0, t.depth], rotation: [0, t.rotation, 0] }
},
applies: (node) => (node as DuctTerminalNode).mount === 'floor',
},
},
parametrics: ductTerminalParametrics,
geometry: buildDuctTerminalGeometry,
geometryKey: (n) =>
JSON.stringify([
n.terminalType,
n.mount,
n.width,
n.depth,
n.collarShape,
n.collarDiameter,
n.collarWidth,
n.collarHeight,
]),
ports: getDuctTerminalPorts,
floorplan: buildDuctTerminalFloorplan,
tool: () => import('./tool'),
toolHints: [
{ key: 'Click', label: 'Place register' },
{ key: 'M', label: 'Mount: floor / ceiling / wall' },
{ key: 'R / T', label: 'Rotate ±45° (floor / ceiling)' },
{ key: 'Shift', label: 'Smooth (no grid snap)' },
{ key: 'Esc', label: 'Exit' },
],
presentation: {
label: 'Register',
description:
'Duct terminal — supply register, ceiling diffuser, or return grille. Duct runs end at its collar.',
icon: { kind: 'url', src: '/icons/registers.webp' },
paletteSection: 'structure',
paletteOrder: 93,
},
mcp: {
description:
'A duct terminal (supply register, ceiling diffuser, or return grille) with a single collar port. Mount (floor/ceiling/wall) drives the face orientation and collar direction.',
},
}