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
+16 -6
View File
@@ -16,7 +16,6 @@ import {
calculateCursorRotation,
calculateItemRotation,
EDITOR_LAYER,
FacingIndicator,
getSideFromNormal,
isMagneticSnapActive,
isValidWallSideFace,
@@ -24,6 +23,7 @@ import {
triggerSFX,
useAlignmentGuides,
useEditor,
useFacingPose,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useMemo, useRef, useState } from 'react'
@@ -88,7 +88,6 @@ type HostKind = 'wall' | 'roof' | null
const WindowTool: React.FC = () => {
const draftRef = useRef<WindowNode | null>(null)
const cursorGroupRef = useRef<Group>(null!)
const indicatorYOffsetRef = useRef<Group>(null!)
const edgesRef = useRef<LineSegments>(null!)
// Off-host floating ghost: the real window geometry follows the cursor
@@ -112,6 +111,10 @@ const WindowTool: React.FC = () => {
}),
[fallbackPose?.side],
)
// The frame depth is a fixed parse default (the `side` flip doesn't change
// it); a ref lets the facing-pose publish inside the setup effect read it
// without re-subscribing every event listener.
const frameDepthRef = useRef(ghostStub.frameDepth)
useEffect(() => {
useScene.temporal.getState().pause()
@@ -158,6 +161,7 @@ const WindowTool: React.FC = () => {
useAlignmentGuides.getState().clear()
clearOpeningGuides3D()
setFallbackPose(null)
useFacingPose.getState().clear()
}
// Alignment candidates — anchors of every alignable object; refreshed
@@ -180,8 +184,15 @@ const WindowTool: React.FC = () => {
group.visible = true
group.position.set(...worldPosition)
group.rotation.y = cursorRotationY
indicatorYOffsetRef.current?.position.set(0, indicatorYOffset, 0)
edgeMaterial.color.setHex(valid ? 0x22_c5_5e : 0xef_44_44)
// Forward-facing triangle (editor-side overlay). The cursor group is
// already yawed so +Z faces out of the wall, so the window's front is +Z.
// The indicator rides at the sill (`indicatorYOffset`).
useFacingPose.getState().set({
position: [worldPosition[0], worldPosition[1] + indicatorYOffset, worldPosition[2]],
rotationY: cursorRotationY,
depth: frameDepthRef.current,
})
}
// Off-host fallback: hide the wireframe outline and float the real window
@@ -198,6 +209,8 @@ const WindowTool: React.FC = () => {
})
useAlignmentGuides.getState().clear()
clearOpeningGuides3D()
// Off-host (invalid) floating ghost — no direction triangle.
useFacingPose.getState().clear()
}
const showRoofFallbackCursor = (event: RoofEvent) => {
@@ -730,9 +743,6 @@ const WindowTool: React.FC = () => {
material={edgeMaterial}
ref={edgesRef}
/>
<group ref={indicatorYOffsetRef}>
<FacingIndicator depth={ghostStub.frameDepth} />
</group>
</group>
{fallbackPose && (
<group position={fallbackPose.position} rotation-y={fallbackPose.rotationY}>