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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/bridge/app-bridge-entry.js +6 -0
- package/mcp-app/LICENSE.md +9 -0
- package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
- package/mcp-app/README.md +129 -0
- package/mcp-app/dev-tunnel.sh +51 -0
- package/mcp-app/dist/editor-api.json +8493 -0
- package/mcp-app/dist/mcp-app.html +643 -0
- package/mcp-app/dist/method-map.json +915 -0
- package/mcp-app/package.json +42 -0
- package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
- package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
- package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
- package/mcp-app/scripts/extract-editor-api.ts +1374 -0
- package/mcp-app/server.json +21 -0
- package/mcp-app/src/logger.ts +45 -0
- package/mcp-app/src/register-tools.ts +368 -0
- package/mcp-app/src/shared/generated-data.ts +160 -0
- package/mcp-app/src/shared/pending-requests.ts +69 -0
- package/mcp-app/src/shared/types.ts +76 -0
- package/mcp-app/src/shared/utils.ts +132 -0
- package/mcp-app/src/tools/exec.ts +120 -0
- package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
- package/mcp-app/src/tools/search.ts +150 -0
- package/mcp-app/src/widget/app-context.tsx +29 -0
- package/mcp-app/src/widget/dev-log.tsx +70 -0
- package/mcp-app/src/widget/exec-helpers.ts +232 -0
- package/mcp-app/src/widget/export-tldr.ts +35 -0
- package/mcp-app/src/widget/focused/defaults.ts +141 -0
- package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
- package/mcp-app/src/widget/focused/format.ts +366 -0
- package/mcp-app/src/widget/focused/to-focused.ts +258 -0
- package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
- package/mcp-app/src/widget/image-guard.tsx +106 -0
- package/mcp-app/src/widget/index.html +33 -0
- package/mcp-app/src/widget/mcp-app.css +113 -0
- package/mcp-app/src/widget/mcp-app.tsx +857 -0
- package/mcp-app/src/widget/persistence.ts +337 -0
- package/mcp-app/src/widget/snapshot.ts +157 -0
- package/mcp-app/src/worker.ts +305 -0
- package/mcp-app/tsconfig.json +23 -0
- package/mcp-app/vite.config.ts +13 -0
- package/mcp-app/wrangler.toml +45 -0
- package/mcp-app-source.json +36 -0
- package/package.json +90 -0
- package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
- package/scripts/assemble-mcp-app.mjs +193 -0
- package/scripts/build-bridge.mjs +74 -0
- package/scripts/e2e-mcp.mjs +69 -0
- package/scripts/e2e-packaged-mcp-app.mjs +79 -0
- package/scripts/run-mcp-app-dev.mjs +44 -0
- package/scripts/verify-bundle.mjs +41 -0
- package/scripts/verify-mcp-app-source.mjs +51 -0
- package/scripts/verify-mcp-app.mjs +38 -0
- package/scripts/verify-package-files.mjs +50 -0
- package/src/canvas/export.ts +164 -0
- package/src/canvas/state.ts +117 -0
- package/src/canvas/workflow.ts +105 -0
- package/src/commands/tldraw-command.ts +48 -0
- package/src/diagram/guidance.ts +44 -0
- package/src/host/local-host.ts +289 -0
- package/src/index.ts +762 -0
- package/src/mcp/client.ts +126 -0
- package/src/mcp/response.ts +74 -0
- package/src/semantic/layer.ts +309 -0
- package/src/server/server-manager.ts +153 -0
- package/src/store/export-store.ts +33 -0
- package/src/store/project-store.ts +251 -0
- package/src/ui/tldraw-status.ts +88 -0
- package/static/app-bridge-bundle.js +18114 -0
- package/static/app-bridge-bundle.meta.json +164 -0
- package/static/host.html +390 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert FocusedShape → tldraw TLShape.
|
|
3
|
+
* Ported from tldraw/tldraw templates/agent/shared/format/convertFocusedShapeToTldrawShape.ts
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
Box,
|
|
7
|
+
createShapeId,
|
|
8
|
+
Editor,
|
|
9
|
+
IndexKey,
|
|
10
|
+
reverseRecordsDiff,
|
|
11
|
+
TLArrowShape,
|
|
12
|
+
TLBindingCreate,
|
|
13
|
+
TLDefaultSizeStyle,
|
|
14
|
+
TLDrawShape,
|
|
15
|
+
TLGeoShape,
|
|
16
|
+
TLLineShape,
|
|
17
|
+
TLNoteShape,
|
|
18
|
+
TLShape,
|
|
19
|
+
TLTextShape,
|
|
20
|
+
toRichText,
|
|
21
|
+
Vec,
|
|
22
|
+
} from 'tldraw'
|
|
23
|
+
import {
|
|
24
|
+
asColor,
|
|
25
|
+
convertFocusedFillToTldrawFill,
|
|
26
|
+
convertFocusedFontSizeToTldrawFontSizeAndScale,
|
|
27
|
+
convertSimpleIdToTldrawId,
|
|
28
|
+
FOCUSED_TO_GEO_TYPES,
|
|
29
|
+
type FocusedArrowShape,
|
|
30
|
+
type FocusedDrawShape,
|
|
31
|
+
type FocusedGeoShape,
|
|
32
|
+
type FocusedGeoShapeType,
|
|
33
|
+
type FocusedLineShape,
|
|
34
|
+
type FocusedNoteShape,
|
|
35
|
+
type FocusedShape,
|
|
36
|
+
type FocusedTextShape,
|
|
37
|
+
type FocusedUnknownShape,
|
|
38
|
+
} from './format'
|
|
39
|
+
|
|
40
|
+
export function convertFocusedShapeToTldrawShape(
|
|
41
|
+
editor: Editor,
|
|
42
|
+
focusedShape: FocusedShape,
|
|
43
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
44
|
+
): { shape: TLShape; bindings?: TLBindingCreate[] } {
|
|
45
|
+
switch (focusedShape._type) {
|
|
46
|
+
case 'text':
|
|
47
|
+
return convertTextShapeToTldrawShape(editor, focusedShape, { defaultShape })
|
|
48
|
+
case 'line':
|
|
49
|
+
return convertLineShapeToTldrawShape(editor, focusedShape, { defaultShape })
|
|
50
|
+
case 'arrow':
|
|
51
|
+
return convertArrowShapeToTldrawShape(editor, focusedShape, { defaultShape })
|
|
52
|
+
case 'note':
|
|
53
|
+
return convertNoteShapeToTldrawShape(editor, focusedShape, { defaultShape })
|
|
54
|
+
case 'draw':
|
|
55
|
+
return convertDrawShapeToTldrawShape(editor, focusedShape, { defaultShape })
|
|
56
|
+
case 'unknown':
|
|
57
|
+
return convertUnknownShapeToTldrawShape(editor, focusedShape, { defaultShape })
|
|
58
|
+
default:
|
|
59
|
+
// Geo types (rectangle, ellipse, etc.)
|
|
60
|
+
return convertGeoShapeToTldrawShape(editor, focusedShape as FocusedGeoShape, {
|
|
61
|
+
defaultShape,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function convertFocusedGeoTypeToTldrawGeoGeoType(type: FocusedGeoShapeType) {
|
|
67
|
+
return FOCUSED_TO_GEO_TYPES[type]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function convertTextShapeToTldrawShape(
|
|
71
|
+
editor: Editor,
|
|
72
|
+
focusedShape: FocusedTextShape,
|
|
73
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
74
|
+
): { shape: TLTextShape } {
|
|
75
|
+
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
|
76
|
+
const defaultTextShape = defaultShape as TLTextShape
|
|
77
|
+
const baseFontSize = editor.getTheme('default')?.fontSize ?? 16
|
|
78
|
+
|
|
79
|
+
let textSize: TLDefaultSizeStyle = 's'
|
|
80
|
+
let scale = 1
|
|
81
|
+
|
|
82
|
+
if (focusedShape.fontSize) {
|
|
83
|
+
const result = convertFocusedFontSizeToTldrawFontSizeAndScale(
|
|
84
|
+
focusedShape.fontSize,
|
|
85
|
+
baseFontSize
|
|
86
|
+
)
|
|
87
|
+
textSize = result.textSize
|
|
88
|
+
scale = result.scale
|
|
89
|
+
} else if (defaultTextShape.props?.size) {
|
|
90
|
+
textSize = defaultTextShape.props.size
|
|
91
|
+
scale = defaultTextShape.props.scale ?? 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const autoSize =
|
|
95
|
+
focusedShape.maxWidth !== undefined && focusedShape.maxWidth !== null
|
|
96
|
+
? false
|
|
97
|
+
: (defaultTextShape.props?.autoSize ?? true)
|
|
98
|
+
const font = defaultTextShape.props?.font ?? 'draw'
|
|
99
|
+
|
|
100
|
+
let richText
|
|
101
|
+
if (focusedShape.text !== undefined) {
|
|
102
|
+
richText = toRichText(focusedShape.text)
|
|
103
|
+
} else if (defaultTextShape.props?.richText) {
|
|
104
|
+
richText = defaultTextShape.props.richText
|
|
105
|
+
} else {
|
|
106
|
+
richText = toRichText('')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let textAlign: TLTextShape['props']['textAlign'] = defaultTextShape.props?.textAlign ?? 'start'
|
|
110
|
+
switch (focusedShape.anchor) {
|
|
111
|
+
case 'top-left':
|
|
112
|
+
case 'bottom-left':
|
|
113
|
+
case 'center-left':
|
|
114
|
+
textAlign = 'start'
|
|
115
|
+
break
|
|
116
|
+
case 'top-center':
|
|
117
|
+
case 'bottom-center':
|
|
118
|
+
case 'center':
|
|
119
|
+
textAlign = 'middle'
|
|
120
|
+
break
|
|
121
|
+
case 'top-right':
|
|
122
|
+
case 'bottom-right':
|
|
123
|
+
case 'center-right':
|
|
124
|
+
textAlign = 'end'
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const unpositionedShape: TLTextShape = {
|
|
129
|
+
id: shapeId,
|
|
130
|
+
type: 'text',
|
|
131
|
+
typeName: 'shape',
|
|
132
|
+
x: 0,
|
|
133
|
+
y: 0,
|
|
134
|
+
rotation: defaultTextShape.rotation ?? 0,
|
|
135
|
+
index: defaultTextShape.index ?? editor.getHighestIndexForParent(editor.getCurrentPageId()),
|
|
136
|
+
parentId: defaultTextShape.parentId ?? editor.getCurrentPageId(),
|
|
137
|
+
isLocked: defaultTextShape.isLocked ?? false,
|
|
138
|
+
opacity: defaultTextShape.opacity ?? 1,
|
|
139
|
+
props: {
|
|
140
|
+
size: textSize,
|
|
141
|
+
scale,
|
|
142
|
+
richText,
|
|
143
|
+
color: asColor(focusedShape.color ?? defaultTextShape.props?.color ?? 'black'),
|
|
144
|
+
textAlign,
|
|
145
|
+
autoSize,
|
|
146
|
+
w:
|
|
147
|
+
focusedShape.maxWidth !== undefined && focusedShape.maxWidth !== null
|
|
148
|
+
? focusedShape.maxWidth
|
|
149
|
+
: (defaultTextShape.props?.w ?? 100),
|
|
150
|
+
font,
|
|
151
|
+
},
|
|
152
|
+
meta: {
|
|
153
|
+
note: focusedShape.note ?? defaultTextShape.meta?.note ?? '',
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const unpositionedBounds = getDummyBounds(editor, unpositionedShape)
|
|
158
|
+
|
|
159
|
+
const position = new Vec(defaultTextShape.x ?? 0, defaultTextShape.y ?? 0)
|
|
160
|
+
const x = focusedShape.x ?? defaultTextShape.x ?? 0
|
|
161
|
+
const y = focusedShape.y ?? defaultTextShape.y ?? 0
|
|
162
|
+
switch (focusedShape.anchor) {
|
|
163
|
+
case 'top-center': {
|
|
164
|
+
position.x = x - unpositionedBounds.w / 2
|
|
165
|
+
position.y = y
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
case 'top-right': {
|
|
169
|
+
position.x = x - unpositionedBounds.w
|
|
170
|
+
position.y = y
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
case 'bottom-left': {
|
|
174
|
+
position.x = x
|
|
175
|
+
position.y = y - unpositionedBounds.h
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
case 'bottom-center': {
|
|
179
|
+
position.x = x - unpositionedBounds.w / 2
|
|
180
|
+
position.y = y - unpositionedBounds.h
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
case 'bottom-right': {
|
|
184
|
+
position.x = x - unpositionedBounds.w
|
|
185
|
+
position.y = y - unpositionedBounds.h
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
case 'center-left': {
|
|
189
|
+
position.x = x
|
|
190
|
+
position.y = y - unpositionedBounds.h / 2
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
case 'center-right': {
|
|
194
|
+
position.x = x - unpositionedBounds.w
|
|
195
|
+
position.y = y - unpositionedBounds.h / 2
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
case 'center': {
|
|
199
|
+
position.x = focusedShape.x - unpositionedBounds.w / 2
|
|
200
|
+
position.y = focusedShape.y - unpositionedBounds.h / 2
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
case 'top-left':
|
|
204
|
+
default: {
|
|
205
|
+
position.x = x
|
|
206
|
+
position.y = y
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
shape: { ...unpositionedShape, x: position.x, y: position.y },
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function convertLineShapeToTldrawShape(
|
|
217
|
+
editor: Editor,
|
|
218
|
+
focusedShape: FocusedLineShape,
|
|
219
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
220
|
+
): { shape: TLShape } {
|
|
221
|
+
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
|
222
|
+
const defaultLineShape = defaultShape as TLLineShape
|
|
223
|
+
|
|
224
|
+
const x1 = focusedShape.x1 ?? 0
|
|
225
|
+
const y1 = focusedShape.y1 ?? 0
|
|
226
|
+
const x2 = focusedShape.x2 ?? 0
|
|
227
|
+
const y2 = focusedShape.y2 ?? 0
|
|
228
|
+
const minX = Math.min(x1, x2)
|
|
229
|
+
const minY = Math.min(y1, y2)
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
shape: {
|
|
233
|
+
id: shapeId,
|
|
234
|
+
type: 'line',
|
|
235
|
+
typeName: 'shape',
|
|
236
|
+
x: minX,
|
|
237
|
+
y: minY,
|
|
238
|
+
rotation: defaultLineShape.rotation ?? 0,
|
|
239
|
+
index: defaultLineShape.index ?? editor.getHighestIndexForParent(editor.getCurrentPageId()),
|
|
240
|
+
parentId: defaultLineShape.parentId ?? editor.getCurrentPageId(),
|
|
241
|
+
isLocked: defaultLineShape.isLocked ?? false,
|
|
242
|
+
opacity: defaultLineShape.opacity ?? 1,
|
|
243
|
+
props: {
|
|
244
|
+
size: defaultLineShape.props?.size ?? 's',
|
|
245
|
+
points: {
|
|
246
|
+
a1: { id: 'a1', index: 'a1' as IndexKey, x: x1 - minX, y: y1 - minY },
|
|
247
|
+
a2: { id: 'a2', index: 'a2' as IndexKey, x: x2 - minX, y: y2 - minY },
|
|
248
|
+
},
|
|
249
|
+
color: asColor(focusedShape.color ?? defaultLineShape.props?.color ?? 'black'),
|
|
250
|
+
dash: defaultLineShape.props?.dash ?? 'draw',
|
|
251
|
+
scale: defaultLineShape.props?.scale ?? 1,
|
|
252
|
+
spline: defaultLineShape.props?.spline ?? 'line',
|
|
253
|
+
},
|
|
254
|
+
meta: {
|
|
255
|
+
note: focusedShape.note ?? defaultLineShape.meta?.note ?? '',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function convertArrowShapeToTldrawShape(
|
|
262
|
+
editor: Editor,
|
|
263
|
+
focusedShape: FocusedArrowShape,
|
|
264
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
265
|
+
): { shape: TLShape; bindings?: TLBindingCreate[] } {
|
|
266
|
+
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
|
267
|
+
const defaultArrowShape = defaultShape as TLArrowShape
|
|
268
|
+
|
|
269
|
+
const x1 = focusedShape.x1 ?? defaultArrowShape.props?.start?.x ?? 0
|
|
270
|
+
const y1 = focusedShape.y1 ?? defaultArrowShape.props?.start?.y ?? 0
|
|
271
|
+
const x2 = focusedShape.x2 ?? defaultArrowShape.props?.end?.x ?? 0
|
|
272
|
+
const y2 = focusedShape.y2 ?? defaultArrowShape.props?.end?.y ?? 0
|
|
273
|
+
const minX = Math.min(x1, x2)
|
|
274
|
+
const minY = Math.min(y1, y2)
|
|
275
|
+
|
|
276
|
+
let richText
|
|
277
|
+
if (focusedShape.text !== undefined) {
|
|
278
|
+
richText = toRichText(focusedShape.text)
|
|
279
|
+
} else if (defaultArrowShape.props?.richText) {
|
|
280
|
+
richText = defaultArrowShape.props.richText
|
|
281
|
+
} else {
|
|
282
|
+
richText = toRichText('')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const shape = {
|
|
286
|
+
id: shapeId,
|
|
287
|
+
type: 'arrow' as const,
|
|
288
|
+
typeName: 'shape' as const,
|
|
289
|
+
x: minX,
|
|
290
|
+
y: minY,
|
|
291
|
+
rotation: defaultArrowShape.rotation ?? 0,
|
|
292
|
+
index: defaultArrowShape.index ?? editor.getHighestIndexForParent(editor.getCurrentPageId()),
|
|
293
|
+
parentId: defaultArrowShape.parentId ?? editor.getCurrentPageId(),
|
|
294
|
+
isLocked: defaultArrowShape.isLocked ?? false,
|
|
295
|
+
opacity: defaultArrowShape.opacity ?? 1,
|
|
296
|
+
props: {
|
|
297
|
+
arrowheadEnd: defaultArrowShape.props?.arrowheadEnd ?? 'arrow',
|
|
298
|
+
arrowheadStart: defaultArrowShape.props?.arrowheadStart ?? 'none',
|
|
299
|
+
bend: (focusedShape.bend ?? (defaultArrowShape.props?.bend ?? 0) * -1) * -1,
|
|
300
|
+
color: asColor(focusedShape.color ?? defaultArrowShape.props?.color ?? 'black'),
|
|
301
|
+
dash: defaultArrowShape.props?.dash ?? 'draw',
|
|
302
|
+
elbowMidPoint: defaultArrowShape.props?.elbowMidPoint ?? 0.5,
|
|
303
|
+
end: { x: x2 - minX, y: y2 - minY },
|
|
304
|
+
fill: defaultArrowShape.props?.fill ?? 'none',
|
|
305
|
+
font: defaultArrowShape.props?.font ?? 'draw',
|
|
306
|
+
kind: focusedShape.kind ?? defaultArrowShape.props?.kind ?? 'arc',
|
|
307
|
+
labelColor: defaultArrowShape.props?.labelColor ?? 'black',
|
|
308
|
+
labelPosition: defaultArrowShape.props?.labelPosition ?? 0.5,
|
|
309
|
+
richText,
|
|
310
|
+
scale: defaultArrowShape.props?.scale ?? 1,
|
|
311
|
+
size: defaultArrowShape.props?.size ?? 's',
|
|
312
|
+
start: { x: x1 - minX, y: y1 - minY },
|
|
313
|
+
},
|
|
314
|
+
meta: {
|
|
315
|
+
note: focusedShape.note ?? defaultArrowShape.meta?.note ?? '',
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const bindings: TLBindingCreate[] = []
|
|
320
|
+
|
|
321
|
+
if (focusedShape.fromId) {
|
|
322
|
+
const fromId = convertSimpleIdToTldrawId(focusedShape.fromId)
|
|
323
|
+
const startShape = editor.getShape(fromId)
|
|
324
|
+
if (startShape) {
|
|
325
|
+
bindings.push({
|
|
326
|
+
type: 'arrow',
|
|
327
|
+
typeName: 'binding',
|
|
328
|
+
fromId: shapeId,
|
|
329
|
+
toId: startShape.id,
|
|
330
|
+
props: {
|
|
331
|
+
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
332
|
+
isExact: false,
|
|
333
|
+
isPrecise: false,
|
|
334
|
+
terminal: 'start',
|
|
335
|
+
},
|
|
336
|
+
meta: {},
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (focusedShape.toId) {
|
|
342
|
+
const toId = convertSimpleIdToTldrawId(focusedShape.toId)
|
|
343
|
+
const endShape = editor.getShape(toId)
|
|
344
|
+
if (endShape) {
|
|
345
|
+
bindings.push({
|
|
346
|
+
type: 'arrow',
|
|
347
|
+
typeName: 'binding',
|
|
348
|
+
fromId: shapeId,
|
|
349
|
+
toId: endShape.id,
|
|
350
|
+
props: {
|
|
351
|
+
normalizedAnchor: { x: 0.5, y: 0.5 },
|
|
352
|
+
isExact: false,
|
|
353
|
+
isPrecise: false,
|
|
354
|
+
terminal: 'end',
|
|
355
|
+
},
|
|
356
|
+
meta: {},
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
shape,
|
|
363
|
+
bindings: bindings.length > 0 ? bindings : undefined,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function convertGeoShapeToTldrawShape(
|
|
368
|
+
editor: Editor,
|
|
369
|
+
focusedShape: FocusedGeoShape,
|
|
370
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
371
|
+
): { shape: TLShape } {
|
|
372
|
+
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
|
373
|
+
const shapeType = convertFocusedGeoTypeToTldrawGeoGeoType(focusedShape._type)
|
|
374
|
+
const defaultGeoShape = defaultShape as TLGeoShape
|
|
375
|
+
|
|
376
|
+
let richText
|
|
377
|
+
if (focusedShape.text !== undefined) {
|
|
378
|
+
richText = toRichText(focusedShape.text)
|
|
379
|
+
} else if (defaultGeoShape.props?.richText) {
|
|
380
|
+
richText = defaultGeoShape.props.richText
|
|
381
|
+
} else {
|
|
382
|
+
richText = toRichText('')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let fill
|
|
386
|
+
if (focusedShape.fill !== undefined) {
|
|
387
|
+
fill = convertFocusedFillToTldrawFill(focusedShape.fill) ?? 'none'
|
|
388
|
+
} else if (defaultGeoShape.props?.fill) {
|
|
389
|
+
fill = defaultGeoShape.props.fill
|
|
390
|
+
} else {
|
|
391
|
+
fill = convertFocusedFillToTldrawFill('none')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
shape: {
|
|
396
|
+
id: shapeId,
|
|
397
|
+
type: 'geo',
|
|
398
|
+
typeName: 'shape',
|
|
399
|
+
x: focusedShape.x ?? defaultGeoShape.x ?? 0,
|
|
400
|
+
y: focusedShape.y ?? defaultGeoShape.y ?? 0,
|
|
401
|
+
rotation: defaultGeoShape.rotation ?? 0,
|
|
402
|
+
index: defaultGeoShape.index ?? editor.getHighestIndexForParent(editor.getCurrentPageId()),
|
|
403
|
+
parentId: defaultGeoShape.parentId ?? editor.getCurrentPageId(),
|
|
404
|
+
isLocked: defaultGeoShape.isLocked ?? false,
|
|
405
|
+
opacity: defaultGeoShape.opacity ?? 1,
|
|
406
|
+
props: {
|
|
407
|
+
align: focusedShape.textAlign ?? defaultGeoShape.props?.align ?? 'start',
|
|
408
|
+
color: asColor(focusedShape.color ?? defaultGeoShape.props?.color ?? 'black'),
|
|
409
|
+
dash: defaultGeoShape.props?.dash ?? 'draw',
|
|
410
|
+
fill,
|
|
411
|
+
font: defaultGeoShape.props?.font ?? 'draw',
|
|
412
|
+
geo: shapeType,
|
|
413
|
+
growY: defaultGeoShape.props?.growY ?? 0,
|
|
414
|
+
h: focusedShape.h ?? defaultGeoShape.props?.h ?? 100,
|
|
415
|
+
labelColor: defaultGeoShape.props?.labelColor ?? 'black',
|
|
416
|
+
richText,
|
|
417
|
+
scale: defaultGeoShape.props?.scale ?? 1,
|
|
418
|
+
size: defaultGeoShape.props?.size ?? 's',
|
|
419
|
+
url: defaultGeoShape.props?.url ?? '',
|
|
420
|
+
verticalAlign: defaultGeoShape.props?.verticalAlign ?? 'start',
|
|
421
|
+
w: focusedShape.w ?? defaultGeoShape.props?.w ?? 100,
|
|
422
|
+
},
|
|
423
|
+
meta: {
|
|
424
|
+
note: focusedShape.note ?? defaultGeoShape.meta?.note ?? '',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function convertNoteShapeToTldrawShape(
|
|
431
|
+
editor: Editor,
|
|
432
|
+
focusedShape: FocusedNoteShape,
|
|
433
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
434
|
+
): { shape: TLShape } {
|
|
435
|
+
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
|
436
|
+
const defaultNoteShape = defaultShape as TLNoteShape
|
|
437
|
+
|
|
438
|
+
let richText
|
|
439
|
+
if (focusedShape.text !== undefined) {
|
|
440
|
+
richText = toRichText(focusedShape.text)
|
|
441
|
+
} else if (defaultNoteShape.props?.richText) {
|
|
442
|
+
richText = defaultNoteShape.props.richText
|
|
443
|
+
} else {
|
|
444
|
+
richText = toRichText('')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
shape: {
|
|
449
|
+
id: shapeId,
|
|
450
|
+
type: 'note',
|
|
451
|
+
typeName: 'shape',
|
|
452
|
+
x: focusedShape.x ?? defaultNoteShape.x ?? 0,
|
|
453
|
+
y: focusedShape.y ?? defaultNoteShape.y ?? 0,
|
|
454
|
+
rotation: defaultNoteShape.rotation ?? 0,
|
|
455
|
+
index: defaultNoteShape.index ?? editor.getHighestIndexForParent(editor.getCurrentPageId()),
|
|
456
|
+
parentId: defaultNoteShape.parentId ?? editor.getCurrentPageId(),
|
|
457
|
+
isLocked: defaultNoteShape.isLocked ?? false,
|
|
458
|
+
opacity: defaultNoteShape.opacity ?? 1,
|
|
459
|
+
props: {
|
|
460
|
+
color: asColor(focusedShape.color ?? defaultNoteShape.props?.color ?? 'black'),
|
|
461
|
+
richText,
|
|
462
|
+
size: defaultNoteShape.props?.size ?? 's',
|
|
463
|
+
align: defaultNoteShape.props?.align ?? 'middle',
|
|
464
|
+
font: defaultNoteShape.props?.font ?? 'draw',
|
|
465
|
+
fontSizeAdjustment: defaultNoteShape.props?.fontSizeAdjustment ?? 0,
|
|
466
|
+
growY: defaultNoteShape.props?.growY ?? 0,
|
|
467
|
+
labelColor: defaultNoteShape.props?.labelColor ?? 'black',
|
|
468
|
+
scale: defaultNoteShape.props?.scale ?? 1,
|
|
469
|
+
url: defaultNoteShape.props?.url ?? '',
|
|
470
|
+
verticalAlign: defaultNoteShape.props?.verticalAlign ?? 'middle',
|
|
471
|
+
textFirstEditedBy: defaultNoteShape.props?.textFirstEditedBy ?? null,
|
|
472
|
+
},
|
|
473
|
+
meta: {
|
|
474
|
+
note: focusedShape.note ?? defaultNoteShape.meta?.note ?? '',
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function convertDrawShapeToTldrawShape(
|
|
481
|
+
editor: Editor,
|
|
482
|
+
focusedShape: FocusedDrawShape,
|
|
483
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
484
|
+
): { shape: TLShape } {
|
|
485
|
+
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
|
486
|
+
const defaultDrawShape = defaultShape as TLDrawShape
|
|
487
|
+
|
|
488
|
+
let fill
|
|
489
|
+
if (focusedShape.fill !== undefined) {
|
|
490
|
+
fill = convertFocusedFillToTldrawFill(focusedShape.fill)
|
|
491
|
+
} else if (defaultDrawShape.props?.fill) {
|
|
492
|
+
fill = defaultDrawShape.props.fill
|
|
493
|
+
} else {
|
|
494
|
+
fill = convertFocusedFillToTldrawFill('none')
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
shape: {
|
|
499
|
+
id: shapeId,
|
|
500
|
+
type: 'draw',
|
|
501
|
+
typeName: 'shape',
|
|
502
|
+
x: defaultDrawShape.x ?? 0,
|
|
503
|
+
y: defaultDrawShape.y ?? 0,
|
|
504
|
+
rotation: defaultDrawShape.rotation ?? 0,
|
|
505
|
+
index: defaultDrawShape.index ?? editor.getHighestIndexForParent(editor.getCurrentPageId()),
|
|
506
|
+
parentId: defaultDrawShape.parentId ?? editor.getCurrentPageId(),
|
|
507
|
+
isLocked: defaultDrawShape.isLocked ?? false,
|
|
508
|
+
opacity: defaultDrawShape.opacity ?? 1,
|
|
509
|
+
props: {
|
|
510
|
+
...editor.getShapeUtil('draw').getDefaultProps(),
|
|
511
|
+
color: asColor(focusedShape.color ?? defaultDrawShape.props?.color ?? 'black'),
|
|
512
|
+
fill,
|
|
513
|
+
},
|
|
514
|
+
meta: {
|
|
515
|
+
note: focusedShape.note ?? defaultDrawShape.meta?.note ?? '',
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function convertUnknownShapeToTldrawShape(
|
|
522
|
+
editor: Editor,
|
|
523
|
+
focusedShape: FocusedUnknownShape,
|
|
524
|
+
{ defaultShape }: { defaultShape: Partial<TLShape> }
|
|
525
|
+
): { shape: TLShape } {
|
|
526
|
+
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
shape: {
|
|
530
|
+
id: shapeId,
|
|
531
|
+
type: defaultShape.type ?? 'geo',
|
|
532
|
+
typeName: 'shape',
|
|
533
|
+
x: focusedShape.x ?? defaultShape.x ?? 0,
|
|
534
|
+
y: focusedShape.y ?? defaultShape.y ?? 0,
|
|
535
|
+
rotation: defaultShape.rotation ?? 0,
|
|
536
|
+
index: defaultShape.index ?? editor.getHighestIndexForParent(editor.getCurrentPageId()),
|
|
537
|
+
parentId: defaultShape.parentId ?? editor.getCurrentPageId(),
|
|
538
|
+
isLocked: defaultShape.isLocked ?? false,
|
|
539
|
+
opacity: defaultShape.opacity ?? 1,
|
|
540
|
+
props: defaultShape.props ?? ({} as any),
|
|
541
|
+
meta: {
|
|
542
|
+
note: focusedShape.note ?? defaultShape.meta?.note ?? '',
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function getDummyBounds(editor: Editor, shape: TLShape): Box {
|
|
549
|
+
const bounds = editor.getShapePageBounds(shape)
|
|
550
|
+
if (bounds) return bounds
|
|
551
|
+
|
|
552
|
+
let dummyBounds: Box | undefined
|
|
553
|
+
const diff = editor.store.extractingChanges(() => {
|
|
554
|
+
editor.run(
|
|
555
|
+
() => {
|
|
556
|
+
const dummyId = createShapeId()
|
|
557
|
+
editor.createShape({ ...shape, id: dummyId })
|
|
558
|
+
dummyBounds = editor.getShapePageBounds(dummyId)
|
|
559
|
+
},
|
|
560
|
+
{ ignoreShapeLock: false, history: 'ignore' }
|
|
561
|
+
)
|
|
562
|
+
})
|
|
563
|
+
const reverseDiff = reverseRecordsDiff(diff)
|
|
564
|
+
editor.store.applyDiff(reverseDiff)
|
|
565
|
+
|
|
566
|
+
if (!dummyBounds) {
|
|
567
|
+
throw new Error('Failed to get bounds for shape')
|
|
568
|
+
}
|
|
569
|
+
return dummyBounds
|
|
570
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { type TLUiOverrides, useEditor, useToasts } from 'tldraw'
|
|
3
|
+
|
|
4
|
+
function extractImageFiles(data: DataTransfer | null): File[] {
|
|
5
|
+
if (!data) return []
|
|
6
|
+
const result: File[] = []
|
|
7
|
+
for (const item of data.items) {
|
|
8
|
+
if (item.kind === 'file' && item.type.startsWith('image/')) {
|
|
9
|
+
const file = item.getAsFile()
|
|
10
|
+
if (file) result.push(file)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (result.length > 0) return result
|
|
14
|
+
return [...data.files].filter((f) => f.type.startsWith('image/'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const COMING_SOON_TOAST = {
|
|
18
|
+
id: 'feature-coming-soon',
|
|
19
|
+
title: 'Coming soon',
|
|
20
|
+
description: 'This feature is coming soon!',
|
|
21
|
+
severity: 'info' as const,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Intercepts image drop/paste and shows a "coming soon" toast. */
|
|
25
|
+
export function ImageDropGuard() {
|
|
26
|
+
const editor = useEditor()
|
|
27
|
+
const { addToast } = useToasts()
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const container = editor.getContainer()
|
|
31
|
+
|
|
32
|
+
const showBlockedToast = (type: string) => {
|
|
33
|
+
addToast({
|
|
34
|
+
id: `blocked-${type}`,
|
|
35
|
+
title: 'Coming soon!',
|
|
36
|
+
description: `${type} are not yet supported in the tldraw MCP app.`,
|
|
37
|
+
severity: 'info',
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const onDrop = (e: DragEvent) => {
|
|
42
|
+
const imageFiles = extractImageFiles(e.dataTransfer)
|
|
43
|
+
if (imageFiles.length > 0) {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
e.stopPropagation()
|
|
46
|
+
showBlockedToast('Images')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const onPaste = (e: ClipboardEvent) => {
|
|
51
|
+
const imageFiles = extractImageFiles(e.clipboardData)
|
|
52
|
+
if (imageFiles.length > 0) {
|
|
53
|
+
e.preventDefault()
|
|
54
|
+
e.stopPropagation()
|
|
55
|
+
showBlockedToast('Images')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
container.addEventListener('drop', onDrop, { capture: true })
|
|
60
|
+
document.addEventListener('paste', onPaste, { capture: true })
|
|
61
|
+
|
|
62
|
+
// Override external content handlers to block images, embeds, and URLs.
|
|
63
|
+
// The context menu paste uses navigator.clipboard.read() directly,
|
|
64
|
+
// bypassing DOM paste events, so we need to intercept at this level too.
|
|
65
|
+
editor.registerExternalContentHandler('files', async ({ files }) => {
|
|
66
|
+
const hasImages = files.some((f) => f.type.startsWith('image/'))
|
|
67
|
+
if (hasImages) {
|
|
68
|
+
showBlockedToast('Images')
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
editor.registerExternalContentHandler('embed', async () => {
|
|
72
|
+
showBlockedToast('Embeds')
|
|
73
|
+
})
|
|
74
|
+
editor.registerExternalContentHandler('url', async () => {
|
|
75
|
+
showBlockedToast('Links')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
container.removeEventListener('drop', onDrop, { capture: true })
|
|
80
|
+
document.removeEventListener('paste', onPaste, { capture: true })
|
|
81
|
+
}
|
|
82
|
+
}, [editor, addToast])
|
|
83
|
+
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Override actions/tools to block media, embeds, and flatten. */
|
|
88
|
+
export const uiOverrides: TLUiOverrides = {
|
|
89
|
+
actions(_editor, actions, helpers) {
|
|
90
|
+
const { 'insert-media': _media, 'insert-embed': _embed, ...rest } = actions
|
|
91
|
+
return {
|
|
92
|
+
...rest,
|
|
93
|
+
'flatten-to-image': {
|
|
94
|
+
...actions['flatten-to-image'],
|
|
95
|
+
onSelect() {
|
|
96
|
+
helpers.addToast(COMING_SOON_TOAST)
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
tools(_editor, tools) {
|
|
102
|
+
// Remove the asset tool (image/media picker from toolbar)
|
|
103
|
+
const { asset: _asset, ...rest } = tools
|
|
104
|
+
return rest
|
|
105
|
+
},
|
|
106
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>tldraw MCP</title>
|
|
7
|
+
<style>
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
|
|
9
|
+
|
|
10
|
+
* {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
html,
|
|
16
|
+
body {
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
font-family: 'Inter', sans-serif;
|
|
21
|
+
}
|
|
22
|
+
#root {
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: 100%;
|
|
25
|
+
position: relative;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div id="root"></div>
|
|
31
|
+
<script type="module" src="./mcp-app.tsx"></script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|