canvas-website/src/hooks/useCameraControls.ts

167 lines
4.8 KiB
TypeScript

import { useEffect } from "react"
import { Editor, TLEventMap, TLFrameShape, TLParentId, TLShapeId } from "tldraw"
import { useSearchParams } from "react-router-dom"
// Define camera state interface
interface CameraState {
x: number
y: number
z: number
}
const MAX_HISTORY = 10
let cameraHistory: CameraState[] = []
// TODO: use this
// Improved camera change tracking with debouncing
const trackCameraChange = (editor: Editor) => {
const currentCamera = editor.getCamera()
const lastPosition = cameraHistory[cameraHistory.length - 1]
// Store any viewport change that's not from a revert operation
if (
!lastPosition ||
currentCamera.x !== lastPosition.x ||
currentCamera.y !== lastPosition.y ||
currentCamera.z !== lastPosition.z
) {
cameraHistory.push({ ...currentCamera })
if (cameraHistory.length > MAX_HISTORY) {
cameraHistory.shift()
}
}
}
export function useCameraControls(editor: Editor | null) {
const [searchParams] = useSearchParams()
// Handle URL-based camera positioning
useEffect(() => {
if (!editor || !editor.store || !editor.getInstanceState().isFocused) {
console.log("Editor not ready:", {
editor: !!editor,
store: !!editor?.store,
isFocused: editor?.getInstanceState().isFocused,
})
return
}
const x = searchParams.get("x")
const y = searchParams.get("y")
const zoom = searchParams.get("zoom")
const frameId = searchParams.get("frameId")
const isLocked = searchParams.get("isLocked") === "true"
console.log("Setting camera:", { x, y, zoom, frameId, isLocked })
// Set camera position if coordinates exist
if (x && y && zoom) {
const position = {
x: Math.round(parseFloat(x)),
y: Math.round(parseFloat(y)),
z: Math.round(parseFloat(zoom)),
}
console.log("Camera position:", position)
requestAnimationFrame(() => {
editor.setCamera(position, { animation: { duration: 0 } })
// Apply camera lock immediately after setting position if needed
if (isLocked) {
editor.setCameraOptions({ isLocked: true })
}
console.log("Current camera:", editor.getCamera())
})
}
// Handle frame-specific logic
if (frameId) {
const frame = editor.getShape(frameId as TLShapeId)
if (frame) {
editor.select(frameId as TLShapeId)
// If x/y/zoom are not provided in URL, zoom to frame bounds
if (!x || !y || !zoom) {
const bounds = editor.getShapePageBounds(frame)!
const viewportPageBounds = editor.getViewportPageBounds()
const targetZoom = Math.min(
viewportPageBounds.width / bounds.width,
viewportPageBounds.height / bounds.height,
1, // Cap at 1x zoom, matching lockCameraToFrame
)
editor.zoomToBounds(bounds, {
animation: { duration: 0 },
targetZoom,
})
}
// Apply camera lock after camera is positioned
if (isLocked) {
requestAnimationFrame(() => {
editor.setCameraOptions({ isLocked: true })
})
}
}
}
}, [editor, searchParams])
// Track camera changes
useEffect(() => {
if (!editor) return
const handler = () => {
trackCameraChange(editor)
}
// Track both viewport changes and user interaction end
editor.on("viewportChange" as keyof TLEventMap, handler)
editor.on("userChangeEnd" as keyof TLEventMap, handler)
return () => {
editor.off("viewportChange" as keyof TLEventMap, handler)
editor.off("userChangeEnd" as keyof TLEventMap, handler)
}
}, [editor])
// Enhanced camera control functions
return {
zoomToFrame: (frameId: string) => {
if (!editor) return
const frame = editor.getShape(frameId as TLParentId) as TLFrameShape
if (!frame) return
editor.zoomToBounds(editor.getShapePageBounds(frame)!, {
inset: 32,
animation: { duration: 500 },
})
},
copyLocationLink: () => {
if (!editor) return
const camera = editor.getCamera()
const url = new URL(window.location.href)
url.searchParams.set("x", Math.round(camera.x).toString())
url.searchParams.set("y", Math.round(camera.y).toString())
url.searchParams.set("zoom", Math.round(camera.z).toString())
navigator.clipboard.writeText(url.toString())
},
copyFrameLink: (frameId: string) => {
const url = new URL(window.location.href)
url.searchParams.set("frameId", frameId)
navigator.clipboard.writeText(url.toString())
},
revertCamera: () => {
if (!editor || cameraHistory.length === 0) return
const previousCamera = cameraHistory.pop()
if (previousCamera) {
editor.setCamera(previousCamera, { animation: { duration: 200 } })
}
},
}
}