diff --git a/apps/examples/src/examples/selection-ui/README.md b/apps/examples/src/examples/selection-ui/README.md
new file mode 100644
index 000000000..e3718ced8
--- /dev/null
+++ b/apps/examples/src/examples/selection-ui/README.md
@@ -0,0 +1,9 @@
+---
+title: Selection UI
+component: ./SelectionUiExample.tsx
+category: shapes/tools
+---
+
+---
+
+You can use the `InFrontOfTheCanvas` component to show extra user interface elements around the user's selection.
diff --git a/apps/examples/src/examples/selection-ui/SelectionUiExample.tsx b/apps/examples/src/examples/selection-ui/SelectionUiExample.tsx
new file mode 100644
index 000000000..905a7873f
--- /dev/null
+++ b/apps/examples/src/examples/selection-ui/SelectionUiExample.tsx
@@ -0,0 +1,135 @@
+import {
+ TLComponents,
+ Tldraw,
+ Vec,
+ intersectLineSegmentPolygon,
+ stopEventPropagation,
+ useEditor,
+ useValue,
+} from 'tldraw'
+import 'tldraw/tldraw.css'
+
+const components: TLComponents = {
+ InFrontOfTheCanvas: () => {
+ const editor = useEditor()
+
+ const info = useValue(
+ 'selection bounds',
+ () => {
+ const screenBounds = editor.getViewportScreenBounds()
+ const rotation = editor.getSelectionRotation()
+ const rotatedScreenBounds = editor.getSelectionRotatedScreenBounds()
+ if (!rotatedScreenBounds) return
+ return {
+ // we really want the position within the
+ // tldraw component's bounds, not the screen itself
+ x: rotatedScreenBounds.x - screenBounds.x,
+ y: rotatedScreenBounds.y - screenBounds.y,
+ width: rotatedScreenBounds.width,
+ height: rotatedScreenBounds.height,
+ rotation: rotation,
+ }
+ },
+ [editor]
+ )
+
+ if (!info) return
+
+ return (
+
+
+
+
+
+
+ )
+ },
+}
+
+export default function BasicExample() {
+ return (
+
+
+
+ )
+}
+
+/**
+ * This button will duplicate the editor's current selected shapes in
+ * a certain direction. Its rotation determines the appearance of the
+ * button (its actual css rotation) as well as the direction in which
+ * the duplicated shapes are offset from the original shapes. It's
+ * zeroed to the right.
+ */
+function DuplicateInDirectionButton({
+ x,
+ y,
+ rotation,
+}: {
+ x: number
+ y: number
+ rotation: number
+}) {
+ const editor = useEditor()
+
+ return (
+
+ )
+}
diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md
index ae3f7136b..e52e7db5a 100644
--- a/packages/editor/api-report.md
+++ b/packages/editor/api-report.md
@@ -726,6 +726,7 @@ export class Editor extends EventEmitter {
getSelectedShapes(): TLShape[];
getSelectionPageBounds(): Box | null;
getSelectionRotatedPageBounds(): Box | undefined;
+ getSelectionRotatedScreenBounds(): Box | undefined;
getSelectionRotation(): number;
getShape(shape: TLParentId | TLShape): T | undefined;
getShapeAncestors(shape: TLShape | TLShapeId, acc?: TLShape[]): TLShape[];
diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json
index 063898148..2d4099197 100644
--- a/packages/editor/api/api.json
+++ b/packages/editor/api/api.json
@@ -12182,6 +12182,42 @@
"isAbstract": false,
"name": "getSelectionRotatedPageBounds"
},
+ {
+ "kind": "Method",
+ "canonicalReference": "@tldraw/editor!Editor#getSelectionRotatedScreenBounds:member(1)",
+ "docComment": "/**\n * The bounds of the selection bounding box in the current page space.\n *\n * @readonly @public\n */\n",
+ "excerptTokens": [
+ {
+ "kind": "Content",
+ "text": "getSelectionRotatedScreenBounds(): "
+ },
+ {
+ "kind": "Reference",
+ "text": "Box",
+ "canonicalReference": "@tldraw/editor!Box:class"
+ },
+ {
+ "kind": "Content",
+ "text": " | undefined"
+ },
+ {
+ "kind": "Content",
+ "text": ";"
+ }
+ ],
+ "isStatic": false,
+ "returnTypeTokenRange": {
+ "startIndex": 1,
+ "endIndex": 3
+ },
+ "releaseTag": "Public",
+ "isProtected": false,
+ "overloadIndex": 1,
+ "parameters": [],
+ "isOptional": false,
+ "isAbstract": false,
+ "name": "getSelectionRotatedScreenBounds"
+ },
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getSelectionRotation:member(1)",
diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts
index cd30560cf..6bebbc141 100644
--- a/packages/editor/src/lib/editor/Editor.ts
+++ b/packages/editor/src/lib/editor/Editor.ts
@@ -1661,6 +1661,7 @@ export class Editor extends EventEmitter {
}
return 0
}
+
/**
* The bounds of the selection bounding box in the current page space.
*
@@ -1701,6 +1702,20 @@ export class Editor extends EventEmitter {
return boxFromRotatedVertices
}
+ /**
+ * The bounds of the selection bounding box in the current page space.
+ *
+ * @readonly
+ * @public
+ */
+ @computed getSelectionRotatedScreenBounds(): Box | undefined {
+ const bounds = this.getSelectionRotatedPageBounds()
+ if (!bounds) return undefined
+ const { x, y } = this.pageToScreen(bounds.point)
+ const zoom = this.getZoomLevel()
+ return new Box(x, y, bounds.width * zoom, bounds.height * zoom)
+ }
+
// Focus Group
/**
@@ -2848,7 +2863,7 @@ export class Editor extends EventEmitter {
* @public
*/
pageToScreen(point: VecLike) {
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
+ const screenBounds = this.getViewportScreenBounds()
const { x: cx, y: cy, z: cz = 1 } = this.getCamera()
return {