@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/ARCHITECTURE.md +421 -0
- package/BENCHMARK.md +519 -0
- package/CLEAN_ROOM.md +50 -0
- package/COMPAT.md +282 -0
- package/CUSTOM_SHAPES.md +880 -0
- package/LICENSE +110 -16
- package/MIGRATION.md +807 -0
- package/README.md +24 -5
- package/UI.md +256 -0
- package/dist/index.d.ts +13091 -1419
- package/dist/index.js +12748 -2126
- package/dist/index.js.map +1 -1
- package/package.json +14 -16
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).
|