feat(editor): unify placement/move facing triangle into one editor-side renderer
Every placement and move path now publishes its ghost pose to a single
`useFacingPose` store, drawn by one editor-side `<FacingPoseIndicator>` overlay,
instead of each path drawing its own triangle (which left the nodes-package and
PlacementBox paths invisible):
- column/shelf presets + all moves (PlacementBox via move-registry, and
DragBoundingBox) now publish the pose, so the triangle finally shows
- stair create + move use a declarative `facingIndicator: { reversed: true }`
(new registry resolver) so the triangle sits before the entry pointing out —
resolved in one place, so create and move match automatically
- stair placement defaults to single and respects the shared `point`
continuation (C) toggle, like the other placement tools
Checkpoint on the placement-interaction epic: also carries the in-flight
continuation-profile extraction (lib/continuation), grid surface (item #8), and
HUD work. Door/window still render their own legacy inline triangle and are
migrated to the overlay next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
76089d85ea
commit
68e5c6ca67
@@ -16,11 +16,13 @@ import {
|
||||
calculateCursorRotation,
|
||||
calculateItemRotation,
|
||||
EDITOR_LAYER,
|
||||
FacingIndicator,
|
||||
getSideFromNormal,
|
||||
isMagneticSnapActive,
|
||||
isValidWallSideFace,
|
||||
triggerSFX,
|
||||
useAlignmentGuides,
|
||||
useEditor,
|
||||
} from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
@@ -73,6 +75,7 @@ type HostKind = 'wall' | 'roof' | null
|
||||
const DoorTool: React.FC = () => {
|
||||
const draftRef = useRef<DoorNode | null>(null)
|
||||
const cursorGroupRef = useRef<Group>(null!)
|
||||
const indicatorYOffsetRef = useRef<Group>(null!)
|
||||
const edgesRef = useRef<LineSegments>(null!)
|
||||
|
||||
// Off-host floating ghost: the real door geometry follows the cursor over
|
||||
@@ -155,6 +158,7 @@ const DoorTool: React.FC = () => {
|
||||
worldPosition: [number, number, number],
|
||||
cursorRotationY: number,
|
||||
valid: boolean,
|
||||
indicatorYOffset: number,
|
||||
) => {
|
||||
setFallbackPose(null)
|
||||
const group = cursorGroupRef.current
|
||||
@@ -162,6 +166,7 @@ const DoorTool: 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)
|
||||
}
|
||||
|
||||
@@ -282,6 +287,7 @@ const DoorTool: React.FC = () => {
|
||||
),
|
||||
cursorRotationY,
|
||||
valid,
|
||||
-clampedY,
|
||||
)
|
||||
|
||||
if (draftRef.current) {
|
||||
@@ -358,11 +364,16 @@ const DoorTool: React.FC = () => {
|
||||
|
||||
useScene.getState().createNode(node, wall.id as AnyNodeId)
|
||||
useViewer.getState().setSelection({ selectedIds: [node.id] })
|
||||
useScene.temporal.getState().pause()
|
||||
triggerSFX('sfx:structure-build')
|
||||
alignmentCandidates = collectWallOpeningAlignmentCandidates(useScene.getState().nodes, '')
|
||||
useAlignmentGuides.getState().clear()
|
||||
clearOpeningGuides3D()
|
||||
if (useEditor.getState().getContinuation('point') === 'repeat') {
|
||||
useScene.temporal.getState().pause()
|
||||
alignmentCandidates = collectWallOpeningAlignmentCandidates(useScene.getState().nodes, '')
|
||||
} else {
|
||||
hideCursor()
|
||||
useEditor.getState().setTool(null)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Direct wall-mesh hover ──────────────────────────────────────
|
||||
@@ -469,7 +480,7 @@ const DoorTool: React.FC = () => {
|
||||
|
||||
const updateRoofCursor = (target: RoofWallOpeningTarget, roof: RoofNode) => {
|
||||
const pose = getRoofWallOpeningCursorPose(target, roof)
|
||||
if (pose) updateCursor(pose.position, pose.rotationY, target.valid)
|
||||
if (pose) updateCursor(pose.position, pose.rotationY, target.valid, -target.position[1])
|
||||
}
|
||||
|
||||
const onRoofHover = (event: RoofEvent) => {
|
||||
@@ -568,8 +579,13 @@ const DoorTool: React.FC = () => {
|
||||
// picks up the new opening cut.
|
||||
useScene.getState().dirtyNodes.add(segment.id as AnyNodeId)
|
||||
useViewer.getState().setSelection({ selectedIds: [node.id] })
|
||||
useScene.temporal.getState().pause()
|
||||
triggerSFX('sfx:structure-build')
|
||||
if (useEditor.getState().getContinuation('point') === 'repeat') {
|
||||
useScene.temporal.getState().pause()
|
||||
} else {
|
||||
hideCursor()
|
||||
useEditor.getState().setTool(null)
|
||||
}
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
@@ -661,6 +677,9 @@ const DoorTool: React.FC = () => {
|
||||
material={edgeMaterial}
|
||||
ref={edgesRef}
|
||||
/>
|
||||
<group ref={indicatorYOffsetRef}>
|
||||
<FacingIndicator depth={ghostStub.frameDepth} />
|
||||
</group>
|
||||
</group>
|
||||
{fallbackPose && (
|
||||
<group position={fallbackPose.position} rotation-y={fallbackPose.rotationY}>
|
||||
|
||||
Reference in New Issue
Block a user