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.")
+ }
+}