Spaces:
Sleeping
Sleeping
| import { useEffect, useRef } from 'react' | |
| import { Editor } from 'tldraw' | |
| export function useSPen(editor: Editor) { | |
| const previousToolRef = useRef<string>('draw') | |
| // State tracking | |
| const isPenDownRef = useRef<boolean>(false) | |
| const isInButtonModeRef = useRef<boolean>(false) | |
| const lastEventRef = useRef<PointerEvent | null>(null) | |
| // DOUBLE TAP TRACKER | |
| const lastTapTimeRef = useRef<number>(0) | |
| useEffect(() => { | |
| if (!editor) return | |
| // 1. Independent Pointer State Tracker & Double Tap Detector | |
| const trackPointer = (e: PointerEvent) => { | |
| // Only care about the pen | |
| if (e.pointerType !== 'pen' || !e.isTrusted) return | |
| lastEventRef.current = e | |
| if (e.type === 'pointerdown') { | |
| isPenDownRef.current = true | |
| // --- GESTURE: HOLD BUTTON + DOUBLE TAP TO UNDO --- | |
| if (isInButtonModeRef.current) { | |
| const now = Date.now() | |
| const timeSinceLastTap = now - lastTapTimeRef.current | |
| // Check if double tap (within 300ms) | |
| if (timeSinceLastTap < 300) { | |
| console.log("↩️ S-Pen Gesture: Undo") | |
| // 1. Stop this event so we don't draw a second dot | |
| e.stopPropagation() | |
| e.preventDefault() | |
| // 2. Perform Undo | |
| // We undo twice: once to remove the 'dot' from the first tap, | |
| // and once to undo the actual action you wanted to undo. | |
| // (If the first tap didn't erase anything, tldraw ignores the extra undo safely) | |
| editor.undo() | |
| setTimeout(() => editor.undo(), 50) | |
| return // Stop processing | |
| } | |
| lastTapTimeRef.current = now | |
| } | |
| // ------------------------------------------------ | |
| } | |
| if (e.type === 'pointerup' || e.type === 'pointercancel') { | |
| isPenDownRef.current = false | |
| } | |
| } | |
| // 2. The "Hot Swap" Function (Draw <-> Eraser) | |
| const performHotSwap = (newTool: string) => { | |
| const lastEvent = lastEventRef.current | |
| // If pen isn't touching screen, just switch tool | |
| if (!isPenDownRef.current || !lastEvent) { | |
| editor.setCurrentTool(newTool) | |
| return | |
| } | |
| // If pen IS touching, cut the line and switch | |
| const target = document.elementFromPoint(lastEvent.clientX, lastEvent.clientY) || window | |
| // A. End current stroke | |
| target.dispatchEvent(new PointerEvent('pointerup', { | |
| bubbles: true, cancelable: true, view: window, | |
| clientX: lastEvent.clientX, clientY: lastEvent.clientY, | |
| pointerId: lastEvent.pointerId, pointerType: 'pen', isPrimary: true, | |
| buttons: 0, pressure: 0 | |
| })) | |
| // B. Switch tool | |
| editor.setCurrentTool(newTool) | |
| // C. Start new stroke (delayed slightly for state machine) | |
| setTimeout(() => { | |
| target.dispatchEvent(new PointerEvent('pointerdown', { | |
| bubbles: true, cancelable: true, view: window, | |
| clientX: lastEvent.clientX, clientY: lastEvent.clientY, | |
| pointerId: lastEvent.pointerId, pointerType: 'pen', isPrimary: true, | |
| buttons: 1, | |
| pressure: lastEvent.pressure || 0.5, | |
| tiltX: lastEvent.tiltX, tiltY: lastEvent.tiltY | |
| })) | |
| }, 0) | |
| } | |
| const onButtonDown = () => { | |
| isInButtonModeRef.current = true // Mark button as active | |
| const currentTool = editor.getCurrentToolId() | |
| if (currentTool !== 'eraser') { | |
| previousToolRef.current = currentTool | |
| performHotSwap('eraser') | |
| } | |
| } | |
| const onButtonUp = () => { | |
| isInButtonModeRef.current = false // Mark button as inactive | |
| performHotSwap(previousToolRef.current) | |
| } | |
| // Listeners | |
| window.addEventListener('spen-button-down', onButtonDown) | |
| window.addEventListener('spen-button-up', onButtonUp) | |
| // Capture phase (true) is critical to intercept the double tap before tldraw | |
| window.addEventListener('pointerdown', trackPointer, { capture: true }) | |
| window.addEventListener('pointerup', trackPointer, { capture: true }) | |
| window.addEventListener('pointercancel', trackPointer, { capture: true }) | |
| window.addEventListener('pointermove', trackPointer, { capture: true }) | |
| return () => { | |
| window.removeEventListener('spen-button-down', onButtonDown) | |
| window.removeEventListener('spen-button-up', onButtonUp) | |
| window.removeEventListener('pointerdown', trackPointer, { capture: true }) | |
| window.removeEventListener('pointerup', trackPointer, { capture: true }) | |
| window.removeEventListener('pointercancel', trackPointer, { capture: true }) | |
| window.removeEventListener('pointermove', trackPointer, { capture: true }) | |
| } | |
| }, [editor]) | |
| } |