PrintToPDF integration

This commit is contained in:
Jeff Emmett 2024-12-08 13:31:53 -05:00
parent 0ff9c64908
commit ce50026cc3
7 changed files with 117 additions and 10 deletions

View File

@ -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",

View File

@ -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(<App />)
//createRoot(document.getElementById("root")!).render(<App />)
function App() {
return (
// <React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<Default />} />
@ -60,6 +59,7 @@ function App() {
<Route path="/inbox" element={<Inbox />} />
</Routes>
</BrowserRouter>
// </React.StrictMode>
)
}
createRoot(document.getElementById("root")!).render(<App />)

View File

@ -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)}
/>
<TldrawUiMenuItem
id="save-to-pdf"
label="Save Selection as PDF"
icon="file"
kbd="alt+p"
disabled={!hasSelection}
onSelect={() => saveToPdf(editor)}
/>
</TldrawUiMenuGroup>
{/* Creation Tools Group */}

View File

@ -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 (
<DefaultToolbar>

View File

@ -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,
},
}
},
}

View File

@ -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))
}
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)
}
}

65
src/utils/pdfUtils.ts Normal file
View File

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