PrintToPDF integration
This commit is contained in:
parent
5d17bf7795
commit
10c191212c
|
|
@ -20,11 +20,14 @@
|
||||||
"@tldraw/sync-core": "^3.6.0",
|
"@tldraw/sync-core": "^3.6.0",
|
||||||
"@tldraw/tldraw": "^3.6.0",
|
"@tldraw/tldraw": "^3.6.0",
|
||||||
"@tldraw/tlschema": "^3.6.0",
|
"@tldraw/tlschema": "^3.6.0",
|
||||||
|
"@types/jspdf": "^2.0.0",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
"cloudflare-workers-unfurl": "^0.0.7",
|
"cloudflare-workers-unfurl": "^0.0.7",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"itty-router": "^5.0.17",
|
"itty-router": "^5.0.17",
|
||||||
|
"jspdf": "^2.5.2",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { EmbedShape } from "./shapes/EmbedShapeUtil"
|
||||||
import { MarkdownShape } from "./shapes/MarkdownShapeUtil"
|
import { MarkdownShape } from "./shapes/MarkdownShapeUtil"
|
||||||
import { MarkdownTool } from "./tools/MarkdownTool"
|
import { MarkdownTool } from "./tools/MarkdownTool"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad"
|
import { handleInitialPageLoad } from "./utils/handleInitialPageLoad"
|
||||||
|
|
||||||
inject()
|
inject()
|
||||||
|
|
||||||
|
|
@ -47,11 +47,10 @@ export default function InteractiveShapeExample() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(<App />)
|
//createRoot(document.getElementById("root")!).render(<App />)
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
// <React.StrictMode>
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Default />} />
|
<Route path="/" element={<Default />} />
|
||||||
|
|
@ -60,6 +59,7 @@ function App() {
|
||||||
<Route path="/inbox" element={<Inbox />} />
|
<Route path="/inbox" element={<Inbox />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
// </React.StrictMode>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<App />)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
zoomToSelection,
|
zoomToSelection,
|
||||||
} from "./cameraUtils"
|
} from "./cameraUtils"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { saveToPdf } from "../utils/pdfUtils"
|
||||||
|
|
||||||
export function CustomContextMenu(props: TLUiContextMenuProps) {
|
export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
@ -71,6 +72,14 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
disabled={!hasCameraHistory}
|
disabled={!hasCameraHistory}
|
||||||
onSelect={() => revertCamera(editor)}
|
onSelect={() => revertCamera(editor)}
|
||||||
/>
|
/>
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
id="save-to-pdf"
|
||||||
|
label="Save Selection as PDF"
|
||||||
|
icon="file"
|
||||||
|
kbd="alt+p"
|
||||||
|
disabled={!hasSelection}
|
||||||
|
onSelect={() => saveToPdf(editor)}
|
||||||
|
/>
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
||||||
{/* Creation Tools Group */}
|
{/* Creation Tools Group */}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,20 @@ import { TldrawUiMenuItem } from "tldraw"
|
||||||
import { DefaultToolbar, DefaultToolbarContent } from "tldraw"
|
import { DefaultToolbar, DefaultToolbarContent } from "tldraw"
|
||||||
import { useTools } from "tldraw"
|
import { useTools } from "tldraw"
|
||||||
import { useEditor } from "tldraw"
|
import { useEditor } from "tldraw"
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
|
||||||
export function CustomToolbar() {
|
export function CustomToolbar() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
|
const [isReady, setIsReady] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && tools) {
|
||||||
|
setIsReady(true)
|
||||||
|
}
|
||||||
|
}, [editor, tools])
|
||||||
|
|
||||||
|
if (!isReady) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultToolbar>
|
<DefaultToolbar>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
revertCamera,
|
revertCamera,
|
||||||
zoomToSelection,
|
zoomToSelection,
|
||||||
} from "./cameraUtils"
|
} from "./cameraUtils"
|
||||||
|
import { saveToPdf } from "../utils/pdfUtils"
|
||||||
|
|
||||||
export const overrides: TLUiOverrides = {
|
export const overrides: TLUiOverrides = {
|
||||||
tools(editor, tools) {
|
tools(editor, tools) {
|
||||||
|
|
@ -85,6 +86,17 @@ export const overrides: TLUiOverrides = {
|
||||||
kbd: "shift+l",
|
kbd: "shift+l",
|
||||||
onSelect: () => lockCameraToFrame(editor),
|
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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { Editor } from "tldraw"
|
import { Editor, TLEventMap, TLInstancePresence } from "tldraw"
|
||||||
|
|
||||||
export const handleInitialPageLoad = (editor: Editor) => {
|
export const handleInitialPageLoad = async (editor: Editor) => {
|
||||||
if (!editor.store || !editor.getInstanceState().isFocused) {
|
// Wait for editor to be ready
|
||||||
setTimeout(() => handleInitialPageLoad(editor), 100)
|
while (!editor.store || !editor.getInstanceState().isFocused) {
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue