pi-tldraw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/bridge/app-bridge-entry.js +6 -0
  4. package/mcp-app/LICENSE.md +9 -0
  5. package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
  6. package/mcp-app/README.md +129 -0
  7. package/mcp-app/dev-tunnel.sh +51 -0
  8. package/mcp-app/dist/editor-api.json +8493 -0
  9. package/mcp-app/dist/mcp-app.html +643 -0
  10. package/mcp-app/dist/method-map.json +915 -0
  11. package/mcp-app/package.json +42 -0
  12. package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
  13. package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
  14. package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
  15. package/mcp-app/scripts/extract-editor-api.ts +1374 -0
  16. package/mcp-app/server.json +21 -0
  17. package/mcp-app/src/logger.ts +45 -0
  18. package/mcp-app/src/register-tools.ts +368 -0
  19. package/mcp-app/src/shared/generated-data.ts +160 -0
  20. package/mcp-app/src/shared/pending-requests.ts +69 -0
  21. package/mcp-app/src/shared/types.ts +76 -0
  22. package/mcp-app/src/shared/utils.ts +132 -0
  23. package/mcp-app/src/tools/exec.ts +120 -0
  24. package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
  25. package/mcp-app/src/tools/search.ts +150 -0
  26. package/mcp-app/src/widget/app-context.tsx +29 -0
  27. package/mcp-app/src/widget/dev-log.tsx +70 -0
  28. package/mcp-app/src/widget/exec-helpers.ts +232 -0
  29. package/mcp-app/src/widget/export-tldr.ts +35 -0
  30. package/mcp-app/src/widget/focused/defaults.ts +141 -0
  31. package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
  32. package/mcp-app/src/widget/focused/format.ts +366 -0
  33. package/mcp-app/src/widget/focused/to-focused.ts +258 -0
  34. package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
  35. package/mcp-app/src/widget/image-guard.tsx +106 -0
  36. package/mcp-app/src/widget/index.html +33 -0
  37. package/mcp-app/src/widget/mcp-app.css +113 -0
  38. package/mcp-app/src/widget/mcp-app.tsx +857 -0
  39. package/mcp-app/src/widget/persistence.ts +337 -0
  40. package/mcp-app/src/widget/snapshot.ts +157 -0
  41. package/mcp-app/src/worker.ts +305 -0
  42. package/mcp-app/tsconfig.json +23 -0
  43. package/mcp-app/vite.config.ts +13 -0
  44. package/mcp-app/wrangler.toml +45 -0
  45. package/mcp-app-source.json +36 -0
  46. package/package.json +90 -0
  47. package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
  48. package/scripts/assemble-mcp-app.mjs +193 -0
  49. package/scripts/build-bridge.mjs +74 -0
  50. package/scripts/e2e-mcp.mjs +69 -0
  51. package/scripts/e2e-packaged-mcp-app.mjs +79 -0
  52. package/scripts/run-mcp-app-dev.mjs +44 -0
  53. package/scripts/verify-bundle.mjs +41 -0
  54. package/scripts/verify-mcp-app-source.mjs +51 -0
  55. package/scripts/verify-mcp-app.mjs +38 -0
  56. package/scripts/verify-package-files.mjs +50 -0
  57. package/src/canvas/export.ts +164 -0
  58. package/src/canvas/state.ts +117 -0
  59. package/src/canvas/workflow.ts +105 -0
  60. package/src/commands/tldraw-command.ts +48 -0
  61. package/src/diagram/guidance.ts +44 -0
  62. package/src/host/local-host.ts +289 -0
  63. package/src/index.ts +762 -0
  64. package/src/mcp/client.ts +126 -0
  65. package/src/mcp/response.ts +74 -0
  66. package/src/semantic/layer.ts +309 -0
  67. package/src/server/server-manager.ts +153 -0
  68. package/src/store/export-store.ts +33 -0
  69. package/src/store/project-store.ts +251 -0
  70. package/src/ui/tldraw-status.ts +88 -0
  71. package/static/app-bridge-bundle.js +18114 -0
  72. package/static/app-bridge-bundle.meta.json +164 -0
  73. package/static/host.html +390 -0
  74. package/tsconfig.json +13 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Focused shape format types and conversion utilities.
3
+ * Ported from tldraw/tldraw templates/agent/shared/format/
4
+ */
5
+ import { TLDefaultFillStyle, TLDefaultSizeStyle, TLGeoShapeGeoStyle, TLShapeId } from 'tldraw'
6
+
7
+ // ---- Colors ----
8
+
9
+ export const FOCUSED_COLORS = [
10
+ 'red',
11
+ 'light-red',
12
+ 'green',
13
+ 'light-green',
14
+ 'blue',
15
+ 'light-blue',
16
+ 'orange',
17
+ 'yellow',
18
+ 'black',
19
+ 'violet',
20
+ 'light-violet',
21
+ 'grey',
22
+ 'white',
23
+ ] as const
24
+
25
+ type FocusedColor = (typeof FOCUSED_COLORS)[number]
26
+
27
+ export function asColor(color: string): FocusedColor {
28
+ if (FOCUSED_COLORS.includes(color as FocusedColor)) {
29
+ return color as FocusedColor
30
+ }
31
+ switch (color) {
32
+ case 'pink':
33
+ case 'light-pink':
34
+ return 'light-violet'
35
+ }
36
+ return 'black'
37
+ }
38
+
39
+ // ---- Fill ----
40
+
41
+ type FocusedFill = 'none' | 'tint' | 'background' | 'solid' | 'pattern'
42
+
43
+ const FOCUSED_TO_SHAPE_FILLS: Record<FocusedFill, TLDefaultFillStyle> = {
44
+ none: 'none',
45
+ solid: 'lined-fill',
46
+ background: 'semi',
47
+ tint: 'solid',
48
+ pattern: 'pattern',
49
+ }
50
+
51
+ const SHAPE_TO_FOCUSED_FILLS: Record<TLDefaultFillStyle, FocusedFill> = {
52
+ none: 'none',
53
+ fill: 'solid',
54
+ 'lined-fill': 'solid',
55
+ semi: 'background',
56
+ solid: 'tint',
57
+ pattern: 'pattern',
58
+ }
59
+
60
+ export function convertFocusedFillToTldrawFill(fill: FocusedFill): TLDefaultFillStyle {
61
+ return FOCUSED_TO_SHAPE_FILLS[fill]
62
+ }
63
+
64
+ export function convertTldrawFillToFocusedFill(fill: TLDefaultFillStyle): FocusedFill {
65
+ return SHAPE_TO_FOCUSED_FILLS[fill]
66
+ }
67
+
68
+ // ---- Font Size ----
69
+
70
+ const FONT_SIZE_MULTIPLIERS: Record<TLDefaultSizeStyle, number> = {
71
+ s: 1.125,
72
+ m: 1.5,
73
+ l: 2.25,
74
+ xl: 2.75,
75
+ }
76
+
77
+ const DEFAULT_BASE_FONT_SIZE = 16
78
+
79
+ export function convertFocusedFontSizeToTldrawFontSizeAndScale(
80
+ targetFontSize: number,
81
+ baseFontSize = DEFAULT_BASE_FONT_SIZE
82
+ ) {
83
+ const fontSizeEntries = Object.entries(FONT_SIZE_MULTIPLIERS)
84
+ let closestSize = fontSizeEntries[0]
85
+ let closestPixelSize = closestSize[1] * baseFontSize
86
+ let minDifference = Math.abs(targetFontSize - closestPixelSize)
87
+
88
+ for (const [size, multiplier] of fontSizeEntries) {
89
+ const pixelSize = multiplier * baseFontSize
90
+ const difference = Math.abs(targetFontSize - pixelSize)
91
+ if (difference < minDifference) {
92
+ minDifference = difference
93
+ closestSize = [size, multiplier]
94
+ closestPixelSize = pixelSize
95
+ }
96
+ }
97
+
98
+ const textSize = closestSize[0] as TLDefaultSizeStyle
99
+ const scale = targetFontSize / closestPixelSize
100
+
101
+ return { textSize, scale }
102
+ }
103
+
104
+ export function convertTldrawFontSizeAndScaleToFocusedFontSize(
105
+ textSize: TLDefaultSizeStyle,
106
+ scale: number,
107
+ baseFontSize = DEFAULT_BASE_FONT_SIZE
108
+ ) {
109
+ return Math.round(FONT_SIZE_MULTIPLIERS[textSize] * baseFontSize * scale)
110
+ }
111
+
112
+ // ---- Geo Shape Types ----
113
+
114
+ export type FocusedGeoShapeType =
115
+ | 'rectangle'
116
+ | 'ellipse'
117
+ | 'triangle'
118
+ | 'diamond'
119
+ | 'hexagon'
120
+ | 'pill'
121
+ | 'cloud'
122
+ | 'x-box'
123
+ | 'check-box'
124
+ | 'heart'
125
+ | 'pentagon'
126
+ | 'octagon'
127
+ | 'star'
128
+ | 'parallelogram-right'
129
+ | 'parallelogram-left'
130
+ | 'trapezoid'
131
+ | 'fat-arrow-right'
132
+ | 'fat-arrow-left'
133
+ | 'fat-arrow-up'
134
+ | 'fat-arrow-down'
135
+
136
+ export const FOCUSED_TO_GEO_TYPES: Record<FocusedGeoShapeType, TLGeoShapeGeoStyle> = {
137
+ rectangle: 'rectangle',
138
+ ellipse: 'ellipse',
139
+ triangle: 'triangle',
140
+ diamond: 'diamond',
141
+ hexagon: 'hexagon',
142
+ pill: 'oval',
143
+ cloud: 'cloud',
144
+ 'x-box': 'x-box',
145
+ 'check-box': 'check-box',
146
+ heart: 'heart',
147
+ pentagon: 'pentagon',
148
+ octagon: 'octagon',
149
+ star: 'star',
150
+ 'parallelogram-right': 'rhombus',
151
+ 'parallelogram-left': 'rhombus-2',
152
+ trapezoid: 'trapezoid',
153
+ 'fat-arrow-right': 'arrow-right',
154
+ 'fat-arrow-left': 'arrow-left',
155
+ 'fat-arrow-up': 'arrow-up',
156
+ 'fat-arrow-down': 'arrow-down',
157
+ } as const
158
+
159
+ export const GEO_TO_FOCUSED_TYPES: Record<TLGeoShapeGeoStyle, FocusedGeoShapeType> = {
160
+ rectangle: 'rectangle',
161
+ ellipse: 'ellipse',
162
+ triangle: 'triangle',
163
+ diamond: 'diamond',
164
+ hexagon: 'hexagon',
165
+ oval: 'pill',
166
+ cloud: 'cloud',
167
+ 'x-box': 'x-box',
168
+ 'check-box': 'check-box',
169
+ heart: 'heart',
170
+ pentagon: 'pentagon',
171
+ octagon: 'octagon',
172
+ star: 'star',
173
+ rhombus: 'parallelogram-right',
174
+ 'rhombus-2': 'parallelogram-left',
175
+ trapezoid: 'trapezoid',
176
+ 'arrow-right': 'fat-arrow-right',
177
+ 'arrow-left': 'fat-arrow-left',
178
+ 'arrow-up': 'fat-arrow-up',
179
+ 'arrow-down': 'fat-arrow-down',
180
+ } as const
181
+
182
+ // ---- ID Conversion ----
183
+
184
+ export function convertSimpleIdToTldrawId(id: string): TLShapeId {
185
+ if (id.startsWith('shape:')) return id as TLShapeId
186
+ return ('shape:' + id) as TLShapeId
187
+ }
188
+
189
+ export function convertTldrawIdToSimpleId(id: TLShapeId): string {
190
+ return id.slice(6)
191
+ }
192
+
193
+ // ---- Text Anchor ----
194
+
195
+ export type FocusedTextAnchor =
196
+ | 'bottom-center'
197
+ | 'bottom-left'
198
+ | 'bottom-right'
199
+ | 'center-left'
200
+ | 'center-right'
201
+ | 'center'
202
+ | 'top-center'
203
+ | 'top-left'
204
+ | 'top-right'
205
+
206
+ // ---- Focused Shape Types ----
207
+
208
+ /**
209
+ * Geometric shapes like rectangles, ellipses, triangles, and other predefined forms.
210
+ * The _type field determines the geometric form.
211
+ */
212
+ export interface FocusedGeoShape {
213
+ /** Geometric shape type */
214
+ _type: FocusedGeoShapeType
215
+ /** Shape color */
216
+ color: FocusedColor
217
+ /** Fill style */
218
+ fill: FocusedFill
219
+ /** Height in pixels */
220
+ h: number
221
+ /** Metadata note */
222
+ note: string
223
+ /** Unique shape identifier */
224
+ shapeId: string
225
+ /** Text label inside the shape */
226
+ text?: string
227
+ /** Text alignment */
228
+ textAlign?: 'start' | 'middle' | 'end'
229
+ /** Width in pixels */
230
+ w: number
231
+ /** X position */
232
+ x: number
233
+ /** Y position */
234
+ y: number
235
+ }
236
+
237
+ /** A straight line between two points. */
238
+ export interface FocusedLineShape {
239
+ /** Always 'line' */
240
+ _type: 'line'
241
+ /** Line color */
242
+ color: FocusedColor
243
+ /** Metadata note */
244
+ note: string
245
+ /** Unique shape identifier */
246
+ shapeId: string
247
+ /** Start X */
248
+ x1: number
249
+ /** End X */
250
+ x2: number
251
+ /** Start Y */
252
+ y1: number
253
+ /** End Y */
254
+ y2: number
255
+ }
256
+
257
+ /** A sticky note. */
258
+ export interface FocusedNoteShape {
259
+ /** Always 'note' */
260
+ _type: 'note'
261
+ /** Note color */
262
+ color: FocusedColor
263
+ /** Metadata note */
264
+ note: string
265
+ /** Unique shape identifier */
266
+ shapeId: string
267
+ /** Note text content */
268
+ text?: string
269
+ /** X position */
270
+ x: number
271
+ /** Y position */
272
+ y: number
273
+ }
274
+
275
+ /** A text shape for placing text on the canvas. */
276
+ export interface FocusedTextShape {
277
+ /** Always 'text' */
278
+ _type: 'text'
279
+ /** Where the (x,y) point anchors on the text bounding box */
280
+ anchor: FocusedTextAnchor
281
+ /** Text color */
282
+ color: FocusedColor
283
+ /** Font size in pixels */
284
+ fontSize?: number
285
+ /** Max width before wrapping (null = auto-size) */
286
+ maxWidth: number | null
287
+ /** Metadata note */
288
+ note: string
289
+ /** Unique shape identifier */
290
+ shapeId: string
291
+ /** Text content */
292
+ text: string
293
+ /** X position */
294
+ x: number
295
+ /** Y position */
296
+ y: number
297
+ }
298
+
299
+ /** An arrow connecting two points or shapes. Use fromId/toId to bind to shapes. */
300
+ export interface FocusedArrowShape {
301
+ /** Always 'arrow' */
302
+ _type: 'arrow'
303
+ /** Arrow color */
304
+ color: FocusedColor
305
+ /** Shape ID to bind the arrow start to */
306
+ fromId: string | null
307
+ /** Metadata note */
308
+ note: string
309
+ /** Unique shape identifier */
310
+ shapeId: string
311
+ /** Arrow label text */
312
+ text?: string
313
+ /** Shape ID to bind the arrow end to */
314
+ toId: string | null
315
+ /** Start X */
316
+ x1: number
317
+ /** End X */
318
+ x2: number
319
+ /** Start Y */
320
+ y1: number
321
+ /** End Y */
322
+ y2: number
323
+ /** Signed bend amount for the curve */
324
+ bend?: number
325
+ /** Arrow routing style */
326
+ kind?: 'arc' | 'elbow'
327
+ }
328
+
329
+ /** A freehand drawing. */
330
+ export interface FocusedDrawShape {
331
+ /** Always 'draw' */
332
+ _type: 'draw'
333
+ /** Stroke color */
334
+ color: FocusedColor
335
+ /** Fill style */
336
+ fill?: FocusedFill
337
+ /** Metadata note */
338
+ note: string
339
+ /** Unique shape identifier */
340
+ shapeId: string
341
+ }
342
+
343
+ /** Fallback for unsupported shape types. */
344
+ export interface FocusedUnknownShape {
345
+ /** Always 'unknown' */
346
+ _type: 'unknown'
347
+ /** Metadata note */
348
+ note: string
349
+ /** Unique shape identifier */
350
+ shapeId: string
351
+ /** Original tldraw shape type */
352
+ subType: string
353
+ /** X position */
354
+ x: number
355
+ /** Y position */
356
+ y: number
357
+ }
358
+
359
+ export type FocusedShape =
360
+ | FocusedGeoShape
361
+ | FocusedLineShape
362
+ | FocusedNoteShape
363
+ | FocusedTextShape
364
+ | FocusedArrowShape
365
+ | FocusedDrawShape
366
+ | FocusedUnknownShape
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Convert tldraw TLShape → FocusedShape.
3
+ * Ported from tldraw/tldraw templates/agent/shared/format/convertTldrawShapeToFocusedShape.ts
4
+ */
5
+ import {
6
+ Box,
7
+ createShapeId,
8
+ Editor,
9
+ isPageId,
10
+ reverseRecordsDiff,
11
+ TLArrowBinding,
12
+ TLArrowShape,
13
+ TLDrawShape,
14
+ TLGeoShape,
15
+ TLLineShape,
16
+ TLNoteShape,
17
+ TLShape,
18
+ TLTextShape,
19
+ Vec,
20
+ } from 'tldraw'
21
+ import {
22
+ convertTldrawFillToFocusedFill,
23
+ convertTldrawFontSizeAndScaleToFocusedFontSize,
24
+ convertTldrawIdToSimpleId,
25
+ GEO_TO_FOCUSED_TYPES,
26
+ type FocusedArrowShape,
27
+ type FocusedDrawShape,
28
+ type FocusedGeoShape,
29
+ type FocusedLineShape,
30
+ type FocusedNoteShape,
31
+ type FocusedShape,
32
+ type FocusedTextAnchor,
33
+ type FocusedTextShape,
34
+ type FocusedUnknownShape,
35
+ } from './format'
36
+
37
+ export function convertTldrawShapeToFocusedShape(editor: Editor, shape: TLShape): FocusedShape {
38
+ switch (shape.type) {
39
+ case 'text':
40
+ return convertTextShapeToFocused(editor, shape as TLTextShape)
41
+ case 'geo':
42
+ return convertGeoShapeToFocused(editor, shape as TLGeoShape)
43
+ case 'line':
44
+ return convertLineShapeToFocused(editor, shape as TLLineShape)
45
+ case 'arrow':
46
+ return convertArrowShapeToFocused(editor, shape as TLArrowShape)
47
+ case 'note':
48
+ return convertNoteShapeToFocused(editor, shape as TLNoteShape)
49
+ case 'draw':
50
+ return convertDrawShapeToFocused(editor, shape as TLDrawShape)
51
+ default:
52
+ return convertUnknownShapeToFocused(editor, shape)
53
+ }
54
+ }
55
+
56
+ function convertDrawShapeToFocused(_editor: Editor, shape: TLDrawShape): FocusedDrawShape {
57
+ return {
58
+ _type: 'draw',
59
+ color: shape.props.color,
60
+ fill: convertTldrawFillToFocusedFill(shape.props.fill),
61
+ note: (shape.meta.note as string) ?? '',
62
+ shapeId: convertTldrawIdToSimpleId(shape.id),
63
+ }
64
+ }
65
+
66
+ function convertTextShapeToFocused(editor: Editor, shape: TLTextShape): FocusedTextShape {
67
+ const util = editor.getShapeUtil(shape)
68
+ const text = util.getText(shape) ?? ''
69
+ const bounds = getSimpleBounds(editor, shape)
70
+ const textSize = shape.props.size
71
+ const baseFontSize = editor.getTheme('default')?.fontSize ?? 16
72
+
73
+ const position = new Vec()
74
+ let anchor: FocusedTextAnchor = 'top-left'
75
+ switch (shape.props.textAlign) {
76
+ case 'middle': {
77
+ anchor = 'top-center'
78
+ position.x = bounds.center.x
79
+ position.y = bounds.top
80
+ break
81
+ }
82
+ case 'end': {
83
+ anchor = 'top-right'
84
+ position.x = bounds.right
85
+ position.y = bounds.top
86
+ break
87
+ }
88
+ case 'start': {
89
+ anchor = 'top-left'
90
+ position.x = bounds.left
91
+ position.y = bounds.top
92
+ break
93
+ }
94
+ }
95
+
96
+ return {
97
+ _type: 'text',
98
+ anchor,
99
+ color: shape.props.color,
100
+ fontSize: convertTldrawFontSizeAndScaleToFocusedFontSize(
101
+ textSize,
102
+ shape.props.scale,
103
+ baseFontSize
104
+ ),
105
+ maxWidth: shape.props.autoSize ? null : shape.props.w,
106
+ note: (shape.meta.note as string) ?? '',
107
+ shapeId: convertTldrawIdToSimpleId(shape.id),
108
+ text,
109
+ x: position.x,
110
+ y: position.y,
111
+ }
112
+ }
113
+
114
+ function convertGeoShapeToFocused(editor: Editor, shape: TLGeoShape): FocusedGeoShape {
115
+ const util = editor.getShapeUtil(shape)
116
+ const text = util.getText(shape)
117
+ const bounds = getSimpleBounds(editor, shape)
118
+ const shapeTextAlign = shape.props.align
119
+
120
+ let newTextAlign: FocusedGeoShape['textAlign']
121
+ switch (shapeTextAlign) {
122
+ case 'start-legacy':
123
+ newTextAlign = 'start'
124
+ break
125
+ case 'middle-legacy':
126
+ newTextAlign = 'middle'
127
+ break
128
+ case 'end-legacy':
129
+ newTextAlign = 'end'
130
+ break
131
+ default:
132
+ newTextAlign = shapeTextAlign
133
+ break
134
+ }
135
+
136
+ return {
137
+ _type: GEO_TO_FOCUSED_TYPES[shape.props.geo],
138
+ color: shape.props.color,
139
+ fill: convertTldrawFillToFocusedFill(shape.props.fill),
140
+ h: shape.props.h,
141
+ note: (shape.meta.note as string) ?? '',
142
+ shapeId: convertTldrawIdToSimpleId(shape.id),
143
+ text: text ?? '',
144
+ textAlign: newTextAlign,
145
+ w: shape.props.w,
146
+ x: bounds.x,
147
+ y: bounds.y,
148
+ }
149
+ }
150
+
151
+ function convertLineShapeToFocused(editor: Editor, shape: TLLineShape): FocusedLineShape {
152
+ const bounds = getSimpleBounds(editor, shape)
153
+ const points = Object.values(shape.props.points).sort((a, b) => a.index.localeCompare(b.index))
154
+ return {
155
+ _type: 'line',
156
+ color: shape.props.color,
157
+ note: (shape.meta.note as string) ?? '',
158
+ shapeId: convertTldrawIdToSimpleId(shape.id),
159
+ x1: points[0].x + bounds.x,
160
+ x2: points[1].x + bounds.x,
161
+ y1: points[0].y + bounds.y,
162
+ y2: points[1].y + bounds.y,
163
+ }
164
+ }
165
+
166
+ function convertArrowShapeToFocused(editor: Editor, shape: TLArrowShape): FocusedArrowShape {
167
+ const bounds = getSimpleBounds(editor, shape)
168
+ const bindings = editor.store.query.records('binding').get()
169
+ const arrowBindings = bindings.filter(
170
+ (b) => b.type === 'arrow' && b.fromId === shape.id
171
+ ) as TLArrowBinding[]
172
+ const startBinding = arrowBindings.find((b) => b.props.terminal === 'start')
173
+ const endBinding = arrowBindings.find((b) => b.props.terminal === 'end')
174
+
175
+ return {
176
+ _type: 'arrow',
177
+ bend: shape.props.bend * -1,
178
+ kind: shape.props.kind,
179
+ color: shape.props.color,
180
+ fromId: startBinding ? convertTldrawIdToSimpleId(startBinding.toId) : null,
181
+ note: (shape.meta.note as string) ?? '',
182
+ shapeId: convertTldrawIdToSimpleId(shape.id),
183
+ text: editor.getShapeUtil(shape).getText(shape) ?? '',
184
+ toId: endBinding ? convertTldrawIdToSimpleId(endBinding.toId) : null,
185
+ x1: shape.props.start.x + bounds.x,
186
+ x2: shape.props.end.x + bounds.x,
187
+ y1: shape.props.start.y + bounds.y,
188
+ y2: shape.props.end.y + bounds.y,
189
+ }
190
+ }
191
+
192
+ function convertNoteShapeToFocused(editor: Editor, shape: TLNoteShape): FocusedNoteShape {
193
+ const util = editor.getShapeUtil(shape)
194
+ const text = util.getText(shape)
195
+ const bounds = getSimpleBounds(editor, shape)
196
+ return {
197
+ _type: 'note',
198
+ color: shape.props.color,
199
+ note: (shape.meta.note as string) ?? '',
200
+ shapeId: convertTldrawIdToSimpleId(shape.id),
201
+ text: text ?? '',
202
+ x: bounds.x,
203
+ y: bounds.y,
204
+ }
205
+ }
206
+
207
+ function convertUnknownShapeToFocused(editor: Editor, shape: TLShape): FocusedUnknownShape {
208
+ const bounds = getSimpleBounds(editor, shape)
209
+ return {
210
+ _type: 'unknown',
211
+ note: (shape.meta.note as string) ?? '',
212
+ shapeId: convertTldrawIdToSimpleId(shape.id),
213
+ subType: shape.type,
214
+ x: bounds.x,
215
+ y: bounds.y,
216
+ }
217
+ }
218
+
219
+ function getSimpleBounds(editor: Editor, shape: TLShape): Box {
220
+ const pagePoint = getShapePagePoint(editor, shape)
221
+
222
+ const props = shape.props as { w?: number; h?: number }
223
+ if (props.w !== undefined && props.h !== undefined) {
224
+ return new Box(pagePoint.x, pagePoint.y, props.w, props.h)
225
+ }
226
+
227
+ const bounds = editor.getShapePageBounds(shape)
228
+ if (bounds) {
229
+ return new Box(pagePoint.x, pagePoint.y, bounds.w, bounds.h)
230
+ }
231
+
232
+ let mockBounds: Box | undefined
233
+ const diff = editor.store.extractingChanges(() => {
234
+ editor.run(
235
+ () => {
236
+ const mockId = createShapeId()
237
+ editor.createShape({ ...shape, id: mockId })
238
+ mockBounds = editor.getShapePageBounds(mockId)
239
+ },
240
+ { ignoreShapeLock: false, history: 'ignore' }
241
+ )
242
+ })
243
+ const reverseDiff = reverseRecordsDiff(diff)
244
+ editor.store.applyDiff(reverseDiff)
245
+
246
+ if (!mockBounds) {
247
+ throw new Error('Failed to get bounds for shape')
248
+ }
249
+ return new Box(pagePoint.x, pagePoint.y, mockBounds.w, mockBounds.h)
250
+ }
251
+
252
+ function getShapePagePoint(editor: Editor, shape: TLShape): Vec {
253
+ if (isPageId(shape.parentId)) {
254
+ return new Vec(shape.x, shape.y)
255
+ }
256
+ const parentTransform = editor.getShapePageTransform(shape.parentId)
257
+ return parentTransform.applyToPoint(new Vec(shape.x, shape.y))
258
+ }