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
+36 -27
View File
@@ -3,6 +3,7 @@
import { type AnyNodeId, DormerNode, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useMemo } from 'react'
import { RoofAttachmentFallbackPreview } from '../shared/roof-attachment-fallback-preview'
import { dormerDefinition } from './definition'
import DormerPreview from './preview'
import { useDormerPlacement } from './use-dormer-placement'
@@ -47,36 +48,44 @@ const DormerTool = () => {
[],
)
const { activeBuildingId, segmentXform, hitLocal, ghostRotation } = useDormerPlacement({
onCommit: (hit, rotation) => {
const state = useScene.getState()
const dormer = DormerNode.parse({
...dormerDefinition.defaults(),
name: `Dormer ${nextDormerNumber(state.nodes)}`,
roofSegmentId: hit.segment.id,
parentId: hit.segment.id,
// Anchor at the slope height so the renderer matches the ghost.
// The CSG still carves cleanly because it inverts T(position)
// when bringing the host into dormer-local.
position: [hit.localX, hit.localY, hit.localZ],
rotation,
})
state.createNode(dormer, hit.segment.id as AnyNodeId)
state.dirtyNodes.add(hit.segment.id as AnyNodeId)
setSelection({ selectedIds: [dormer.id] })
},
})
if (!activeBuildingId || !segmentXform || !hitLocal) return null
const { activeBuildingId, clearPreview, segmentXform, hitLocal, ghostRotation } =
useDormerPlacement({
onCommit: (hit, rotation) => {
const state = useScene.getState()
const dormer = DormerNode.parse({
...dormerDefinition.defaults(),
name: `Dormer ${nextDormerNumber(state.nodes)}`,
roofSegmentId: hit.segment.id,
parentId: hit.segment.id,
// Anchor at the slope height so the renderer matches the ghost.
// The CSG still carves cleanly because it inverts T(position)
// when bringing the host into dormer-local.
position: [hit.localX, hit.localY, hit.localZ],
rotation,
})
state.createNode(dormer, hit.segment.id as AnyNodeId)
state.dirtyNodes.add(hit.segment.id as AnyNodeId)
setSelection({ selectedIds: [dormer.id] })
},
})
return (
<group position={segmentXform.position} quaternion={segmentXform.quaternion}>
<group position={hitLocal}>
<group rotation-y={ghostRotation}>
<DormerPreview node={previewNode} />
<>
<RoofAttachmentFallbackPreview
activeBuildingId={activeBuildingId}
onInvalidTarget={clearPreview}
size={[1.8, 1.8, 1.4]}
/>
{activeBuildingId && segmentXform && hitLocal && (
<group position={segmentXform.position} quaternion={segmentXform.quaternion}>
<group position={hitLocal}>
<group rotation-y={ghostRotation}>
<DormerPreview node={previewNode} />
</group>
</group>
</group>
</group>
</group>
)}
</>
)
}