@mocanvas/editor 1.0.0 → 4.0.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/MIGRATION.md ADDED
@@ -0,0 +1,807 @@
1
+ # Migrating an existing tldraw app to mocanvas
2
+
3
+ This guide is for a team that already has a working app built on tldraw and
4
+ wants to move it to mocanvas. It assumes you have read
5
+ [COMPAT.md](COMPAT.md), which is the authoritative symbol-by-symbol map; this
6
+ document is the ordered procedure and the parts where a mechanical rename is
7
+ not enough.
8
+
9
+ The short version: your **documents, records, ids and `.tldr` files carry over
10
+ untouched**, most of your **`Editor` calls carry over untouched**, your
11
+ **`ShapeUtil` / `StateNode` / `BindingUtil` subclasses keep their shape**, and
12
+ the real work is (a) imports, (b) teaching custom shapes about the GPU
13
+ renderer, and (c) rebuilding UI, which has no slot compatibility yet.
14
+
15
+ ---
16
+
17
+ ## 0. Coming from mocanvas 1.x
18
+
19
+ 1.x was shaped after the tldraw 3.x API. 2.0.0 targets **tldraw 5.4**, which is a
20
+ different architecture in a few places rather than a set of renames. Everything
21
+ below is a real change; the rest of the API is unchanged.
22
+
23
+ **Indicators are canvas paths.** `indicator(shape): ReactNode` still works and is
24
+ deprecated. The new form returns a `Path2D` in shape-local space:
25
+
26
+ ```ts
27
+ override getIndicatorPath(shape: MyShape): Path2D {
28
+ const path = new Path2D()
29
+ path.rect(0, 0, shape.props.w, shape.props.h)
30
+ return path
31
+ }
32
+ ```
33
+
34
+ Returning `undefined` means *no outline*, not "use the default". A util that
35
+ implements neither method still gets a rectangle around its geometry bounds.
36
+
37
+ **Custom shapes register their props.** Without this, `shape.props` is `object`:
38
+
39
+ ```ts
40
+ declare module "@mocanvas/mocanvas" {
41
+ interface TLGlobalShapePropsMap {
42
+ myShape: MyShapeProps
43
+ }
44
+ }
45
+ ```
46
+
47
+ `Shape` with no type argument is now the union of *registered* types, and
48
+ `shape.type === "myShape"` narrows its props. A type nobody registered is an
49
+ `UnknownShape` — use that where the type is not known statically. Inside library
50
+ code that must handle any shape, `editor.getShape<UnknownShape>(id)`.
51
+
52
+ **`static props` are validators, not a plain object.** `T` is exported for this:
53
+
54
+ ```ts
55
+ static override props = { w: T.positiveNumber, h: T.positiveNumber, color: DefaultColorStyle }
56
+ ```
57
+
58
+ **`pageToScreen` changed meaning.** 1.x computed container-relative coordinates
59
+ under that name. Now `pageToViewport` is container-relative and `pageToScreen`
60
+ is window-relative. **If you position an overlay inside the canvas container,
61
+ you want `pageToViewport`** — the old call was silently correct only while the
62
+ container sat at the window origin.
63
+
64
+ **Geometry primitives follow the documented contract**, which changed a few
65
+ existing names and behaviours: `Box.expandBy` and `Mat.invert` now mutate and
66
+ return `this` (`Box.ExpandBy` and `Mat.Inverse` are the pure forms);
67
+ `Box.Expand(a, b)` is the union of two boxes, with the old scalar form kept as a
68
+ deprecated overload; `Vec.Dot` is now `Vec.Dpr` (alias kept) and `Vec.Cross`
69
+ returns a `Vec`, with the old scalar as `Vec.Cpr`. `Vec` gained `z` for pen
70
+ pressure, defaulting to `undefined` so `toJson()` is byte-for-byte what it was.
71
+
72
+ **Double click is reported in phases.** A handler that acts on every
73
+ `double_click` will now fire twice. Filter:
74
+
75
+ ```ts
76
+ override onDoubleClick(info: ClickEventInfo): void {
77
+ if (info.phase !== "up") return
78
+
79
+ }
80
+ ```
81
+
82
+ **`engine` is optional.** `new Editor({ store, shapeUtils, tools, getContainer })`
83
+ picks up whatever `loadEngine()` last produced, because importing
84
+ `@mocanvas/mocanvas` registers a provider. Pass one explicitly only to run two
85
+ editors on separate engines.
86
+
87
+ **Built-in shape migrations moved namespace.** If you registered a migration for
88
+ one of *your own* shape types, nothing changes — you keep `com.tldraw.shape.*`.
89
+ mocanvas's own built-ins moved to `com.mocanvas.shape.*` so they stop claiming
90
+ the reference implementation's migration line, which was making real `.tldr`
91
+ files fail to load. You do not need to do anything unless you deliberately
92
+ registered a sequence under a built-in type's id.
93
+
94
+ **Optional, and worth doing for large documents:** a built-in shape can now
95
+ describe its outline to the engine by parameters instead of uploading vertices
96
+ (`ShapeUtil.getEngineGeometry`). Custom shapes keep the `getGeometry` path and
97
+ need no change; see [ARCHITECTURE.md](ARCHITECTURE.md).
98
+
99
+ ---
100
+
101
+ ## 1. What stays the same
102
+
103
+ **Records.** The shape record is field-for-field the same:
104
+
105
+ ```ts
106
+ interface BaseShape<Type extends string, Props extends object> {
107
+ readonly id: ShapeId
108
+ readonly typeName: "shape"
109
+ type: Type
110
+ x: number
111
+ y: number
112
+ rotation: number
113
+ index: IndexKey
114
+ parentId: ParentId
115
+ isLocked: boolean
116
+ opacity: number
117
+ props: Props
118
+ meta: JsonObject
119
+ }
120
+ ```
121
+
122
+ Ids keep their prefixes and their format: `shape:`, `page:`, `binding:`,
123
+ `asset:`. `createShapeId()`, `isShapeId()`, `isShape()` are exported from
124
+ `@mocanvas/editor` (and re-exported from `@mocanvas/mocanvas`). `page`, `document`,
125
+ `camera`, `instance`, `instance_page_state`, `binding` and `asset` records keep
126
+ their names and their fields.
127
+
128
+ **Style values.** `color`, `fill`, `dash`, `size`, `font`, `align`,
129
+ `verticalAlign` keep the same string unions, so existing documents validate
130
+ without translation.
131
+
132
+ **`.tldr` files.** `parseTldrFile` / `serializeTldrFile` read and write the
133
+ `{ tldrawFileFormatVersion, schema, records }` envelope. Existing files load;
134
+ files you save are still `.tldr`.
135
+
136
+ **`Editor` method names.** `createShapes`, `updateShapes`, `deleteShapes`,
137
+ `getShape`, `getShapePageBounds`, `getShapeGeometry`, `select`, `selectAll`,
138
+ `selectNone`, `getSelectedShapeIds`, `setSelectedShapes`, `getCurrentPageId`,
139
+ `setCurrentPage`, `createPage`, `deletePage`, `getCamera`, `setCamera`,
140
+ `zoomIn`, `zoomOut`, `zoomToFit`, `zoomToSelection`, `zoomToBounds`,
141
+ `resetZoom`, `getZoomLevel`, `getViewportPageBounds`, `screenToPage`,
142
+ `pageToScreen`, `setCurrentTool`, `getCurrentToolId`, `getPath`, `isIn`,
143
+ `isInAny`, `getShapeAtPoint`, `getShapesAtPoint`, `getShapesInsideBounds`,
144
+ `markHistoryStoppingPoint` (with `mark` as an alias), `undo`, `redo`, `bail`,
145
+ `getCanUndo`, `getCanRedo`, `run`, `batch`, `getInstanceState`,
146
+ `updateInstanceState`, `getCurrentPageState`, `updateCurrentPageState`,
147
+ `setEditingShape`, `getEditingShapeId`, `setHoveredShape`, `getHoveredShapeId`,
148
+ `bringToFront`, `sendToBack`, `bringForward`, `sendBackward`, `reparentShapes`,
149
+ `groupShapes`, `ungroupShapes`, `duplicateShapes`, `getContentFromCurrentPage`,
150
+ `putContentOntoCurrentPage`, `nudgeShapes`, `rotateShapesBy`, `flipShapes`,
151
+ `alignShapes`, `distributeShapes`, `stackShapes`, `toggleLock` — all present
152
+ with the same names. `editor.store`, `editor.inputs`, `editor.sideEffects`,
153
+ `editor.snaps` and `editor.history` are fields, as before.
154
+
155
+ **Lifecycles.** `ShapeUtil` keeps `static type`, `static props`,
156
+ `static migrations`, `getDefaultProps`, `getGeometry`, `component`,
157
+ `getIndicatorPath`, the `can*` / `hide*` predicates, and the whole
158
+ `onBeforeCreate` / `onBeforeUpdate` / `onResize*` / `onTranslate*` /
159
+ `onRotate*` / `onDoubleClick*` / `onEditEnd` / `onChildrenChange` /
160
+ `onDragShapesOver` / `onDragShapesOut` / `onDropShapesOver` /
161
+ `getHandles` / `onHandleDrag` set. `StateNode` keeps `id`, `initial`,
162
+ `children`, `parent`, `editor`, `onEnter`, `onExit`, the pointer/keyboard/wheel
163
+ handlers, `onCancel`, `onComplete`, `onInterrupt`, `onTick`, `transition`,
164
+ `getCurrent`, `getIsActive`, `getPath`. `BindingUtil` keeps `static type`,
165
+ `getDefaultProps` and the `onBefore*` / `onAfter*` callbacks including
166
+ `onAfterChangeFromShape`, `onAfterChangeToShape`, `onBeforeDeleteFromShape`,
167
+ `onBeforeDeleteToShape`, `onBeforeIsolateFromShape`, `onBeforeIsolateToShape`.
168
+
169
+ ---
170
+
171
+ ## 2. Imports
172
+
173
+ ### Step 0 — the zero-rename step
174
+
175
+ Point every import at `@mocanvas/compat` and change nothing else. That package
176
+ is pure re-exports (no runtime code): it re-exports everything from `@mocanvas/mocanvas`
177
+ and adds back the `TL`-prefixed names.
178
+
179
+ ```ts
180
+ // before
181
+ import { Tldraw, type TLShape, type TLShapeId } from "tldraw"
182
+ // after — same identifiers
183
+ import { Tldraw, type TLShape, type TLShapeId } from "@mocanvas/compat"
184
+ ```
185
+
186
+ The aliases `@mocanvas/compat` actually exports:
187
+
188
+ | Value alias | Is |
189
+ | ---------------- | ---------------------- |
190
+ | `Tldraw` | `Mocanvas` |
191
+ | `TldrawEditor` | `Canvas` |
192
+ | `createTLStore` | `createStore` |
193
+ | `createTLSchema` | `createSchema` |
194
+
195
+ | Type alias | Is | | Type alias | Is |
196
+ | ---------- | -- | - | ---------- | -- |
197
+ | `TLEditor` | `Editor` | | `TLRecord` | `EditorRecord` |
198
+ | `TLStore` | `EditorStore` | | `TLStoreSnapshot` | `EditorStoreSnapshot` |
199
+ | `TLShape` | `Shape` | | `TLUnknownShape` | `UnknownShape` |
200
+ | `TLShapeId` | `ShapeId` | | `TLParentId` | `ParentId` |
201
+ | `TLShapePartial<T>` | `ShapePartial<T>` | | `TLShapeCreate<T>` | `ShapeCreate<T>` |
202
+ | `TLPage` | `Page` | | `TLPageId` | `PageId` |
203
+ | `TLDocument` | `Document` | | `TLCamera` | `Camera` |
204
+ | `TLCameraId` | `CameraId` | | `TLInstance` | `Instance` |
205
+ | `TLInstancePageState` | `InstancePageState` | | `TLBinding` | `UnknownBinding` |
206
+ | `TLBindingId` | `BindingId` | | `TLArrowBinding` | `ArrowBinding` |
207
+ | `TLGeoShape` | `GeoShape` | | `TLDrawShape` | `DrawShape` |
208
+ | `TLLineShape` | `LineShape` | | `TLArrowShape` | `ArrowShape` |
209
+ | `TLTextShape` | `TextShape` | | `TLNoteShape` | `NoteShape` |
210
+ | `TLFrameShape` | `FrameShape` | | `TLGroupShape` | `GroupShape` |
211
+ | `TLGeoShapeGeoStyle` | `GeoShapeKind` | | `TLDefaultColorStyle` | `DefaultColorStyle` |
212
+ | `TLDefaultDashStyle` | `DefaultDashStyle` | | `TLDefaultFillStyle` | `DefaultFillStyle` |
213
+ | `TLDefaultFontStyle` | `DefaultFontStyle` | | `TLDefaultSizeStyle` | `DefaultSizeStyle` |
214
+ | `TLDefaultHorizontalAlignStyle` | `DefaultHorizontalAlignStyle` | | `TLDefaultVerticalAlignStyle` | `DefaultVerticalAlignStyle` |
215
+ | `TLEventInfo` | `EventInfo` | | `TLPointerEventInfo` | `PointerEventInfo` |
216
+ | `TLClickEventInfo` | `ClickEventInfo` | | `TLKeyboardEventInfo` | `KeyboardEventInfo` |
217
+ | `TLWheelEventInfo` | `WheelEventInfo` | | `TLHandle` | `ShapeHandle` |
218
+ | `TLSelectionHandle` | `SelectionHandle` | | `TLResizeInfo<T>` | `ResizeInfo<T>` |
219
+ | `TLShapeUtilConstructor<T>` | `ShapeUtilConstructor<T>` | | `TLAnyShapeUtilConstructor` | `ShapeUtilConstructor` |
220
+ | `TLStateNodeConstructor` | `StateNodeConstructor` | | `TLBindingUtilConstructor` | `BindingUtilConstructor` |
221
+
222
+ Anything not in that list has the same name in both worlds and comes through
223
+ the `export * from "@mocanvas/mocanvas"` at the top of the package.
224
+
225
+ ### Step 1 — package map
226
+
227
+ | Old import | New import | Note |
228
+ | ------------------------------- | ------------------------- | ---- |
229
+ | `tldraw` | `@mocanvas/mocanvas` | `<Mocanvas />`, default shapes, tools, UI, `.tldr` helpers, export helpers |
230
+ | `@tldraw/editor` | `@mocanvas/editor` | `Editor`, `ShapeUtil`, `StateNode`, `BindingUtil`, geometry, `<Canvas />` |
231
+ | `@tldraw/store` | `@mocanvas/store` | records, `Store`, `StoreSchema`, migrations, `.tldr` IO |
232
+ | `@tldraw/state` | `@mocanvas/state` | `atom`, `computed`, `react`, `transact` |
233
+ | `@tldraw/state-react` | `@mocanvas/state/react` | `useValue`, `track`, `useAtom` |
234
+ | `@tldraw/tlschema` | `@mocanvas/editor` | record and prop types live with the editor |
235
+ | any of the above (first pass) | `@mocanvas/compat` | keeps the `TL*` names |
236
+
237
+ Everything `@mocanvas/editor` exports is re-exported by `@mocanvas/mocanvas`, so in app
238
+ code you can import from `@mocanvas/mocanvas` alone.
239
+
240
+ ### Step 2 — drop the prefixes
241
+
242
+ Once the app builds and runs against `@mocanvas/compat`, rename `TLFoo` →
243
+ `Foo` file by file and move imports to `@mocanvas/mocanvas` / `@mocanvas/editor`. Nothing
244
+ forces you to finish this in one pass.
245
+
246
+ ---
247
+
248
+ ## 3. The rendering model, and what it means for your custom shapes
249
+
250
+ This is the substantive difference and the only part of a migration that is not
251
+ mechanical.
252
+
253
+ In mocanvas, built-in shapes are **GPU meshes**, not DOM nodes. The engine
254
+ culls, tessellates and batches in WebAssembly and the canvas issues roughly one
255
+ draw call per batch. React components still exist, but they are a **DOM overlay
256
+ layer** drawn above the GPU canvas, used for text editing, custom shapes that
257
+ opt out of the GPU path, and labels.
258
+
259
+ A `ShapeUtil` therefore has three rendering-related decisions.
260
+
261
+ ### `getRenderStyle` — put the shape on the GPU
262
+
263
+ ```ts
264
+ getRenderStyle(shape: T): StyleWords | null
265
+ ```
266
+
267
+ The base implementation returns `null`, which means *"do not draw me on the
268
+ GPU; render `component` in the DOM overlay instead"*. **A custom shape ported
269
+ from tldraw with no changes keeps working through this path** — it just renders
270
+ like it did before, as DOM. That is the safe first move.
271
+
272
+ To move it onto the GPU, return `StyleWords`:
273
+
274
+ ```ts
275
+ interface StyleWords {
276
+ /** 0xRRGGBBAA, alpha 0 = none */
277
+ fill: number
278
+ stroke: number
279
+ strokeWidth: number
280
+ dash: number
281
+ opacity: number
282
+ /** host texture id; 0 or undefined = none */
283
+ texture?: number
284
+ }
285
+ ```
286
+
287
+ Colours are packed `0xRRGGBBAA` integers, not CSS strings. Convert with
288
+ `hexToRgba`, exported from `@mocanvas/editor`:
289
+
290
+ ```ts
291
+ export function hexToRgba(hex: string, alpha = 1): number
292
+ ```
293
+
294
+ It accepts `#rgb`, `#rrggbb` and `#rrggbbaa`. `dash` is `0` solid, `1` dashed,
295
+ `2` dotted, `3` draw. `strokeWidth` is in page units and scales with zoom.
296
+
297
+ The geometry that gets tessellated is whatever `getGeometry` returns — the
298
+ `Geometry2d` is serialized to the engine's flat path encoding, so your existing
299
+ `getGeometry` is reused as-is.
300
+
301
+ ### `component` — the DOM overlay
302
+
303
+ `component(shape)` is unchanged in signature and still returns a `ReactNode`.
304
+ What changed is *when* it runs:
305
+
306
+ - `getRenderStyle` returns `null` → `component` is the only renderer.
307
+ - `getRenderStyle` returns a style → `component` runs only if `needsOverlay` or
308
+ `hasOverlayLabel` says so.
309
+
310
+ Overlay shapes are positioned in page space by the overlay layer; render inside
311
+ the shape's local box starting at `(0, 0)` and do not add your own page
312
+ transform.
313
+
314
+ ### `needsOverlay` — temporarily promote to DOM
315
+
316
+ ```ts
317
+ needsOverlay(shape: T): boolean // default: editor.getEditingShapeId() === shape.id
318
+ ```
319
+
320
+ The default promotes a shape to the overlay while it is being edited, which is
321
+ what a text-editing shape wants. `NoteShapeUtil` overrides it to `false`
322
+ because the GPU keeps drawing the sticky background even while its label is
323
+ being edited.
324
+
325
+ ### `hasOverlayLabel` — GPU body plus a DOM label
326
+
327
+ ```ts
328
+ hasOverlayLabel(shape: T): boolean // default: false
329
+ ```
330
+
331
+ Return `true` to have the shape drawn on the GPU **and** reported in the
332
+ overlay list, so `component` can render a text label on top of the mesh. This is
333
+ how `geo`, `note`, `arrow` and `frame` carry labels.
334
+
335
+ ### `isClipShape` — clip descendants
336
+
337
+ ```ts
338
+ isClipShape(shape: T): boolean // default: false
339
+ ```
340
+
341
+ Return `true` to clip every descendant to this shape's page-space geometry
342
+ bounds (the shape itself is not clipped by its own rect; nested clips
343
+ intersect). The engine sets a scissor rect per batch. No built-in shape opts in
344
+ yet; GPU frame clipping is a phase 3 item.
345
+
346
+ ### Textures
347
+
348
+ A shape's fill can be a texture instead of a colour: set
349
+ `StyleWords.texture` to a non-zero host texture id and upload the pixels
350
+ through the render backend:
351
+
352
+ ```ts
353
+ uploadTexture(id: number, source: TextureSource, opts?: TextureOptions): void
354
+ ```
355
+
356
+ The fill mesh is then replaced by one quad over the shape's local geometry
357
+ bounds, `uv (0,0)` at the min corner and `(1,1)` at the max, tinted white ×
358
+ opacity; the stroke is still drawn normally. Texture ids are allocated by you,
359
+ the host. The built-in `image` shape does **not** use this yet — it renders an
360
+ `<img>` in the DOM overlay (`getImageTextureSource` is the seam where the GPU
361
+ path will attach).
362
+
363
+ ### Porting checklist for one custom shape
364
+
365
+ 1. Move the import, keep the class.
366
+ 2. Build and run. It renders through the DOM overlay; nothing else to do.
367
+ 3. If it is a hot shape (thousands on a page), implement `getRenderStyle` and
368
+ let `getGeometry` do the drawing; delete the SVG/DOM body from `component`
369
+ and keep only the label, if any, behind `hasOverlayLabel`.
370
+
371
+ ---
372
+
373
+ ## 4. `<Mocanvas />`
374
+
375
+ The batteries-included component. Full prop list:
376
+
377
+ ```ts
378
+ interface MocanvasProps {
379
+ store?: EditorStore
380
+ shapeUtils?: readonly ShapeUtilConstructor[]
381
+ bindingUtils?: readonly BindingUtilConstructor[]
382
+ tools?: readonly StateNodeConstructor[]
383
+ initialState?: string
384
+ onMount?: (editor: Editor) => void | (() => void)
385
+ hideUi?: boolean
386
+ showStats?: boolean
387
+ className?: string
388
+ style?: CSSProperties
389
+ children?: ReactNode
390
+ components?: CanvasProps["components"]
391
+ options?: ConstructorParameters<typeof Editor>[0]["options"]
392
+ }
393
+ ```
394
+
395
+ Points worth knowing:
396
+
397
+ - `shapeUtils`, `bindingUtils` and `tools` are **additive**: they are appended
398
+ to `defaultShapeUtils`, `defaultBindingUtils` and `defaultTools`. You do not
399
+ re-list the built-ins, and there is no "replace the defaults" mode.
400
+ - `onMount` may return a cleanup function; it runs on unmount before
401
+ `editor.dispose()`.
402
+ - The engine is WebAssembly and loads asynchronously, so `<Mocanvas />` shows a
403
+ loading placeholder for one tick before the editor exists. `onMount` is the
404
+ reliable hook for "the editor is ready".
405
+ - `children` render inside `<Canvas>`, above the canvas element.
406
+ - `options` are editor config overrides (`maxShapesPerPage`, `hitTestMargin`,
407
+ `zoomMin`, `zoomMax`, `zoomSteps`, `backgroundColor`, ...).
408
+
409
+ For a bare canvas with no default shapes, tools or UI, construct `Editor`
410
+ yourself and render `<Canvas editor={editor} />` from `@mocanvas/editor`
411
+ (`TldrawEditor` in the compat package).
412
+
413
+ ---
414
+
415
+ ## 5. Styles
416
+
417
+ Styles work the way you expect. `StyleProp.define` and `StyleProp.defineEnum`:
418
+
419
+ ```ts
420
+ static define<T>(id: string, options: { defaultValue: T; validate?: (value: unknown) => T }): StyleProp<T>
421
+ static defineEnum<const V extends readonly string[]>(
422
+ id: string,
423
+ options: { defaultValue: V[number]; values: V },
424
+ ): EnumStyleProp<V[number]>
425
+ ```
426
+
427
+ **The built-in style prop ids use the `mocanvas:` namespace**, not `tldraw:` —
428
+ `mocanvas:color`, `mocanvas:labelColor`, `mocanvas:fill`, `mocanvas:dash`,
429
+ `mocanvas:size`, `mocanvas:font`, `mocanvas:horizontalAlign`,
430
+ `mocanvas:verticalAlign`, `mocanvas:geo`. The *values* are unchanged, so
431
+ documents are unaffected; only code that hard-codes a style id needs a look.
432
+ For your own styles, namespace them with your app's name.
433
+
434
+ Declare styles on the util's `static props` map, keyed by the prop name:
435
+
436
+ ```ts
437
+ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
438
+ static override type = "card" as const
439
+ static override props = {
440
+ color: DefaultColorStyle,
441
+ size: DefaultSizeStyle,
442
+ }
443
+ }
444
+ ```
445
+
446
+ `getStylePropsOf` picks the `StyleProp` instances out of that map; the editor
447
+ uses it for `editor.getStylePropsForType(type)`. Reading and writing:
448
+
449
+ ```ts
450
+ editor.getStyleForNextShape(DefaultColorStyle) // T
451
+ editor.setStyleForNextShapes(DefaultColorStyle, "blue") // this
452
+ editor.setStyleForSelectedShapes(DefaultColorStyle, "blue")
453
+ editor.getSharedStyles() // SharedStyleMap
454
+ ```
455
+
456
+ `SharedStyleMap` entries are `{ type: "shared"; value: T } | { type: "mixed" }`;
457
+ `getAsKnownValue(prop)` returns the value or `undefined` when mixed.
458
+
459
+ ---
460
+
461
+ ## 6. Bindings
462
+
463
+ `BindingUtil` is present with the same lifecycle. Records:
464
+
465
+ ```ts
466
+ interface BaseBinding<Type extends string, Props extends object> {
467
+ readonly id: BindingId
468
+ readonly typeName: "binding"
469
+ type: Type
470
+ fromId: ShapeId
471
+ toId: ShapeId
472
+ props: Props
473
+ meta: JsonObject
474
+ }
475
+ ```
476
+
477
+ Editor methods: `createBinding` / `createBindings`, `updateBinding` /
478
+ `updateBindings`, `deleteBinding` / `deleteBindings` (which take
479
+ `{ isolateShapes?: boolean }`), `getBinding`, `getBindingsFromShape`,
480
+ `getBindingsToShape`, `getBindingsInvolvingShape`, `getBindingUtil`,
481
+ `hasBindingUtil`.
482
+
483
+ Callbacks run inside the store transaction that caused them:
484
+ `onBeforeCreate`, `onAfterCreate`, `onBeforeChange`, `onAfterChange`,
485
+ `onBeforeDelete`, `onAfterDelete`, `onAfterChangeFromShape`,
486
+ `onAfterChangeToShape`, `onBeforeDeleteFromShape`, `onBeforeDeleteToShape`,
487
+ `onBeforeIsolateFromShape`, `onBeforeIsolateToShape`. `getDefaultProps` is
488
+ abstract and returns `Partial<B["props"]>`.
489
+
490
+ The built-in `ArrowBindingUtil` (type `"arrow"`) stores
491
+ `{ terminal, normalizedAnchor, isExact, isPrecise }`, matching what `.tldr`
492
+ files hold for arrow bindings, so arrow documents round-trip. Register custom
493
+ binding utils through `<Mocanvas bindingUtils={[...]} />`.
494
+
495
+ Clipboard content is `{ shapes, bindings }`: `getContentFromCurrentPage`
496
+ returns both and `putContentOntoCurrentPage` accepts both.
497
+
498
+ ---
499
+
500
+ ## 7. UI
501
+
502
+ **There is no slot compatibility yet**, and it is an explicit v1 non-goal. Do
503
+ not expect your existing overrides of tldraw's UI components to compile.
504
+
505
+ What you have:
506
+
507
+ - `hideUi` on `<Mocanvas />` turns off the default toolbar and zoom bar
508
+ entirely, leaving you the canvas and your `children`.
509
+ - `components` (passed through to `<Canvas>`) lets you replace the canvas-level
510
+ render slots.
511
+ - `useEditor()` inside any descendant of `<Canvas>` / `<EditorProvider>`
512
+ returns the `Editor` (`useMaybeEditor()` returns `Editor | null`).
513
+ - `track(Component)` and `useValue` from `@mocanvas/state/react` — re-exported
514
+ from `@mocanvas/editor` and `@mocanvas/mocanvas` — make a component re-render when the
515
+ signals it reads change.
516
+
517
+ The practical migration is: `hideUi`, then rebuild your chrome as ordinary
518
+ React inside `<Mocanvas>`, reading and driving the editor through `useEditor`
519
+ and `track`. Because the default UI is still being reworked, treat its internals
520
+ as unstable and build against `useEditor` rather than against specific UI
521
+ components.
522
+
523
+ ---
524
+
525
+ ## 8. Files
526
+
527
+ Two helpers, both from `@mocanvas/mocanvas`:
528
+
529
+ ```ts
530
+ function serializeMocanvasFile(editor: Editor): string
531
+ function loadMocanvasFile(editor: Editor, json: unknown): ParseTldrFileResult
532
+ ```
533
+
534
+ `loadMocanvasFile` accepts the JSON text or an already-parsed value, replaces
535
+ the document, clears history, fixes up the current page if the file does not
536
+ contain it, and calls `zoomToFit`. It never throws: the result is
537
+
538
+ ```ts
539
+ type ParseTldrFileResult =
540
+ | { ok: true; schema: SerializedSchema; records: UnknownRecord[] }
541
+ | { ok: false; error: TldrFileParseError; cause?: unknown }
542
+ ```
543
+
544
+ with `error` one of `"notATldrFile" | "v1File" | "invalidRecords" | "futureVersion"`.
545
+
546
+ **What round-trips.** The `{ tldrawFileFormatVersion, schema, records }`
547
+ envelope, every document-scoped record as stored, the schema `sequences` map,
548
+ and unknown record types and unknown props — those survive load and save
549
+ untouched. Output is pretty-printed with recursively sorted keys, so identical
550
+ documents produce identical bytes.
551
+
552
+ **What is dropped.** `serializeMocanvasFile` writes the `"document"` scope
553
+ only, so session state — `camera`, `instance`, `instance_page_state` — is not
554
+ in the file. Your current selection, camera position, editing shape and hovered
555
+ shape do not survive a save/load, and neither does undo history, which is
556
+ cleared on load. The pre-envelope legacy format is detected and rejected with
557
+ `error: "v1File"` rather than converted.
558
+
559
+ ---
560
+
561
+ ## 9. Assets and external content
562
+
563
+ Assets are **document-scoped** records, not per page:
564
+
565
+ ```ts
566
+ editor.getAsset<A>(id) editor.getAssets()
567
+ editor.createAsset(asset) editor.createAssets(assets)
568
+ editor.updateAsset(partial) editor.updateAssets(partials)
569
+ editor.deleteAsset(id) editor.deleteAssets(ids)
570
+ ```
571
+
572
+ `Asset` is `ImageAsset | VideoAsset | BookmarkAsset`. Image and video props are
573
+ `{ w, h, name, isAnimated, mimeType, src, fileSize? }`; bookmark props are
574
+ `{ title, description, image, favicon, src }`. `src` is a URL or data URL, and
575
+ `null` while uploading.
576
+
577
+ Drops and pastes go through the external-content pipeline:
578
+
579
+ ```ts
580
+ editor.registerExternalContentHandler<T extends ExternalContentType>(
581
+ type: T, handler: ExternalContentHandler<T> | null,
582
+ ): () => void
583
+
584
+ editor.registerExternalAssetHandler<T extends ExternalAssetType>(
585
+ type: T, handler: ExternalAssetHandler<T> | null,
586
+ ): () => void
587
+
588
+ editor.putExternalContent(info: ExternalContent): Promise<void>
589
+ editor.getAssetForExternalContent(info: ExternalAssetContent): Promise<Asset | undefined>
590
+ ```
591
+
592
+ Both register calls return an unregister function, and passing `null` clears
593
+ the handler. Content types are `files`, `text`, `url` and `svg-text`:
594
+
595
+ ```ts
596
+ type ExternalContent =
597
+ | { type: "files"; files: File[]; point?: { x: number; y: number } }
598
+ | { type: "text"; text: string; point?: { x: number; y: number } }
599
+ | { type: "url"; url: string; point?: { x: number; y: number } }
600
+ | { type: "svg-text"; text: string; point?: { x: number; y: number } }
601
+
602
+ type ExternalAssetContent = { type: "file"; file: File } | { type: "url"; url: string }
603
+ ```
604
+
605
+ `point` is in page space and defaults to the viewport centre.
606
+ `getAssetForExternalContent` **produces an asset record without storing it** —
607
+ you decide whether to `createAssets` it.
608
+
609
+ `<Mocanvas />` installs the defaults for you via `useExternalContent`; to wire
610
+ them up on a hand-built editor call
611
+ `registerDefaultExternalContentHandlers(editor, opts)`, which returns a single
612
+ teardown function. Its `ExternalContentOptions` let you swap the image-size
613
+ loader and cap the imported dimension (`DEFAULT_MAX_IMAGE_DIMENSION` is 1000).
614
+ Building blocks are exported individually if you want to compose your own:
615
+ `createImageAssetFromFile`, `createImageAssetFromSvgText`,
616
+ `createImageShapesForAssets`, `createTextShapeAt`, `classifyExternalText`,
617
+ `fitImageSize`, `isImageFile`, `isSvgFile`, `isAnimatedImageType`,
618
+ `looksLikeUrl`, `looksLikeSvg`, `readFileAsDataUrl`, `svgTextToDataUrl`,
619
+ `getSvgTextSize`, `loadImageSizeInBrowser`.
620
+
621
+ To upload dropped files to your own storage, register an asset handler that
622
+ returns an asset whose `props.src` is your URL — that is the same seam you used
623
+ before.
624
+
625
+ ---
626
+
627
+ ## 10. Known gaps
628
+
629
+ Taken from the "phase 3" and "later" rows of [COMPAT.md](COMPAT.md), plus what
630
+ the code confirms today.
631
+
632
+ | Gap | Status |
633
+ | --- | ------ |
634
+ | `editor.resizeShape`, `editor.stretchShapes` | phase 3. Interactive resize lives in the select tool; `ShapeUtil.onResize` and `BaseBoxShapeUtil` work, but there is no imperative resize entry point on `Editor`. |
635
+ | `editor.getSvgString`, `editor.toImage` | Not `Editor` methods. Export is a set of free functions in `@mocanvas/mocanvas`: `getSvgString(editor, ids?, opts?)`, `exportToBlob(editor, opts)`, `downloadBlob(blob, filename)`, `copyBlobToClipboard(blob)`. |
636
+ | `ShapeUtil.toSvg`, `ShapeUtil.toBackgroundSvg` | Not `ShapeUtil` members. Custom shapes contribute to SVG export through `registerShapeSvgRenderer(type, renderer)`; without one they fall back to `geometryFallbackSvg`. |
637
+ | `pointer` and `instance_presence` records | later — collaboration. No presence records, and no `mergeRemoteChanges` transport yet. |
638
+ | Sync protocol | Explicit v1 non-goal. Wire compatibility with tldraw's sync protocol is not planned for v1. |
639
+ | Slot-compatible UI | Explicit v1 non-goal. See §7. |
640
+ | `image` shape on the GPU | The texture path exists in the engine (`StyleWords.texture` + `uploadTexture`) but the `image` shape still draws an `<img>` in the DOM overlay. |
641
+ | `editor.textMeasure`, `editor.user`, `editor.menus` | Not implemented. Text measurement is DOM-backed inside the text layer; `editor.inputs` and `editor.sideEffects` do exist. |
642
+ | Text rendering | DOM overlay. Glyph-atlas text in WASM is phase 3. |
643
+ | GPU frame clipping | The `CLIP` flag and `isClipShape` hook are wired end to end, but no built-in shape enables it yet (phase 3). |
644
+ | WebGPU backend | Phase 3. WebGL2 is the only backend today, behind `RenderBackend`. |
645
+ | Real `.tldr` schema migrations | Phase 4. The `sequences` map is preserved and migrations run for sequences that are known; unknown ones pass through. |
646
+ | Pixel-identical built-in shapes | Explicit v1 non-goal. Built-ins are GPU meshes and will not match a DOM renderer pixel for pixel. |
647
+ | No watermark, license key or telemetry | Intentional difference, not a gap. |
648
+
649
+ ---
650
+
651
+ ## 11. Worked example: a small custom "card" shape
652
+
653
+ A sketch, not a copy of anything: a card with a coloured body and a title. The
654
+ *before* is how such a shape is typically written for a DOM renderer; the
655
+ *after* is the same shape in mocanvas, on the GPU with a DOM label.
656
+
657
+ ### Before (DOM renderer)
658
+
659
+ ```tsx
660
+ // Everything the shape looks like is in `component`.
661
+ export class CardShapeUtil extends ShapeUtil<CardShape> {
662
+ static override type = "card"
663
+
664
+ getDefaultProps(): CardShapeProps {
665
+ return { w: 220, h: 140, title: "", color: "blue" }
666
+ }
667
+
668
+ getGeometry(shape: CardShape) {
669
+ return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true })
670
+ }
671
+
672
+ component(shape: CardShape) {
673
+ return (
674
+ <div
675
+ style={{
676
+ width: shape.props.w,
677
+ height: shape.props.h,
678
+ background: PALETTE[shape.props.color],
679
+ border: "2px solid #1e1e1e",
680
+ borderRadius: 8,
681
+ display: "grid",
682
+ placeItems: "center",
683
+ }}
684
+ >
685
+ {shape.props.title}
686
+ </div>
687
+ )
688
+ }
689
+
690
+ override getIndicatorPath(shape: CardShape): Path2D {
691
+ const path = new Path2D()
692
+ path.rect(0, 0, shape.props.w, shape.props.h)
693
+ return path
694
+ }
695
+ }
696
+ ```
697
+
698
+ ### After (mocanvas)
699
+
700
+ Step 1 is *do nothing*: change the import to `@mocanvas/editor` and the class
701
+ above runs unmodified, rendered by the DOM overlay because `getRenderStyle`
702
+ defaults to `null`.
703
+
704
+ Step 2 moves the body onto the GPU and leaves only the title in the overlay:
705
+
706
+ ```tsx
707
+ import {
708
+ BaseBoxShapeUtil,
709
+ DefaultColorStyle,
710
+ DefaultSizeStyle,
711
+ hexToRgba,
712
+ LIGHT_THEME,
713
+ Rectangle2d,
714
+ type BaseShape,
715
+ type Geometry2d,
716
+ type StyleWords,
717
+ } from "@mocanvas/editor"
718
+ import type { ReactNode } from "react"
719
+
720
+ // `DefaultColorStyle` and `DefaultSizeStyle` are each both a value (the
721
+ // `StyleProp`) and a type (the value union), so one import serves both uses.
722
+ export interface CardShapeProps {
723
+ w: number
724
+ h: number
725
+ title: string
726
+ color: DefaultColorStyle
727
+ size: DefaultSizeStyle
728
+ }
729
+
730
+ export type CardShape = BaseShape<"card", CardShapeProps>
731
+
732
+ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
733
+ static override type = "card" as const
734
+ // These two props are now app-wide styles: the style panel edits them and
735
+ // `setStyleForSelectedShapes` reaches them.
736
+ static override props = {
737
+ color: DefaultColorStyle,
738
+ size: DefaultSizeStyle,
739
+ }
740
+
741
+ getDefaultProps(): CardShapeProps {
742
+ return { w: 220, h: 140, title: "", color: "blue", size: "m" }
743
+ }
744
+
745
+ getGeometry(shape: CardShape): Geometry2d {
746
+ return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true })
747
+ }
748
+
749
+ // The body: fill, stroke and dash as packed 0xRRGGBBAA words.
750
+ override getRenderStyle(shape: CardShape): StyleWords {
751
+ const theme = LIGHT_THEME[shape.props.color]
752
+ return {
753
+ fill: hexToRgba(theme.semi),
754
+ stroke: hexToRgba(theme.solid),
755
+ strokeWidth: 2,
756
+ dash: 0,
757
+ opacity: 1,
758
+ }
759
+ }
760
+
761
+ // Draw the GPU body *and* report the shape to the overlay, so `component`
762
+ // can put the title on top of the mesh.
763
+ override hasOverlayLabel(shape: CardShape): boolean {
764
+ return shape.props.title.length > 0
765
+ }
766
+
767
+ // Only the label now — the box itself is a mesh.
768
+ component(shape: CardShape): ReactNode {
769
+ const { w, h, title, color } = shape.props
770
+ return (
771
+ <div
772
+ style={{
773
+ width: w,
774
+ height: h,
775
+ display: "grid",
776
+ placeItems: "center",
777
+ color: LIGHT_THEME[color].solid,
778
+ pointerEvents: "none",
779
+ }}
780
+ >
781
+ {title}
782
+ </div>
783
+ )
784
+ }
785
+
786
+ override getIndicatorPath(shape: CardShape): Path2D {
787
+ const path = new Path2D()
788
+ path.rect(0, 0, shape.props.w, shape.props.h)
789
+ return path
790
+ }
791
+ }
792
+ ```
793
+
794
+ Register it, additively:
795
+
796
+ ```tsx
797
+ <Mocanvas shapeUtils={[CardShapeUtil]} />
798
+ ```
799
+
800
+ What changed, and only this: `getRenderStyle` was added, `hasOverlayLabel` was
801
+ added, `component` shrank to the label, `extends ShapeUtil` became
802
+ `extends BaseBoxShapeUtil` to inherit `onResize`, and two props were promoted to
803
+ styles with `static props`. `getGeometry`, `getIndicatorPath`, `getDefaultProps`, the
804
+ record and the props are the same code you already had.
805
+
806
+ For a shape written from scratch — including tools, handles and geometry
807
+ composition — see [CUSTOM_SHAPES.md](CUSTOM_SHAPES.md).