feat(editor): MEP placement migration — Shift=cycle / mode-driven snapping

Migrate all 9 MEP kinds' placement tools onto the unified snapping model:
declare snapProfile ('item' for point-placed hvac-equipment / duct-terminal /
duct-fitting / pipe-fitting / pipe-trap; 'structural' for directional runs
duct-segment / pipe-segment / liquid-line / lineset), and replace the legacy
shiftKey-bypass reads with mode-driven isGridSnapActive / isMagneticSnapActive /
isAngleSnapActive. For runs the 45° lock becomes the cyclable 'angles' mode;
Alt stays the vertical-riser modifier (run drafting has no validity gate to
force). Port mating gated on "mode != off". Dropped stale "⇧ smooth/free" hints.

The bespoke MEP move-tool/selection (endpoint) tools stay on the legacy model —
they use setMovingNode(null) so no moving-scope context resolves yet; migrating
them needs scope-wiring first (follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-25 10:56:55 -04:00
co-authored by Claude Opus 4.8
parent 76096ffe72
commit 2fdbcf03c3
18 changed files with 158 additions and 97 deletions
@@ -17,6 +17,7 @@ export const pipeTrapDefinition: NodeDefinition<typeof PipeTrapNode> = {
schema: PipeTrapNode,
category: 'utility',
distributionRole: 'fitting',
snapProfile: 'item',
portConnectivityFollow: false, // trap is anchored; dragging a connected run stretches the arm, not the trap
defaults: () => ({
+5 -4
View File
@@ -1,7 +1,7 @@
'use client'
import { emitter, type GridEvent, PipeTrapNode, useScene } from '@pascal-app/core'
import { triggerSFX, useEditor } from '@pascal-app/editor'
import { isGridSnapActive, triggerSFX, useEditor } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { Html } from '@react-three/drei'
import { useEffect, useMemo, useRef, useState } from 'react'
@@ -19,8 +19,9 @@ function snap(value: number, step: number): number {
/**
* Click-place tool for P-traps. The ghost follows the cursor on the
* floor. **R / T** rotate the arm ±45°, **Shift** disables grid snap.
* The pipe tool then draws the trap arm off the outlet toward the vent.
* floor. **R / T** rotate the arm ±45°; grid snap follows the active
* snapping mode (the contextual HUD chip — Shift cycles it). The pipe
* tool then draws the trap arm off the outlet toward the vent.
*/
const PipeTrapTool = () => {
const activeLevelId = useViewer((s) => s.selection.levelId)
@@ -55,7 +56,7 @@ const PipeTrapTool = () => {
if (!activeLevelId) return
const resolve = (event: GridEvent) => {
const step = event.nativeEvent?.shiftKey === true ? 0 : useEditor.getState().gridSnapStep
const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
return {
position: [snap(event.localPosition[0], step), 0, snap(event.localPosition[2], step)] as [
number,