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,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
|
+
}
|