From ce50026cc31db844a137fadd8aba0e5560090a9e Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sun, 8 Dec 2024 13:31:53 -0500 Subject: [PATCH] PrintToPDF integration --- package.json | 3 ++ src/App.tsx | 8 ++-- src/ui/CustomContextMenu.tsx | 9 +++++ src/ui/CustomToolbar.tsx | 10 +++++ src/ui/overrides.tsx | 12 ++++++ src/utils/handleInitialPageLoad.ts | 20 ++++++--- src/utils/pdfUtils.ts | 65 ++++++++++++++++++++++++++++++ 7 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 src/utils/pdfUtils.ts diff --git a/package.json b/package.json index 7f895bb..10aa195 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,14 @@ "@tldraw/sync-core": "^3.6.0", "@tldraw/tldraw": "^3.6.0", "@tldraw/tlschema": "^3.6.0", + "@types/jspdf": "^2.0.0", "@types/markdown-it": "^14.1.1", "@vercel/analytics": "^1.2.2", "cloudflare-workers-unfurl": "^0.0.7", "gray-matter": "^4.0.3", + "html2canvas": "^1.4.1", "itty-router": "^5.0.17", + "jspdf": "^2.5.2", "lodash.throttle": "^4.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index 4ba6a78..a776046 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,7 @@ import { EmbedShape } from "./shapes/EmbedShapeUtil" import { MarkdownShape } from "./shapes/MarkdownShapeUtil" import { MarkdownTool } from "./tools/MarkdownTool" import { createRoot } from "react-dom/client" -import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad" +import { handleInitialPageLoad } from "./utils/handleInitialPageLoad" inject() @@ -47,11 +47,10 @@ export default function InteractiveShapeExample() { ) } -createRoot(document.getElementById("root")!).render() +//createRoot(document.getElementById("root")!).render() function App() { return ( - // } /> @@ -60,6 +59,7 @@ function App() { } /> - // ) } + +createRoot(document.getElementById("root")!).render() diff --git a/src/ui/CustomContextMenu.tsx b/src/ui/CustomContextMenu.tsx index 5294b84..e5f4e3c 100644 --- a/src/ui/CustomContextMenu.tsx +++ b/src/ui/CustomContextMenu.tsx @@ -11,6 +11,7 @@ import { zoomToSelection, } from "./cameraUtils" import { useState, useEffect } from "react" +import { saveToPdf } from "../utils/pdfUtils" export function CustomContextMenu(props: TLUiContextMenuProps) { const editor = useEditor() @@ -71,6 +72,14 @@ export function CustomContextMenu(props: TLUiContextMenuProps) { disabled={!hasCameraHistory} onSelect={() => revertCamera(editor)} /> + saveToPdf(editor)} + /> {/* Creation Tools Group */} diff --git a/src/ui/CustomToolbar.tsx b/src/ui/CustomToolbar.tsx index 1074c9d..1b974c6 100644 --- a/src/ui/CustomToolbar.tsx +++ b/src/ui/CustomToolbar.tsx @@ -2,10 +2,20 @@ import { TldrawUiMenuItem } from "tldraw" import { DefaultToolbar, DefaultToolbarContent } from "tldraw" import { useTools } from "tldraw" import { useEditor } from "tldraw" +import { useState, useEffect } from "react" export function CustomToolbar() { const editor = useEditor() const tools = useTools() + const [isReady, setIsReady] = useState(false) + + useEffect(() => { + if (editor && tools) { + setIsReady(true) + } + }, [editor, tools]) + + if (!isReady) return null return ( diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx index b483b54..d4a276c 100644 --- a/src/ui/overrides.tsx +++ b/src/ui/overrides.tsx @@ -6,6 +6,7 @@ import { revertCamera, zoomToSelection, } from "./cameraUtils" +import { saveToPdf } from "../utils/pdfUtils" export const overrides: TLUiOverrides = { tools(editor, tools) { @@ -85,6 +86,17 @@ export const overrides: TLUiOverrides = { kbd: "shift+l", onSelect: () => lockCameraToFrame(editor), }, + saveToPdf: { + id: "save-to-pdf", + label: "Save Selection as PDF", + kbd: "alt+p", + onSelect: () => { + if (editor.getSelectedShapeIds().length > 0) { + saveToPdf(editor) + } + }, + readonlyOk: true, + }, } }, } diff --git a/src/utils/handleInitialPageLoad.ts b/src/utils/handleInitialPageLoad.ts index c0fd469..90e1557 100644 --- a/src/utils/handleInitialPageLoad.ts +++ b/src/utils/handleInitialPageLoad.ts @@ -1,10 +1,18 @@ -import { Editor } from "tldraw" +import { Editor, TLEventMap, TLInstancePresence } from "tldraw" -export const handleInitialPageLoad = (editor: Editor) => { - if (!editor.store || !editor.getInstanceState().isFocused) { - setTimeout(() => handleInitialPageLoad(editor), 100) - return +export const handleInitialPageLoad = async (editor: Editor) => { + // Wait for editor to be ready + while (!editor.store || !editor.getInstanceState().isFocused) { + await new Promise((resolve) => requestAnimationFrame(resolve)) } - editor.setCurrentTool("hand") + try { + // Set initial tool + editor.setCurrentTool("hand") + + // Force a re-render of the toolbar + editor.emit("toolsChange" as keyof TLEventMap) + } catch (error) { + console.error("Error during initial page load:", error) + } } diff --git a/src/utils/pdfUtils.ts b/src/utils/pdfUtils.ts new file mode 100644 index 0000000..269af35 --- /dev/null +++ b/src/utils/pdfUtils.ts @@ -0,0 +1,65 @@ +import { Editor, TLShapeId } from "tldraw" +import { jsPDF } from "jspdf" +import { exportToBlob } from "tldraw" + +export const saveToPdf = async (editor: Editor) => { + const selectedIds = editor.getSelectedShapeIds() + if (selectedIds.length === 0) return + + try { + // Get common bounds of selected shapes + const selectionBounds = editor.getSelectionPageBounds() + if (!selectionBounds) return + + // Get blob using the editor's export functionality + const blob = await exportToBlob({ + editor, + ids: selectedIds, + format: "svg", + opts: { + scale: 2, + background: true, + padding: 10, + preserveAspectRatio: "xMidYMid slice", + }, + }) + + if (!blob) return + + // Convert blob to data URL + const url = URL.createObjectURL(blob) + + // Create image from blob + const img = new Image() + img.src = url + + await new Promise((resolve, reject) => { + img.onload = resolve + img.onerror = reject + }) + + // Create PDF with proper dimensions + const pdf = new jsPDF({ + orientation: selectionBounds.width > selectionBounds.height ? "l" : "p", + unit: "px", + format: [selectionBounds.width, selectionBounds.height], + }) + + // Add the image to the PDF + pdf.addImage( + img, + "SVG", + 0, + 0, + selectionBounds.width, + selectionBounds.height, + ) + pdf.save("canvas-selection.pdf") + + // Cleanup + URL.revokeObjectURL(url) + } catch (error) { + console.error("Failed to generate PDF:", error) + alert("Failed to generate PDF. Please try again.") + } +}