225 lines
7.5 KiB
TypeScript
225 lines
7.5 KiB
TypeScript
import { Editor } from "tldraw"
|
|
|
|
export const cameraHistory: { x: number; y: number; z: number }[] = []
|
|
const MAX_HISTORY = 10 // Keep last 10 camera positions
|
|
|
|
// Helper function to store camera position
|
|
const storeCameraPosition = (editor: Editor) => {
|
|
const currentCamera = editor.getCamera()
|
|
// Only store if there's a meaningful change from the last position
|
|
const lastPosition = cameraHistory[cameraHistory.length - 1]
|
|
if (
|
|
!lastPosition ||
|
|
Math.abs(lastPosition.x - currentCamera.x) > 1 ||
|
|
Math.abs(lastPosition.y - currentCamera.y) > 1 ||
|
|
Math.abs(lastPosition.z - currentCamera.z) > 0.1
|
|
) {
|
|
cameraHistory.push({ ...currentCamera })
|
|
if (cameraHistory.length > MAX_HISTORY) {
|
|
cameraHistory.shift()
|
|
}
|
|
console.log("Stored camera position:", currentCamera)
|
|
}
|
|
}
|
|
|
|
export const zoomToSelection = (editor: Editor) => {
|
|
// Store camera position before zooming
|
|
storeCameraPosition(editor)
|
|
|
|
// Get all selected shape IDs
|
|
const selectedIds = editor.getSelectedShapeIds()
|
|
if (selectedIds.length === 0) return
|
|
|
|
// Get the common bounds that encompass all selected shapes
|
|
const commonBounds = editor.getSelectionPageBounds()
|
|
if (!commonBounds) return
|
|
|
|
// Calculate viewport dimensions
|
|
const viewportPageBounds = editor.getViewportPageBounds()
|
|
|
|
// Calculate the ratio of selection size to viewport size
|
|
const widthRatio = commonBounds.width / viewportPageBounds.width
|
|
const heightRatio = commonBounds.height / viewportPageBounds.height
|
|
|
|
// Calculate target zoom based on selection size
|
|
let targetZoom
|
|
if (widthRatio < 0.1 || heightRatio < 0.1) {
|
|
// For very small selections, zoom in up to 20x
|
|
targetZoom = Math.min(
|
|
(viewportPageBounds.width * 0.8) / commonBounds.width,
|
|
(viewportPageBounds.height * 0.8) / commonBounds.height,
|
|
40, // Max zoom of 20x for small selections
|
|
)
|
|
} else if (widthRatio > 1 || heightRatio > 1) {
|
|
// For selections larger than viewport, zoom out more
|
|
targetZoom = Math.min(
|
|
(viewportPageBounds.width * 0.7) / commonBounds.width,
|
|
(viewportPageBounds.height * 0.7) / commonBounds.height,
|
|
0.125, // Min zoom of 1/8x for large selections (reciprocal of 8)
|
|
)
|
|
} else {
|
|
// For medium-sized selections, allow up to 10x zoom
|
|
targetZoom = Math.min(
|
|
(viewportPageBounds.width * 0.8) / commonBounds.width,
|
|
(viewportPageBounds.height * 0.8) / commonBounds.height,
|
|
20, // Medium zoom level
|
|
)
|
|
}
|
|
|
|
// Zoom to the common bounds
|
|
editor.zoomToBounds(commonBounds, {
|
|
targetZoom,
|
|
inset: widthRatio > 1 || heightRatio > 1 ? 20 : 50, // Less padding for large selections
|
|
animation: {
|
|
duration: 400,
|
|
easing: (t) => t * (2 - t),
|
|
},
|
|
})
|
|
|
|
// Update URL with new camera position and first selected shape ID
|
|
const newCamera = editor.getCamera()
|
|
const url = new URL(window.location.href)
|
|
url.searchParams.set("shapeId", selectedIds[0].toString())
|
|
url.searchParams.set("x", newCamera.x.toString())
|
|
url.searchParams.set("y", newCamera.y.toString())
|
|
url.searchParams.set("zoom", newCamera.z.toString())
|
|
window.history.replaceState(null, "", url.toString())
|
|
}
|
|
|
|
export const revertCamera = (editor: Editor) => {
|
|
if (cameraHistory.length > 0) {
|
|
const previousCamera = cameraHistory.pop()
|
|
if (previousCamera) {
|
|
// Get current viewport bounds
|
|
const viewportPageBounds = editor.getViewportPageBounds()
|
|
|
|
// Create bounds that center on the previous camera position
|
|
const targetBounds = {
|
|
x: previousCamera.x - viewportPageBounds.width / 2 / previousCamera.z,
|
|
y: previousCamera.y - viewportPageBounds.height / 2 / previousCamera.z,
|
|
w: viewportPageBounds.width / previousCamera.z,
|
|
h: viewportPageBounds.height / previousCamera.z,
|
|
}
|
|
|
|
// Use the same zoom animation as zoomToShape
|
|
editor.zoomToBounds(targetBounds, {
|
|
targetZoom: previousCamera.z,
|
|
animation: {
|
|
duration: 400,
|
|
easing: (t) => t * (2 - t),
|
|
},
|
|
})
|
|
|
|
console.log("Reverted to camera position:", previousCamera)
|
|
}
|
|
} else {
|
|
console.log("No camera history available")
|
|
}
|
|
}
|
|
|
|
export const copyLinkToCurrentView = async (editor: Editor) => {
|
|
console.log("Starting copyLinkToCurrentView")
|
|
|
|
if (!editor.store.serialize()) {
|
|
console.warn("Store not ready")
|
|
return
|
|
}
|
|
|
|
try {
|
|
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
|
console.log("Base URL:", baseUrl)
|
|
|
|
const url = new URL(baseUrl)
|
|
const camera = editor.getCamera()
|
|
console.log("Current camera position:", {
|
|
x: camera.x,
|
|
y: camera.y,
|
|
zoom: camera.z,
|
|
})
|
|
|
|
// Set camera parameters first
|
|
url.searchParams.set("x", camera.x.toString())
|
|
url.searchParams.set("y", camera.y.toString())
|
|
url.searchParams.set("zoom", camera.z.toString())
|
|
|
|
// Add shape ID last if needed
|
|
const selectedIds = editor.getSelectedShapeIds()
|
|
if (selectedIds.length > 0) {
|
|
url.searchParams.set("shapeId", selectedIds[0].toString())
|
|
}
|
|
|
|
const finalUrl = url.toString()
|
|
console.log("Final URL to copy:", finalUrl)
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
console.log("Using modern clipboard API...")
|
|
await navigator.clipboard.writeText(finalUrl)
|
|
console.log("URL copied successfully using clipboard API")
|
|
} else {
|
|
console.log("Falling back to legacy clipboard method...")
|
|
const textArea = document.createElement("textarea")
|
|
textArea.value = finalUrl
|
|
document.body.appendChild(textArea)
|
|
try {
|
|
await navigator.clipboard.writeText(textArea.value)
|
|
} catch (err) {
|
|
console.error("Clipboard API failed:", err)
|
|
}
|
|
document.body.removeChild(textArea)
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to copy to clipboard:", error)
|
|
alert("Failed to copy link. Please check clipboard permissions.")
|
|
}
|
|
}
|
|
|
|
/** TODO: doesnt UNlock */
|
|
export const lockCameraToFrame = async (editor: Editor) => {
|
|
const selectedShapes = editor.getSelectedShapes()
|
|
if (selectedShapes.length === 0) return
|
|
const selectedShape = selectedShapes[0]
|
|
const isFrame = selectedShape.type === "frame"
|
|
const bounds = editor.getShapePageBounds(selectedShape)
|
|
if (!isFrame || !bounds) return
|
|
|
|
try {
|
|
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
|
const url = new URL(baseUrl)
|
|
|
|
// Calculate zoom level to fit the frame (for URL only)
|
|
const viewportPageBounds = editor.getViewportPageBounds()
|
|
const targetZoom = Math.min(
|
|
viewportPageBounds.width / bounds.width,
|
|
viewportPageBounds.height / bounds.height,
|
|
1, // Cap at 1x zoom
|
|
)
|
|
|
|
// Set URL parameters without affecting the current view
|
|
url.searchParams.set("x", Math.round(bounds.x).toString())
|
|
url.searchParams.set("y", Math.round(bounds.y).toString())
|
|
url.searchParams.set(
|
|
"zoom",
|
|
(Math.round(targetZoom * 100) / 100).toString(),
|
|
)
|
|
url.searchParams.set("frameId", selectedShape.id)
|
|
url.searchParams.set("isLocked", "true")
|
|
|
|
const finalUrl = url.toString()
|
|
|
|
// Copy URL to clipboard without modifying the current view
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(finalUrl)
|
|
} else {
|
|
const textArea = document.createElement("textarea")
|
|
textArea.value = finalUrl
|
|
document.body.appendChild(textArea)
|
|
textArea.select()
|
|
document.execCommand("copy")
|
|
document.body.removeChild(textArea)
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to copy frame link:", error)
|
|
alert("Failed to copy frame link. Please check clipboard permissions.")
|
|
}
|
|
}
|