167 lines
4.8 KiB
TypeScript
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 } })
|
|
}
|
|
},
|
|
}
|
|
}
|