();
+ const pdfImageShapes: any[] = [];
+ for (const shape of allShapes) {
+ if (shape.type === 'image' && (shape.props as any).assetId) {
+ const asset = editor.getAsset((shape.props as any).assetId);
+ if (asset && (asset.meta as any)?.isPdfAsset) {
+ pdfAssetIds.add(asset.id);
+ pdfImageShapes.push(shape);
+ }
+ }
+ }
+
+ if (pdfAssetIds.size === 0) {
+ return;
+ }
+
+ // Prompt for new DPI
+ const currentDpi = (() => {
+ const firstAssetId = Array.from(pdfAssetIds)[0];
+ const asset = editor.getAsset(firstAssetId as any);
+ return (asset?.meta as any)?.dpi || 150;
+ })();
+
+ // Create a simple input dialog using DOM (Electron doesn't support prompt())
+ const container = editor.getContainer();
+ // Check Obsidian theme (body.theme-dark), not Tldraw theme
+ const isDark = container.ownerDocument.body.classList.contains('theme-dark');
+
+ const overlay = document.createElement('div');
+ overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10000';
+
+ // Theme-aware colors
+ const bgColor = isDark ? '#2d2d2d' : '#ffffff';
+ const textColor = isDark ? '#fff' : '#1a1a1a';
+ const mutedColor = isDark ? '#aaa' : '#666';
+ const inputBg = isDark ? '#1a1a1a' : '#f5f5f5';
+ const inputBorder = isDark ? '#555' : '#ccc';
+ const btnBg = isDark ? '#444' : '#e0e0e0';
+ const btnText = isDark ? '#fff' : '#333';
+
+ const dialog = document.createElement('div');
+ dialog.style.cssText = `background:${bgColor};color:${textColor};padding:20px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:300px;font-family:system-ui,-apple-system,sans-serif`;
+ dialog.innerHTML = `
+ Change PDF Quality (DPI)
+ Higher = sharper but slower (72-300)
+
+
+
+
+
+ `;
+
+ overlay.appendChild(dialog);
+ container.ownerDocument.body.appendChild(overlay);
+
+ const input = dialog.querySelector('input') as HTMLInputElement;
+ input.focus();
+ input.select();
+
+ const cleanup = () => overlay.remove();
+
+ const apply = () => {
+ const newDpi = Math.min(300, Math.max(72, parseInt(input.value) || 150));
+ cleanup();
+
+ // Update all selected PDF assets
+ editor.run(() => {
+ for (const assetId of pdfAssetIds) {
+ const asset = editor.getAsset(assetId as any);
+ if (asset) {
+ editor.updateAssets([{
+ ...asset,
+ meta: { ...asset.meta, dpi: newDpi }
+ }]);
+ }
+ }
+ });
+
+ // Force re-render by nudging shapes slightly
+ editor.run(() => {
+ for (const shape of pdfImageShapes) {
+ editor.updateShape({ id: shape.id, type: shape.type, x: shape.x + 0.001 });
+ editor.updateShape({ id: shape.id, type: shape.type, x: shape.x });
+ }
+ });
+ };
+
+ dialog.querySelector('.cancel-btn')?.addEventListener('click', cleanup);
+ dialog.querySelector('.ok-btn')?.addEventListener('click', apply);
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanup(); });
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') apply();
+ if (e.key === 'Escape') cleanup();
+ });
+ },
+ }
+
return actions;
},
- // toolbar(editor, toolbar, { tools }) {
- // // console.log(toolbar);
- // // toolbar.splice(4, 0, toolbarItem(tools.card))
- // return toolbar;
- // },
- // keyboardShortcutsMenu(editor, keyboardShortcutsMenu, { tools }) {
- // // console.log(keyboardShortcutsMenu);
- // // const toolsGroup = keyboardShortcutsMenu.find(
- // // (group) => group.id === 'shortcuts-dialog.tools'
- // // ) as TLUiMenuGroup
- // // toolsGroup.children.push(menuItem(tools.card))
- // return keyboardShortcutsMenu;
- // },
- // contextMenu(editor, schema, helpers) {
- // // console.log({ schema });
- // // console.log(JSON.stringify(schema[0]));
- // return schema;
- // },
+ contextMenu(editor, schema, helpers) {
+ // Helper to recursively get all descendant shapes
+ function getAllDescendants(shapes: any[]): any[] {
+ const result: any[] = [];
+ for (const shape of shapes) {
+ result.push(shape);
+ if (shape.type === 'group') {
+ const children = editor.getSortedChildIdsForParent(shape.id)
+ .map((id: any) => editor.getShape(id))
+ .filter(Boolean);
+ result.push(...getAllDescendants(children));
+ }
+ }
+ return result;
+ }
+
+ // Add PDF DPI option if a PDF shape is selected (or inside a selected group)
+ const selectedShapes = editor.getSelectedShapes();
+ const allShapes = getAllDescendants(selectedShapes);
+
+ console.log('[PDF Menu Debug] Selected shapes:', selectedShapes.length, selectedShapes.map((s: any) => ({ id: s.id, type: s.type })));
+ console.log('[PDF Menu Debug] All shapes (including children):', allShapes.length);
+
+ const hasPdfSelected = allShapes.some((shape: any) => {
+ if (shape.type !== 'image') return false;
+ const assetId = (shape.props as any).assetId;
+ const asset = editor.getAsset(assetId);
+ console.log('[PDF Menu Debug] Image shape:', shape.id, 'assetId:', assetId, 'asset:', asset, 'meta:', asset?.meta);
+ return asset && (asset.meta as any)?.isPdfAsset;
+ });
+
+ console.log('[PDF Menu Debug] hasPdfSelected:', hasPdfSelected);
+
+ if (hasPdfSelected) {
+ // Add to the beginning of context menu
+ schema.unshift({
+ id: 'pdf-options',
+ type: 'group',
+ children: [
+ { id: 'change-pdf-dpi', type: 'item' }
+ ]
+ });
+ }
+
+ return schema;
+ },
+ actionsMenu(editor, schema, helpers) {
+ // Helper to recursively get all descendant shapes
+ function getAllDescendants(shapes: any[]): any[] {
+ const result: any[] = [];
+ for (const shape of shapes) {
+ result.push(shape);
+ if (shape.type === 'group') {
+ const children = editor.getSortedChildIdsForParent(shape.id)
+ .map((id: any) => editor.getShape(id))
+ .filter(Boolean);
+ result.push(...getAllDescendants(children));
+ }
+ }
+ return result;
+ }
+
+ // Add PDF DPI option if a PDF shape is selected
+ const selectedShapes = editor.getSelectedShapes();
+ const allShapes = getAllDescendants(selectedShapes);
+
+ const hasPdfSelected = allShapes.some((shape: any) => {
+ if (shape.type !== 'image') return false;
+ const asset = editor.getAsset((shape.props as any).assetId);
+ return asset && (asset.meta as any)?.isPdfAsset;
+ });
+
+ if (hasPdfSelected) {
+ // Add to menu
+ schema.push({
+ id: 'pdf-options',
+ type: 'group',
+ children: [
+ { id: 'change-pdf-dpi', type: 'item' }
+ ]
+ });
+ }
+
+ return schema;
+ },
}
}
diff --git a/src/utils/migrate/tl-data-to-tlstore.ts b/src/utils/migrate/tl-data-to-tlstore.ts
index b76d075..eca370c 100644
--- a/src/utils/migrate/tl-data-to-tlstore.ts
+++ b/src/utils/migrate/tl-data-to-tlstore.ts
@@ -1,5 +1,6 @@
-import { TldrawFile, TLStore, parseTldrawJsonFile, createTLSchema, JsonObject, UnknownRecord } from "tldraw";
+import { TldrawFile, TLStore, parseTldrawJsonFile, createTLSchema, JsonObject, UnknownRecord, defaultShapeUtils } from "tldraw";
import { TLData } from "../document";
+import { PdfPageShapeUtil } from "src/tldraw/shapes/PdfPageShapeUtil";
/**
* Tldraw handles the migration here.
@@ -9,7 +10,9 @@ import { TLData } from "../document";
export function migrateTldrawFileDataIfNecessary(tldrawFileData: string | TldrawFile): TLStore {
const res = parseTldrawJsonFile(
{
- schema: createTLSchema(),
+ schema: createTLSchema({
+ shapeUtils: [...defaultShapeUtils, PdfPageShapeUtil]
+ }),
json: typeof tldrawFileData === 'string'
? tldrawFileData
: JSON.stringify(tldrawFileData)
@@ -25,7 +28,7 @@ export function migrateTldrawFileDataIfNecessary(tldrawFileData: string | Tldraw
function isJsonUnknownRecord(json: JsonObject): json is JsonObject & UnknownRecord {
const { id, typeName } = json as Partial;
- if(id === undefined || typeName === undefined) return false;
+ if (id === undefined || typeName === undefined) return false;
return true;
}
@@ -37,7 +40,7 @@ function isJsonUnknownRecord(json: JsonObject): json is JsonObject & UnknownReco
export function tLDataToTLStore(tldata: TLData): TLStore {
const { tldrawFileFormatVersion, schema, records } = tldata.raw as Partial;
- if(tldrawFileFormatVersion && schema && records) {
+ if (tldrawFileFormatVersion && schema && records) {
return migrateTldrawFileDataIfNecessary({
tldrawFileFormatVersion, schema, records
})
@@ -48,15 +51,15 @@ export function tLDataToTLStore(tldata: TLData): TLStore {
}
const oldRecords = Object.values(tldata.raw ?? {})
- .filter((e) => e !== undefined && e !== null)
- .filter((e) => typeof e === 'object')
- .filter((e): e is JsonObject => !Array.isArray(e))
- .map((e) => {
- if(!isJsonUnknownRecord(e)) {
- throw new Error(`Invalid json object found while parsing: ${e}`)
- }
- return e;
- });
+ .filter((e) => e !== undefined && e !== null)
+ .filter((e) => typeof e === 'object')
+ .filter((e): e is JsonObject => !Array.isArray(e))
+ .map((e) => {
+ if (!isJsonUnknownRecord(e)) {
+ throw new Error(`Invalid json object found while parsing: ${e}`)
+ }
+ return e;
+ });
/**
* tldrawFileFormatVersion and schema were obtained by exporting a tldr file using tldraw version 2.1.4 and extracting the values.
diff --git a/src/utils/migration.ts b/src/utils/migration.ts
new file mode 100644
index 0000000..f9efdff
--- /dev/null
+++ b/src/utils/migration.ts
@@ -0,0 +1,87 @@
+import { Editor, createShapeId } from "tldraw";
+
+/**
+ * Upgrades legacy PDF 'image' shapes to new 'pdf-page' shapes.
+ * Keeps position, size, and rotation.
+ * Deletes the old image shape and its associated asset.
+ */
+export function upgradeLegacyPdfShapes(editor: Editor) {
+ // We only run this once per session ideally, or efficiently.
+ // Scan all shapes.
+
+ const shapesToUpgrade: any[] = [];
+ const assetsToDelete: any[] = [];
+ const shapesToDelete: any[] = [];
+
+ const shapes = editor.getCurrentPageShapes(); // Or all pages? Better just current page to be safe/fast initially.
+ // actually better to check all shapes in store if possible, but store.allRecords() is safer.
+
+ const allShapes = editor.store.allRecords().filter(r => r.typeName === 'shape' && r.type === 'image');
+
+ for (const shape of allShapes as any[]) {
+ if (shape.meta?.isPdfPage && shape.meta?.pdfPath) {
+ // Found a candidate
+ shapesToUpgrade.push(shape);
+ }
+ }
+
+ if (shapesToUpgrade.length === 0) return;
+
+ console.log(`[Migration] Upgrading ${shapesToUpgrade.length} legacy PDF shapes...`);
+
+ editor.run(() => {
+ const newShapes: any[] = [];
+
+ for (const oldShape of shapesToUpgrade) {
+ // Create new shape
+ // We use a new ID to avoid type conflicts during swap,
+ // but we could try to reuse ID if we delete first.
+ // Reuse ID is better for bindings (arrows).
+ const id = oldShape.id;
+ const assetId = oldShape.props.assetId;
+
+ // Prepare new shape record
+ const newShape = {
+ id,
+ type: 'pdf-page',
+ x: oldShape.x,
+ y: oldShape.y,
+ rotation: oldShape.rotation,
+ index: oldShape.index,
+ parentId: oldShape.parentId,
+ opacity: oldShape.opacity,
+ isLocked: oldShape.isLocked,
+ props: {
+ pdfPath: oldShape.meta.pdfPath,
+ pageNumber: oldShape.meta.pageNumber,
+ w: oldShape.props.w,
+ h: oldShape.props.h,
+ },
+ meta: {
+ ...oldShape.meta,
+ isLegacyUpgraded: true,
+ }
+ };
+
+ newShapes.push(newShape);
+
+ if (assetId) {
+ assetsToDelete.push(assetId);
+ }
+ shapesToDelete.push(id);
+ }
+
+ // Delete old shapes first (to free up IDs)
+ editor.deleteShapes(shapesToDelete);
+
+ // Create new shapes (reusing IDs)
+ editor.createShapes(newShapes);
+
+ // Cleanup assets
+ if (assetsToDelete.length > 0) {
+ editor.deleteAssets(assetsToDelete);
+ }
+ });
+
+ console.log(`[Migration] Upgrade complete. Removed ${assetsToDelete.length} obsolete assets.`);
+}
diff --git a/src/utils/tldraw-file/index.ts b/src/utils/tldraw-file/index.ts
index af23b57..9881267 100644
--- a/src/utils/tldraw-file/index.ts
+++ b/src/utils/tldraw-file/index.ts
@@ -1,4 +1,5 @@
-import { createTLStore, TldrawFile, TLStore } from "tldraw"
+import { createTLStore, TldrawFile, TLStore, defaultShapeUtils } from "tldraw"
+import { PdfPageShapeUtil } from "src/tldraw/shapes/PdfPageShapeUtil";
/**
*
@@ -6,7 +7,9 @@ import { createTLStore, TldrawFile, TLStore } from "tldraw"
* @returns
*/
export function createRawTldrawFile(store?: TLStore): TldrawFile {
- store ??= createTLStore();
+ store ??= createTLStore({
+ shapeUtils: [...defaultShapeUtils, PdfPageShapeUtil]
+ });
return {
tldrawFileFormatVersion: 1,
schema: store.schema.serialize(),