fix(editor): improve guided manipulation and snap affordances

This commit is contained in:
Aymeric Rabot
2026-06-11 23:59:15 -04:00
committed by GitHub
parent aab48e053f
commit 5411f5abc8
89 changed files with 2830 additions and 518 deletions
+33 -2
View File
@@ -9,7 +9,12 @@ import {
sceneRegistry,
useScene,
} from '@pascal-app/core'
import { markToolCancelConsumed, triggerSFX, useEditor } from '@pascal-app/editor'
import {
consumePlacementDragRelease,
markToolCancelConsumed,
triggerSFX,
useEditor,
} from '@pascal-app/editor'
import { useCallback, useEffect, useState } from 'react'
import * as THREE from 'three'
import {
@@ -57,11 +62,22 @@ export default function MoveCupolaTool({ node }: { node: CupolaNode }) {
let lastSnap: [number, number] | null = null
let lastTarget: RelativeRoofDragTarget | null = null
let committed = false
const roofDrag = createRelativeRoofDrag(original)
const clearTarget = () => {
lastTarget = null
lastSnap = null
setPreviewPos(null)
setPreviewSurfaceQuat(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
@@ -88,8 +104,10 @@ export default function MoveCupolaTool({ node }: { node: CupolaNode }) {
}
const onRoofClick = (event: RoofEvent) => {
if (committed) return
const target = lastTarget ?? roofDrag.resolve(event)
if (!target) return
committed = true
const targetSegmentId = target.segment.id as AnyNodeId
const st = useScene.getState()
@@ -168,16 +186,29 @@ export default function MoveCupolaTool({ node }: { node: CupolaNode }) {
exitMoveMode()
}
const onPlacementDragPointerUp = (event: PointerEvent) => {
if (!consumePlacementDragRelease(event)) return
if (!lastTarget) return
onRoofClick({
nativeEvent: event,
stopPropagation: () => event.stopPropagation(),
} as unknown as RoofEvent)
}
emitter.on('roof:move', updatePreview)
emitter.on('roof:enter', updatePreview)
emitter.on('roof:click', onRoofClick)
emitter.on('roof:leave', clearTarget)
emitter.on('tool:cancel', onCancel)
window.addEventListener('pointerup', onPlacementDragPointerUp)
return () => {
emitter.off('roof:move', updatePreview)
emitter.off('roof:enter', updatePreview)
emitter.off('roof:click', onRoofClick)
emitter.off('roof:leave', clearTarget)
emitter.off('tool:cancel', onCancel)
window.removeEventListener('pointerup', onPlacementDragPointerUp)
const obj = sceneRegistry.nodes.get(node.id)
if (obj) obj.visible = true
+19 -8
View File
@@ -13,6 +13,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 { getAnalyticalNormal, surfaceQuatFromNormal } from '../shared/roof-surface'
import { cupolaDefinition } from './definition'
@@ -114,16 +115,26 @@ const CupolaTool = () => {
}
}, [activeBuildingId, setSelection])
if (!activeBuildingId || !previewPos || !previewSurfaceQuat) return null
return (
<group position={previewPos}>
<group rotation-y={previewYaw}>
<group quaternion={previewSurfaceQuat}>
<CupolaPreview node={previewNode} />
<>
<RoofAttachmentFallbackPreview
activeBuildingId={activeBuildingId}
onInvalidTarget={() => {
setPreviewPos(null)
setPreviewSurfaceQuat(null)
}}
size={[0.8, 1.2, 0.8]}
/>
{activeBuildingId && previewPos && previewSurfaceQuat && (
<group position={previewPos}>
<group rotation-y={previewYaw}>
<group quaternion={previewSurfaceQuat}>
<CupolaPreview node={previewNode} />
</group>
</group>
</group>
</group>
</group>
)}
</>
)
}