diff --git a/assets/translations/main.json b/assets/translations/main.json
index 782c24d28..ed227ce0c 100644
--- a/assets/translations/main.json
+++ b/assets/translations/main.json
@@ -96,6 +96,7 @@
"action.toggle-grid.menu": "Show grid",
"action.toggle-grid": "Toggle grid",
"action.toggle-lock": "Toggle locked",
+ "action.flatten-to-image": "Flatten",
"action.toggle-snap-mode.menu": "Always snap",
"action.toggle-snap-mode": "Toggle always snap",
"action.toggle-tool-lock.menu": "Tool lock",
@@ -232,6 +233,7 @@
"menu.language": "Language",
"menu.preferences": "Preferences",
"menu.view": "View",
+ "context-menu.edit": "Edit",
"context-menu.arrange": "Arrange",
"context-menu.copy-as": "Copy as",
"context-menu.export-as": "Export as",
diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md
index ec071d1ad..1420a5202 100644
--- a/packages/editor/api-report.md
+++ b/packages/editor/api-report.md
@@ -681,6 +681,8 @@ export const defaultTldrawOptions: {
readonly dragDistanceSquared: 16;
readonly edgeScrollDistance: 8;
readonly edgeScrollSpeed: 20;
+ readonly flattenImageBoundsExpand: 64;
+ readonly flattenImageBoundsPadding: 16;
readonly followChaseViewportSnap: 2;
readonly gridSteps: readonly [{
readonly mid: 0.15;
@@ -2571,6 +2573,10 @@ export interface TldrawOptions {
// (undocumented)
readonly edgeScrollSpeed: number;
// (undocumented)
+ readonly flattenImageBoundsExpand: number;
+ // (undocumented)
+ readonly flattenImageBoundsPadding: number;
+ // (undocumented)
readonly followChaseViewportSnap: number;
// (undocumented)
readonly gridSteps: readonly {
diff --git a/packages/editor/src/lib/options.ts b/packages/editor/src/lib/options.ts
index fbc4f6b47..971ba8670 100644
--- a/packages/editor/src/lib/options.ts
+++ b/packages/editor/src/lib/options.ts
@@ -45,6 +45,8 @@ export interface TldrawOptions {
readonly longPressDurationMs: number
readonly textShadowLod: number
readonly adjacentShapeMargin: number
+ readonly flattenImageBoundsExpand: number
+ readonly flattenImageBoundsPadding: number
}
/** @public */
@@ -79,4 +81,6 @@ export const defaultTldrawOptions = {
longPressDurationMs: 500,
textShadowLod: 0.35,
adjacentShapeMargin: 10,
+ flattenImageBoundsExpand: 64,
+ flattenImageBoundsPadding: 16,
} as const satisfies TldrawOptions
diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md
index 8b0a5d380..0962a3756 100644
--- a/packages/tldraw/api-report.md
+++ b/packages/tldraw/api-report.md
@@ -2221,6 +2221,8 @@ export interface TLUiEventMap {
// (undocumented)
'fit-frame-to-content': null;
// (undocumented)
+ 'flatten-to-image': null;
+ // (undocumented)
'flip-shapes': {
operation: 'horizontal' | 'vertical';
};
@@ -2684,7 +2686,7 @@ export interface TLUiTranslation {
export type TLUiTranslationContextType = TLUiTranslation;
// @public (undocumented)
-export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-all-as-json.short' | 'action.export-all-as-json' | 'action.export-all-as-png.short' | 'action.export-all-as-png' | 'action.export-all-as-svg.short' | 'action.export-all-as-svg' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project-on-tldraw' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rename' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-edge-scrolling.menu' | 'action.toggle-edge-scrolling' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.toggle-wrap-mode.menu' | 'action.toggle-wrap-mode' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'assets.files.upload-failed' | 'assets.url.failed' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.white' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-all-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'document.default-name' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copied' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.creating-project' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'verticalAlign-style.end' | 'verticalAlign-style.middle' | 'verticalAlign-style.start' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open';
+export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-all-as-json.short' | 'action.export-all-as-json' | 'action.export-all-as-png.short' | 'action.export-all-as-png' | 'action.export-all-as-svg.short' | 'action.export-all-as-svg' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flatten-to-image' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project-on-tldraw' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rename' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-edge-scrolling.menu' | 'action.toggle-edge-scrolling' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.toggle-wrap-mode.menu' | 'action.toggle-wrap-mode' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'assets.files.upload-failed' | 'assets.url.failed' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.white' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.edit' | 'context-menu.export-all-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'document.default-name' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copied' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.creating-project' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'verticalAlign-style.end' | 'verticalAlign-style.middle' | 'verticalAlign-style.start' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open';
// @public (undocumented)
export interface TLUiTranslationProviderProps {
diff --git a/packages/tldraw/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx b/packages/tldraw/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx
index 6dc90182c..65c982a5d 100644
--- a/packages/tldraw/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx
+++ b/packages/tldraw/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx
@@ -3,18 +3,10 @@ import {
ArrangeMenuSubmenu,
ClipboardMenuGroup,
ConversionsMenuGroup,
- ConvertToBookmarkMenuItem,
- ConvertToEmbedMenuItem,
- EditLinkMenuItem,
- FitFrameToContentMenuItem,
- GroupMenuItem,
+ EditMenuSubmenu,
MoveToPageMenu,
- RemoveFrameMenuItem,
ReorderMenuSubmenu,
SelectAllMenuItem,
- ToggleAutoSizeMenuItem,
- ToggleLockMenuItem,
- UngroupMenuItem,
} from '../menu-items'
import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
@@ -32,18 +24,8 @@ export function DefaultContextMenuContent() {
return (
<>
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx b/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx
index 1f9005c97..2a235cbf7 100644
--- a/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx
+++ b/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx
@@ -9,6 +9,7 @@ import {
ConvertToEmbedMenuItem,
EditLinkMenuItem,
FitFrameToContentMenuItem,
+ FlattenMenuItem,
GroupMenuItem,
RemoveFrameMenuItem,
SelectAllMenuItem,
@@ -101,6 +102,7 @@ export function MiscMenuGroup() {
+
)
}
diff --git a/packages/tldraw/src/lib/ui/components/menu-items.tsx b/packages/tldraw/src/lib/ui/components/menu-items.tsx
index e4680768e..21b2fdc3d 100644
--- a/packages/tldraw/src/lib/ui/components/menu-items.tsx
+++ b/packages/tldraw/src/lib/ui/components/menu-items.tsx
@@ -2,6 +2,7 @@ import {
TLBookmarkShape,
TLEmbedShape,
TLFrameShape,
+ TLImageShape,
TLPageId,
useEditor,
useValue,
@@ -51,6 +52,27 @@ export function DuplicateMenuItem() {
return
}
/** @public @react */
+export function FlattenMenuItem() {
+ const actions = useActions()
+ const editor = useEditor()
+ const shouldDisplay = useValue(
+ 'should display flatten option',
+ () => {
+ const selectedShapeIds = editor.getSelectedShapeIds()
+ if (selectedShapeIds.length === 0) return false
+ const onlySelectedShape = editor.getOnlySelectedShape()
+ if (onlySelectedShape && editor.isShapeOfType(onlySelectedShape, 'image')) {
+ return false
+ }
+ return true
+ },
+ [editor]
+ )
+ if (!shouldDisplay) return null
+
+ return
+}
+/** @public @react */
export function GroupMenuItem() {
const actions = useActions()
const shouldDisplay = useAllowGroup()
@@ -305,6 +327,25 @@ export function DeleteMenuItem() {
}
/* --------------------- Modify --------------------- */
+
+/** @public @react */
+export function EditMenuSubmenu() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
/** @public @react */
export function ArrangeMenuSubmenu() {
const twoSelected = useUnlockedSelectedShapesCount(2)
diff --git a/packages/tldraw/src/lib/ui/context/actions.tsx b/packages/tldraw/src/lib/ui/context/actions.tsx
index 635fdb96d..ac8759ea5 100644
--- a/packages/tldraw/src/lib/ui/context/actions.tsx
+++ b/packages/tldraw/src/lib/ui/context/actions.tsx
@@ -27,6 +27,7 @@ import { EmbedDialog } from '../components/EmbedDialog'
import { useMenuClipboardEvents } from '../hooks/useClipboardEvents'
import { useCopyAs } from '../hooks/useCopyAs'
import { useExportAs } from '../hooks/useExportAs'
+import { flattenShapesToImages } from '../hooks/useFlatten'
import { useInsertMedia } from '../hooks/useInsertMedia'
import { usePrint } from '../hooks/usePrint'
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
@@ -1341,6 +1342,28 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
trackEvent('set-style', { source, id: style.id, value: 'white' })
},
},
+ {
+ id: 'flatten-to-image',
+ label: 'action.flatten-to-image',
+ kbd: '!f',
+ onSelect: async (source) => {
+ const ids = editor.getSelectedShapeIds()
+ if (ids.length === 0) return
+
+ editor.mark('flattening to image')
+ trackEvent('flatten-to-image', { source })
+
+ const newShapeIds = await flattenShapesToImages(
+ editor,
+ ids,
+ editor.options.flattenImageBoundsExpand
+ )
+
+ if (newShapeIds?.length) {
+ editor.setSelectedShapes(newShapeIds)
+ }
+ },
+ },
]
const actions = makeActions(actionItems)
diff --git a/packages/tldraw/src/lib/ui/context/events.tsx b/packages/tldraw/src/lib/ui/context/events.tsx
index 7c948b020..35a427010 100644
--- a/packages/tldraw/src/lib/ui/context/events.tsx
+++ b/packages/tldraw/src/lib/ui/context/events.tsx
@@ -96,6 +96,7 @@ export interface TLUiEventMap {
'open-cursor-chat': null
'zoom-tool': null
'unlock-all': null
+ 'flatten-to-image': null
}
/** @public */
diff --git a/packages/tldraw/src/lib/ui/hooks/useFlatten.ts b/packages/tldraw/src/lib/ui/hooks/useFlatten.ts
new file mode 100644
index 000000000..ea8ca123b
--- /dev/null
+++ b/packages/tldraw/src/lib/ui/hooks/useFlatten.ts
@@ -0,0 +1,202 @@
+import {
+ AssetRecordType,
+ Box,
+ Editor,
+ IndexKey,
+ TLImageAsset,
+ TLImageShape,
+ TLShape,
+ TLShapeId,
+ Vec,
+ compact,
+ createShapeId,
+ isShapeId,
+ transact,
+ useEditor,
+} from '@tldraw/editor'
+import { useCallback } from 'react'
+
+export async function flattenShapesToImages(
+ editor: Editor,
+ shapeIds: TLShapeId[],
+ flattenImageBoundsExpand?: number
+) {
+ const shapes = compact(
+ shapeIds.map((id) => {
+ const shape = editor.getShape(id)
+ if (!shape) return
+ const util = editor.getShapeUtil(shape.type)
+ // skip shapes that don't have a toSvg method
+ if (util.toSvg === undefined) return
+ return shape
+ })
+ )
+
+ if (shapes.length === 0) return
+
+ // Don't flatten if it's just one image
+ if (shapes.length === 1) {
+ const shape = shapes[0]
+ if (!shape) return
+ if (editor.isShapeOfType(shape, 'image')) return
+ }
+
+ const groups: { shapes: TLShape[]; bounds: Box; asset?: TLImageAsset }[] = []
+
+ if (flattenImageBoundsExpand !== undefined) {
+ const expandedBounds = shapes.map((shape) => {
+ return {
+ shape,
+ bounds: editor.getShapeMaskedPageBounds(shape)!.clone().expandBy(flattenImageBoundsExpand),
+ }
+ })
+
+ for (let i = 0; i < expandedBounds.length; i++) {
+ const item = expandedBounds[i]
+ if (i === 0) {
+ groups[0] = {
+ shapes: [item.shape],
+ bounds: item.bounds,
+ }
+ continue
+ }
+
+ let didLand = false
+
+ for (const group of groups) {
+ if (group.bounds.includes(item.bounds)) {
+ group.shapes.push(item.shape)
+ group.bounds.expand(item.bounds)
+ didLand = true
+ break
+ }
+ }
+
+ if (!didLand) {
+ groups.push({
+ shapes: [item.shape],
+ bounds: item.bounds,
+ })
+ }
+ }
+ } else {
+ const bounds = Box.Common(shapes.map((shape) => editor.getShapeMaskedPageBounds(shape)!))
+ groups.push({
+ shapes,
+ bounds,
+ })
+ }
+
+ const padding = editor.options.flattenImageBoundsPadding
+
+ for (const group of groups) {
+ if (flattenImageBoundsExpand !== undefined) {
+ // shrink the bounds again, removing the expanded area
+ group.bounds.expandBy(-flattenImageBoundsExpand)
+ }
+
+ // get an image for the shapes
+ const svgResult = await editor.getSvgString(group.shapes, {
+ padding,
+ })
+ if (!svgResult?.svg) continue
+
+ // get an image asset for the image
+ const blob = new Blob([svgResult.svg], { type: 'image/svg+xml' })
+ const asset = (await editor.getAssetForExternalContent({
+ type: 'file',
+ file: new File([blob], 'asset.svg', { type: 'image/svg+xml' }),
+ })) as TLImageAsset
+ if (!asset) continue
+
+ // add it to the group
+ group.asset = asset
+ }
+
+ const createdShapeIds: TLShapeId[] = []
+
+ transact(() => {
+ for (const group of groups) {
+ const { asset, bounds, shapes } = group
+ if (!asset) continue
+
+ const assetId = AssetRecordType.createId()
+
+ const commonAncestorId = editor.findCommonAncestor(shapes) ?? editor.getCurrentPageId()
+ if (!commonAncestorId) continue
+
+ let index: IndexKey = 'a1' as IndexKey
+ for (const shape of shapes) {
+ if (shape.parentId === commonAncestorId) {
+ if (shape.index > index) {
+ index = shape.index
+ }
+ break
+ }
+ }
+
+ let x: number
+ let y: number
+ let rotation: number
+
+ if (isShapeId(commonAncestorId)) {
+ const commonAncestor = editor.getShape(commonAncestorId)
+ if (!commonAncestor) continue
+ // put the point in the parent's space
+ const point = editor.getPointInShapeSpace(commonAncestor, {
+ x: bounds.x,
+ y: bounds.y,
+ })
+ // get the parent's rotation
+ rotation = editor.getShapePageTransform(commonAncestorId).rotation()
+ // rotate the point against the parent's rotation
+ point.sub(new Vec(padding, padding).rot(-rotation))
+ x = point.x
+ y = point.y
+ } else {
+ // if the common ancestor is the page, then just adjust for the padding
+ x = bounds.x - padding
+ y = bounds.y - padding
+ rotation = 0
+ }
+
+ // delete the shapes
+ editor.deleteShapes(shapes)
+
+ // create the asset
+ editor.createAssets([{ ...asset, id: assetId }])
+
+ const shapeId = createShapeId()
+
+ // create an image shape in the same place as the shapes
+ editor.createShape({
+ id: shapeId,
+ type: 'image',
+ index,
+ parentId: commonAncestorId,
+ x,
+ y,
+ rotation: -rotation,
+ props: {
+ assetId,
+ w: bounds.w + padding * 2,
+ h: bounds.h + padding * 2,
+ },
+ })
+
+ createdShapeIds.push(shapeId)
+ }
+ })
+
+ return createdShapeIds
+}
+
+export function useFlatten() {
+ const editor = useEditor()
+ return useCallback(
+ (ids: TLShapeId[]) => {
+ return flattenShapesToImages(editor, ids)
+ },
+ [editor]
+ )
+}
diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts
index 6bcb41a72..e9cf03848 100644
--- a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts
+++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts
@@ -100,6 +100,7 @@ export type TLUiTranslationKey =
| 'action.toggle-grid.menu'
| 'action.toggle-grid'
| 'action.toggle-lock'
+ | 'action.flatten-to-image'
| 'action.toggle-snap-mode.menu'
| 'action.toggle-snap-mode'
| 'action.toggle-tool-lock.menu'
@@ -236,6 +237,7 @@ export type TLUiTranslationKey =
| 'menu.language'
| 'menu.preferences'
| 'menu.view'
+ | 'context-menu.edit'
| 'context-menu.arrange'
| 'context-menu.copy-as'
| 'context-menu.export-as'
diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts
index d1ac772a9..ea26d1db9 100644
--- a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts
+++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts
@@ -100,6 +100,7 @@ export const DEFAULT_TRANSLATION = {
'action.toggle-grid.menu': 'Show grid',
'action.toggle-grid': 'Toggle grid',
'action.toggle-lock': 'Toggle locked',
+ 'action.flatten-to-image': 'Flatten',
'action.toggle-snap-mode.menu': 'Always snap',
'action.toggle-snap-mode': 'Toggle always snap',
'action.toggle-tool-lock.menu': 'Tool lock',
@@ -236,6 +237,7 @@ export const DEFAULT_TRANSLATION = {
'menu.language': 'Language',
'menu.preferences': 'Preferences',
'menu.view': 'View',
+ 'context-menu.edit': 'Edit',
'context-menu.arrange': 'Arrange',
'context-menu.copy-as': 'Copy as',
'context-menu.export-as': 'Export as',