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
+32 -2
View File
@@ -10,7 +10,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 { createRelativeRoofDrag } from '../shared/relative-roof-drag'
import { type EaveSnap, resolveEaveSnap } from './eave-snap'
@@ -71,8 +76,15 @@ export default function MoveGutterTool({ node }: { node: GutterNode }) {
let lastSnap: [number, number] | null = null
let lastTarget: GutterDragTarget | null = null
let committed = false
const roofDrag = createRelativeRoofDrag(original)
const clearTarget = () => {
lastTarget = null
lastSnap = null
setTarget(null)
}
const resolveTarget = (event: RoofEvent): GutterDragTarget | null => {
const target = roofDrag.resolve(event)
if (!target) return null
@@ -85,7 +97,10 @@ export default function MoveGutterTool({ node }: { node: GutterNode }) {
const updatePreview = (event: RoofEvent) => {
const roof = event.node as RoofNode
const target = resolveTarget(event)
if (!target) return
if (!target) {
clearTarget()
return
}
lastTarget = target
// Same snap math as the placement tool — picking-up and putting-
@@ -120,8 +135,10 @@ export default function MoveGutterTool({ node }: { node: GutterNode }) {
}
const onRoofClick = (event: RoofEvent) => {
if (committed) return
const target = lastTarget ?? resolveTarget(event)
if (!target) return
committed = true
const targetSegmentId = target.segment.id as AnyNodeId
const { snap } = target
const st = useScene.getState()
@@ -201,16 +218,29 @@ export default function MoveGutterTool({ node }: { node: GutterNode }) {
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 -11
View File
@@ -11,6 +11,7 @@ import {
import { triggerSFX } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useMemo, useRef, useState } from 'react'
import { RoofAttachmentFallbackPreview } from '../shared/roof-attachment-fallback-preview'
import { resolveRoofSegmentHit } from '../shared/roof-segment-hit'
import { gutterDefinition } from './definition'
import { type EaveSnap, resolveEaveSnap } from './eave-snap'
@@ -142,19 +143,26 @@ const GutterTool = () => {
}
}, [activeBuildingId, setSelection])
if (!activeBuildingId || !target) return null
return (
<group position={target.roof.position} rotation-y={target.roof.rotation}>
<group position={target.segment.position} rotation-y={target.segment.rotation}>
<group
position={[target.snap.eaveX, target.snap.eaveY, target.snap.eaveZ]}
rotation-y={target.snap.rotation}
>
<GutterPreview node={previewNode} />
<>
<RoofAttachmentFallbackPreview
activeBuildingId={activeBuildingId}
onInvalidTarget={() => setTarget(null)}
size={[2, 0.2, 0.25]}
/>
{activeBuildingId && target && (
<group position={target.roof.position} rotation-y={target.roof.rotation}>
<group position={target.segment.position} rotation-y={target.segment.rotation}>
<group
position={[target.snap.eaveX, target.snap.eaveY, target.snap.eaveZ]}
rotation-y={target.snap.rotation}
>
<GutterPreview node={previewNode} />
</group>
</group>
</group>
</group>
</group>
)}
</>
)
}