fix(editor): improve guided manipulation and snap affordances
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { triggerSFX, useEditor } from '@pascal-app/editor'
|
||||
import { consumePlacementDragRelease, triggerSFX, useEditor } from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import * as THREE from 'three'
|
||||
@@ -89,9 +89,19 @@ const MoveChimneyTool = ({ node }: { node: ChimneyNode }) => {
|
||||
roofSegmentId: node.roofSegmentId,
|
||||
})
|
||||
|
||||
const clearTarget = () => {
|
||||
lastTarget = null
|
||||
setSegmentXform(null)
|
||||
setHitLocal(null)
|
||||
setPreviewSegment(null)
|
||||
}
|
||||
|
||||
const updatePreview = (event: RoofEvent) => {
|
||||
const target = roofDrag.resolve(event)
|
||||
if (!target) return
|
||||
if (!target) {
|
||||
clearTarget()
|
||||
return
|
||||
}
|
||||
lastTarget = target
|
||||
|
||||
const sx = Math.round(target.localX * 20) / 20
|
||||
@@ -156,14 +166,27 @@ const MoveChimneyTool = ({ node }: { node: ChimneyNode }) => {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const onPlacementDragPointerUp = (event: PointerEvent) => {
|
||||
if (!consumePlacementDragRelease(event)) return
|
||||
if (!lastTarget) return
|
||||
onClick({
|
||||
nativeEvent: event,
|
||||
stopPropagation: () => event.stopPropagation(),
|
||||
} as unknown as RoofEvent)
|
||||
}
|
||||
|
||||
emitter.on('roof:move', updatePreview)
|
||||
emitter.on('roof:enter', updatePreview)
|
||||
emitter.on('roof:click', onClick)
|
||||
emitter.on('roof:leave', clearTarget)
|
||||
window.addEventListener('pointerup', onPlacementDragPointerUp)
|
||||
|
||||
return () => {
|
||||
emitter.off('roof:move', updatePreview)
|
||||
emitter.off('roof:enter', updatePreview)
|
||||
emitter.off('roof:click', onClick)
|
||||
emitter.off('roof:leave', clearTarget)
|
||||
window.removeEventListener('pointerup', onPlacementDragPointerUp)
|
||||
}
|
||||
}, [activeBuildingId, node, setMovingNode, setSelection])
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { triggerSFX } from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { RoofAttachmentFallbackPreview } from '../shared/roof-attachment-fallback-preview'
|
||||
import { resolveRoofSegmentHit } from '../shared/roof-segment-hit'
|
||||
import { chimneyDefinition } from './definition'
|
||||
import ChimneyPreview from './preview'
|
||||
@@ -139,19 +140,30 @@ const ChimneyTool = () => {
|
||||
}
|
||||
}, [activeBuildingId, setSelection])
|
||||
|
||||
if (!activeBuildingId || !segmentXform || !hitLocal || !previewSegment) return null
|
||||
|
||||
// Outer group mirrors the real renderer's `position={segment.position}
|
||||
// rotation-y={segment.rotation}` chain by composing the segment's
|
||||
// building-local matrix (which walks roof + level + segment). Inner
|
||||
// group offsets by the cursor's segment-local x/z so the chimney
|
||||
// geometry (built with `position[0,2] = 0`) lands under the cursor.
|
||||
return (
|
||||
<group position={segmentXform.position} quaternion={segmentXform.quaternion}>
|
||||
<group position={[hitLocal[0], 0, hitLocal[2]]}>
|
||||
<ChimneyPreview node={previewNode} segment={previewSegment} />
|
||||
</group>
|
||||
</group>
|
||||
<>
|
||||
<RoofAttachmentFallbackPreview
|
||||
activeBuildingId={activeBuildingId}
|
||||
onInvalidTarget={() => {
|
||||
setSegmentXform(null)
|
||||
setHitLocal(null)
|
||||
setPreviewSegment(null)
|
||||
}}
|
||||
size={[1, 2.5, 1]}
|
||||
/>
|
||||
{activeBuildingId && segmentXform && hitLocal && previewSegment && (
|
||||
// Outer group mirrors the real renderer's `position={segment.position}
|
||||
// rotation-y={segment.rotation}` chain by composing the segment's
|
||||
// building-local matrix (which walks roof + level + segment). Inner
|
||||
// group offsets by the cursor's segment-local x/z so the chimney
|
||||
// geometry (built with `position[0,2] = 0`) lands under the cursor.
|
||||
<group position={segmentXform.position} quaternion={segmentXform.quaternion}>
|
||||
<group position={[hitLocal[0], 0, hitLocal[2]]}>
|
||||
<ChimneyPreview node={previewNode} segment={previewSegment} />
|
||||
</group>
|
||||
</group>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user