feat(editor): route door/window facing triangle through the unified overlay

Door and window are placed via preset and moved with a bespoke wall-bound
`move-tool` (affordanceTools.move), not the draw tool — so the previous inline
triangle never showed for the paths actually used. Migrate both:

- move-tool (move + preset, the community path): publish the on-wall ghost pose
  to `useFacingPose` in the same building-local frame the ghost renders in,
  dropped to the floor under the wall (the ghost Y is the opening centre); clear
  on every off-wall / hide / reveal / unmount path
- draw tool (standalone from-scratch path): publish the on-host pose, clear on
  fallback/hide; frame depth read via a ref to keep the setup effect deps clean

Removes the now-dead `FacingIndicator` public export (the editor-side overlay is
its only consumer, via relative import). The unified overlay now covers every
placement/move path: items, column/shelf, stair, and door/window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-26 22:12:08 -04:00
co-authored by Claude Opus 4.8
parent 68e5c6ca67
commit 67a558a883
5 changed files with 88 additions and 29 deletions
+28 -8
View File
@@ -25,6 +25,7 @@ import {
triggerSFX,
useAlignmentGuides,
useEditor,
useFacingPose,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -261,6 +262,7 @@ const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWindowNode
useAlignmentGuides.getState().clear()
clearOpeningGuides3D()
setGhostPose(null)
useFacingPose.getState().clear()
}
// Alignment candidates — only OTHER things on a wall (sibling openings +
@@ -413,19 +415,33 @@ const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWindowNode
target.wallNode.end[1] - target.wallNode.start[1],
target.wallNode.end[0] - target.wallNode.start[0],
)
const ghostWorldPos = wallLocalToWorld(
target.wallNode,
target.clampedX,
target.clampedY,
getLevelYOffset(),
getSlabElevation(target.event),
)
const ghostYaw = target.itemRotation - wallAngle
setGhostPose({
position: wallLocalToWorld(
target.wallNode,
target.clampedX,
target.clampedY,
getLevelYOffset(),
getSlabElevation(target.event),
),
rotationY: target.itemRotation - wallAngle,
position: ghostWorldPos,
rotationY: ghostYaw,
tint: placement.tint,
floorY: getLevelYOffset() + getSlabElevation(target.event),
side: target.side,
})
// Forward-facing triangle (editor-side overlay), in the same building-local
// frame the ghost renders in. The window's front is its local +Z. Drop it
// to the floor under the wall (the ghost Y is the sill centre, up the wall).
useFacingPose.getState().set({
position: [
ghostWorldPos[0],
getLevelYOffset() + getSlabElevation(target.event),
ghostWorldPos[2],
],
rotationY: ghostYaw,
depth: movingWindowNode.frameDepth ?? 0.07,
})
publishOpeningGuidesForWallEvent({
wall: target.wallNode,
@@ -595,6 +611,7 @@ const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWindowNode
// which previews with the real mesh (the ghost-tint flow is wall-specific).
const revealRealNode = () => {
setGhostPose(null)
useFacingPose.getState().clear()
const live = useScene.getState().nodes[movingWindowNode.id as AnyNodeId] as
| WindowNode
| undefined
@@ -652,6 +669,8 @@ const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWindowNode
floorY: getLevelYOffset(),
side: sideOverride,
})
// Off-wall (no host) floating ghost — no direction triangle.
useFacingPose.getState().clear()
}
const onGridMove = (event: GridEvent) => {
@@ -953,6 +972,7 @@ const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWindowNode
useLiveTransforms.getState().clear(movingWindowNode.id)
useAlignmentGuides.getState().clear()
clearOpeningGuides3D()
useFacingPose.getState().clear()
useScene.temporal.getState().resume()
emitter.off('wall:enter', onWallEnter)
emitter.off('wall:move', onWallMove)