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:
co-authored by
Claude Opus 4.8
parent
76096ffe72
commit
2fdbcf03c3
@@ -22,6 +22,10 @@ export const linesetDefinition: NodeDefinition<typeof LinesetNode> = {
|
||||
schema: LinesetNode,
|
||||
category: 'utility',
|
||||
distributionRole: 'run',
|
||||
// Directional run: like a wall, drafting sets a direction, so it takes the
|
||||
// structural snapping context (grid / lines / angles / off) with a 45° angle
|
||||
// lock available as a cyclable mode.
|
||||
snapProfile: 'structural',
|
||||
|
||||
defaults: () => ({
|
||||
object: 'node',
|
||||
|
||||
@@ -5,6 +5,9 @@ import {
|
||||
CursorSphere,
|
||||
DimensionPill,
|
||||
EDITOR_LAYER,
|
||||
isAngleSnapActive,
|
||||
isGridSnapActive,
|
||||
isMagneticSnapActive,
|
||||
markToolCancelConsumed,
|
||||
triggerSFX,
|
||||
useEditor,
|
||||
@@ -28,10 +31,12 @@ import { linesetDefinition } from './definition'
|
||||
* service port (a condenser / coil valve, or another lineset's end) it
|
||||
* snaps onto the port so a run mates flush.
|
||||
* - **Second click** commits a two-point lineset and re-arms the tool.
|
||||
* - The in-flight end is angle-locked to the nearest 45° step in XZ from
|
||||
* the start; Y stays at the start's height. Hold **Shift** to release.
|
||||
* - The in-flight end follows the active snapping mode: `angles` locks it to
|
||||
* the nearest 45° step in XZ from the start (Y stays at the start's
|
||||
* height); `grid`/`lines`/`off` leave it free. Shift cycles the mode.
|
||||
* - Hold **Alt** → vertical mode. XZ locks to the start; vertical mouse
|
||||
* motion drives Y. Click commits the riser segment.
|
||||
* motion drives Y. Click commits the riser segment. (Drafting has no
|
||||
* validity gate, so Alt is the riser modifier here, not force-place.)
|
||||
* - Esc clears an anchored start point.
|
||||
*
|
||||
* Snapping is restricted to refrigerant ports, so a lineset never grabs a
|
||||
@@ -137,14 +142,17 @@ const LinesetTool = () => {
|
||||
const resolveSnappedPoint = (
|
||||
event: GridEvent,
|
||||
): { point: [number, number, number]; snapped: [number, number, number] | null } => {
|
||||
// Port mating is the run's primary affordance; it stays on in every
|
||||
// snapping mode except `off` (the raw-cursor bypass).
|
||||
const snapEnabled = isGridSnapActive() || isMagneticSnapActive() || isAngleSnapActive()
|
||||
const last = draftRef.current.at(-1)
|
||||
if (!last) {
|
||||
const raw: [number, number, number] = [event.localPosition[0], 0, event.localPosition[2]]
|
||||
if (event.nativeEvent?.altKey !== true) {
|
||||
if (event.nativeEvent?.altKey !== true && snapEnabled) {
|
||||
const target = findNearbyPort(raw)
|
||||
if (target) return { point: target, snapped: target }
|
||||
}
|
||||
const step = useEditor.getState().gridSnapStep
|
||||
const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
||||
return { point: [snap(raw[0], step), 0, snap(raw[2], step)], snapped: null }
|
||||
}
|
||||
const rawXZ: [number, number, number] = [
|
||||
@@ -152,13 +160,14 @@ const LinesetTool = () => {
|
||||
last[1],
|
||||
event.localPosition[2],
|
||||
]
|
||||
const shift = event.nativeEvent?.shiftKey === true
|
||||
const angled = shift ? rawXZ : projectToAngleLock(last, rawXZ)
|
||||
if (event.nativeEvent?.altKey !== true && !shift) {
|
||||
// The 45° lock is now the `angles` snapping mode (Shift cycles to it),
|
||||
// not a held key.
|
||||
const angled = isAngleSnapActive() ? projectToAngleLock(last, rawXZ) : rawXZ
|
||||
if (event.nativeEvent?.altKey !== true && snapEnabled) {
|
||||
const target = findNearbyPort(rawXZ)
|
||||
if (target) return { point: target, snapped: target }
|
||||
}
|
||||
const step = useEditor.getState().gridSnapStep
|
||||
const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
||||
return { point: [snap(angled[0], step), angled[1], snap(angled[2], step)], snapped: null }
|
||||
}
|
||||
|
||||
@@ -166,7 +175,7 @@ const LinesetTool = () => {
|
||||
const anchor = altAnchorRef.current
|
||||
const last = draftRef.current.at(-1)
|
||||
if (!anchor || !last) return null
|
||||
const step = useEditor.getState().gridSnapStep
|
||||
const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
||||
const dy = (anchor.clientY - clientY) / ALT_PIXELS_PER_METER
|
||||
const snappedDy = snap(dy, step)
|
||||
const y = Math.min(ALT_Y_MAX_M, Math.max(ALT_Y_MIN_M, anchor.baseY + snappedDy))
|
||||
@@ -175,17 +184,17 @@ const LinesetTool = () => {
|
||||
|
||||
// Resolve the cursor point (port / grid / angle snap) then layer
|
||||
// Figma-style alignment so a lineset lines up with other runs, equipment,
|
||||
// and items as it's drawn. Free point (first vertex / Shift) snaps; an
|
||||
// angle-locked continuation shows the guide passively. Port snap or Alt
|
||||
// bypasses alignment.
|
||||
// and items as it's drawn. A free point (first vertex, or no angle lock)
|
||||
// snaps; an angle-locked continuation shows the guide passively so it
|
||||
// doesn't fight the angle ray. Alignment follows the `lines` mode; a port
|
||||
// snap or Alt-vertical bypasses it.
|
||||
const resolveAlignedPoint = (event: GridEvent) => {
|
||||
const r = resolveSnappedPoint(event)
|
||||
const hasStart = draftRef.current.length > 0
|
||||
const shift = event.nativeEvent?.shiftKey === true
|
||||
const alt = event.nativeEvent?.altKey === true
|
||||
const point = alignDrawPoint(r.point, {
|
||||
applySnap: !hasStart || shift,
|
||||
bypass: alt || r.snapped !== null,
|
||||
applySnap: !hasStart || !isAngleSnapActive(),
|
||||
bypass: !isMagneticSnapActive() || alt || r.snapped !== null,
|
||||
})
|
||||
return { ...r, point }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user