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
@@ -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 {
createRelativeRoofDrag,
@@ -60,8 +65,15 @@ export default function MoveRidgeVentTool({ node }: { node: RidgeVentNode }) {
let lastSnap: [number, number] | null = null
let lastTarget: RidgeVentDragTarget | null = null
let committed = false
const roofDrag = createRelativeRoofDrag(original)
const clearTarget = () => {
lastTarget = null
lastSnap = null
setPreviewPos(null)
}
const resolveTarget = (event: RoofEvent): RidgeVentDragTarget | null => {
const target = roofDrag.resolve(event)
if (!target) return null
@@ -75,7 +87,10 @@ export default function MoveRidgeVentTool({ node }: { node: RidgeVentNode }) {
const updatePreview = (event: RoofEvent) => {
const target = resolveTarget(event)
if (!target) return
if (!target) {
clearTarget()
return
}
lastTarget = target
const sx = Math.round(target.localX * 20) / 20
@@ -100,8 +115,10 @@ export default function MoveRidgeVentTool({ node }: { node: RidgeVentNode }) {
}
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 st = useScene.getState()
@@ -180,16 +197,29 @@ export default function MoveRidgeVentTool({ node }: { node: RidgeVentNode }) {
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
+24 -7
View File
@@ -14,6 +14,7 @@ import { useViewer } from '@pascal-app/viewer'
import { useEffect, useMemo, useRef, useState } from 'react'
import * as THREE from 'three'
import { resolveRidgeSnap } from '../shared/ridge-snap'
import { RoofAttachmentFallbackPreview } from '../shared/roof-attachment-fallback-preview'
import { resolveRoofSegmentHit } from '../shared/roof-segment-hit'
import { ridgeVentDefinition } from './definition'
import RidgeVentPreview from './preview'
@@ -135,14 +136,30 @@ const RidgeVentTool = () => {
}
}, [activeBuildingId, setSelection])
if (!activeBuildingId || !previewPos) return null
return (
<group position={previewPos}>
<group rotation-y={previewYaw}>
<RidgeVentPreview node={previewNode} />
</group>
</group>
<>
<RoofAttachmentFallbackPreview
activeBuildingId={activeBuildingId}
isValidRoofTarget={(event) => {
const hit = resolveRoofSegmentHit(
event.node as RoofNode,
event.position[0],
event.position[1],
event.position[2],
)
return !!hit && !!resolveRidgeSnap(hit.segment, hit.localX, hit.localZ)
}}
onInvalidTarget={() => setPreviewPos(null)}
size={[2, 0.15, 0.35]}
/>
{activeBuildingId && previewPos && (
<group position={previewPos}>
<group rotation-y={previewYaw}>
<RidgeVentPreview node={previewNode} />
</group>
</group>
)}
</>
)
}