Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,41 @@ describe('item move history', () => {
expect(history()).toEqual({ past: before.past, tracking: true, depth: 0 })
})
})

describe('item placement selection', () => {
// A surface `:click` is synthesized on pointer-up (use-node-events), and the browser fires its
// own click after it. By then the editor is back in select mode, where the grid takes that click
// for one on empty ground and drops the selection of the item just placed.
test('a drop on pointer-up selects the new item and eats the click that follows', async () => {
const renderer = await create(<Placement />)
try {
await carry(10, 14)
const dropOnRelease = () =>
emitter.emit('node:click', {
node: { id: 'site_placement_history', type: 'site' },
position: [1.4, 0, 0.5],
localPosition: [1.4, 0, 0.5],
nativeEvent: { nativeEvent: {} },
stopPropagation() {},
} as never)
window.addEventListener('pointerup', dropOnRelease)
await act(async () => {
window.dispatchEvent(new Event('pointerup'))
})
window.removeEventListener('pointerup', dropOnRelease)
const placed = levelItems().find((node) => node.id !== item.id)
expect(placed).toBeDefined()
expect(useViewer.getState().selection.selectedIds).toEqual([placed!.id])
const click = new Event('click', { cancelable: true })
window.dispatchEvent(click)
expect(click.defaultPrevented).toBe(true)
// Only that one click: once the swallow window closes, a click reaches the editor again.
await new Promise((resolve) => setTimeout(resolve, 350))
const next = new Event('click', { cancelable: true })
window.dispatchEvent(next)
expect(next.defaultPrevented).toBe(false)
} finally {
await renderer.unmount()
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,29 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
if (mesh) reconcileDraftRaycast(mesh)
}

// Eat the click the browser fires after a pointer-up that committed, so it
// neither commits a second time through a surface `:click` handler nor reaches
// the grid, in select mode by then, as a click on empty ground.
const swallowNextClick = () => {
const swallow = (e: Event) => {
e.stopPropagation()
e.preventDefault()
}
window.addEventListener('click', swallow, { capture: true, once: true })
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
}
// True between a press's pointer-up and the click the browser fires after it.
// Surface `:click` events are synthesized on pointer-up (use-node-events), so a
// commit they trigger still has that click to come; a `grid:click` commit is
// that click.
let releasing = false
const onPointerUpCapture = () => {
releasing = true
}
const onClickCapture = () => {
releasing = false
}

const finishCommittedPlacement = (
committedId: string | null,
wasAdopted: boolean,
Expand All @@ -629,6 +652,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
// lands on the just-placed (now selected) node instead of a tool-less build
// limbo. Repeat placements took the early return above and stay armed.
useEditor.getState().setMode('select')
if (releasing) swallowNextClick()
}

const commitDraft = (
Expand Down Expand Up @@ -1054,16 +1078,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
// replays it. Captured once at setup — a fresh coordinator mounts per move.
const dragMode = useEditor.getState().placementDragMode
let releaseCommit: (() => void) | null = null
// Eat the click the browser fires after pointer-up so the surface
// `:click` handlers don't commit a second time.
const swallowNextClick = () => {
const swallow = (e: Event) => {
e.stopPropagation()
e.preventDefault()
}
window.addEventListener('click', swallow, { capture: true, once: true })
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
}
const onReleaseCommit = () => {
gridDispatch.flush()
if (!releaseCommit) return
Expand Down Expand Up @@ -2797,11 +2811,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
}
emitter.on('node:click', commitFloorOnSurfaceClick as never)
if (dragMode) window.addEventListener('pointerup', onReleaseCommit)
window.addEventListener('pointerup', onPointerUpCapture, true)
window.addEventListener('click', onClickCapture, true)

return () => {
tearingDown = true
feedback.clear()
if (dragMode) window.removeEventListener('pointerup', onReleaseCommit)
window.removeEventListener('pointerup', onPointerUpCapture, true)
window.removeEventListener('click', onClickCapture, true)
unsubDraftWatch()
useAlignmentGuides.getState().clear()
// Clear every live preview channel before restoring or deleting the draft.
Expand Down
Loading