@mocanvas/editor 4.8.3 → 4.8.5

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/AGENTS.md ADDED
@@ -0,0 +1,476 @@
1
+ # Driving the editor from an agent
2
+
3
+ mocanvas ships no model, no prompt and no agent. It ships a document that exists
4
+ without a browser, and one `Editor` API that a person's toolbar and a program's
5
+ code both call. This guide is for somebody pointing a model at that API: how to
6
+ build an editor with no screen, write to it, read the document back, and where
7
+ the sharp edges are. Everything here is public API from `@mocanvas/mocanvas` /
8
+ `@mocanvas/editor`.
9
+
10
+ Contents:
11
+
12
+ 1. [The shape of the thing](#1-the-shape-of-the-thing)
13
+ 2. [A document with no browser](#2-a-document-with-no-browser)
14
+ 3. [Writing](#3-writing)
15
+ 4. [Reading back](#4-reading-back)
16
+ 5. [Keeping an agent's writes out of a person's undo stack](#5-keeping-an-agents-writes-out-of-a-persons-undo-stack)
17
+ 6. [Reproducible runs](#6-reproducible-runs)
18
+ 7. [Validation on the way in](#7-validation-on-the-way-in)
19
+ 8. [A worked example: a plan, drawn](#8-a-worked-example-a-plan-drawn)
20
+
21
+ ---
22
+
23
+ ## 1. The shape of the thing
24
+
25
+ There is no agent API. An agent calls `editor.createShapes(...)`, and so does the
26
+ toolbar; an agent calls `editor.markHistoryStoppingPoint("style")`, and so does
27
+ the style panel — that exact call is in `packages/mocanvas/src/ui/StylePanel.tsx`.
28
+ "Convert to embed", "fit frame to content" and "auto size" in
29
+ `packages/mocanvas/src/ui/menu-items.tsx` are each a `markHistoryStoppingPoint`
30
+ followed by ordinary editor calls.
31
+
32
+ Three things follow. **There is no second surface to keep in sync** — a method an
33
+ agent can reach is one the UI already exercises, under the same tests. **Nothing
34
+ an agent does is outside the document**: writes go through `editor.run`, which
35
+ opens one store operation, so a sync layer sees an agent's work the way it sees a
36
+ person's. And **nothing an agent does is un-undoable by a person** — an agent's
37
+ writes are recorded steps by default, so Cmd+Z takes them back. §5 covers turning
38
+ that off, and what turning it off does not promise. The UI layer is described in
39
+ [UI.md](UI.md); none of it is required.
40
+
41
+ ---
42
+
43
+ ## 2. A document with no browser
44
+
45
+ `createStore()` builds a store with the editor's record types and seeds the two
46
+ records every document has — the document itself and page 1 — so a store no
47
+ editor has touched is already a readable document:
48
+
49
+ ```ts
50
+ import { createStore, defaultShapeUtils, defaultBindingUtils } from "@mocanvas/mocanvas"
51
+
52
+ const store = createStore({ shapeUtils: defaultShapeUtils, bindingUtils: defaultBindingUtils })
53
+ ```
54
+
55
+ Pass the same util lists you will give the editor: their `static migrations` are
56
+ collected into the schema, so a board persisted before a prop existed is backfilled
57
+ on load. Pass `seed: false` when you will supply your own page afterwards, or that
58
+ store ends up with two pages at the same index — and equal indices have no defined
59
+ order.
60
+
61
+ The WebAssembly engine loads once per realm: `await loadEngine()` in a browser,
62
+ which needs no bundler configuration, and `loadEngineSync(bytes)` in Node — what
63
+ this repository's own tests use.
64
+
65
+ ```ts
66
+ import { readFileSync } from "node:fs"
67
+ import { fileURLToPath } from "node:url"
68
+ import { loadEngineSync } from "@mocanvas/mocanvas"
69
+
70
+ const wasmPath = fileURLToPath(import.meta.resolve("@mocanvas/wasm/pkg/mocanvas_bg.wasm"))
71
+ const engine = loadEngineSync(readFileSync(wasmPath))
72
+ ```
73
+
74
+ `await loadEngine()` does work in Node, but not by the route you want: it fetches
75
+ `new URL("../pkg/mocanvas_bg.wasm", import.meta.url)`, Node's `fetch` cannot read a
76
+ `file:` URL, and the loader falls back to a base64 copy embedded in the package
77
+ with a one-time warning.
78
+
79
+ ### The editor, with no element
80
+
81
+ ```ts
82
+ import { Editor, defaultShapeUtils, defaultBindingUtils, defaultTools } from "@mocanvas/mocanvas"
83
+
84
+ const editor = new Editor({
85
+ store,
86
+ shapeUtils: defaultShapeUtils,
87
+ bindingUtils: defaultBindingUtils,
88
+ tools: defaultTools,
89
+ engine,
90
+ })
91
+ ```
92
+
93
+ **No `getContainer`.** It became optional in 4.8.3, and optional is the point: an
94
+ editor with no screen has no element to give, and requiring one made every headless
95
+ caller begin by inventing a fake DOM node (`getContainer: () => ({}) as HTMLElement`).
96
+ Nothing inside ever needed it — `Editor`'s private `safeContainer()` has always
97
+ treated a missing or throwing container as "no container". It was the type
98
+ insisting, not the runtime.
99
+
100
+ Ask for the element anyway and `editor.getContainer()` — and `editor.container`,
101
+ which calls it — **throws**, saying this editor was built without `getContainer` so
102
+ it has no container, which is expected for a headless editor: a stated condition
103
+ rather than a `TypeError` two layers down. Everything that merely *looks* for one
104
+ copes. `getContainerDocument()` and `getContainerWindow()` return `undefined`, and
105
+ `focus()` and `blur()` are DOM no-ops that still record the state on the instance
106
+ record, so `getIsFocused()` stays answerable.
107
+
108
+ Two things are genuinely different without a DOM. **Text is estimated, not
109
+ measured** — `TextMeasure` falls back to `estimateTextSize` where no `document`
110
+ exists, so label sizes and any auto-grown height (`growY`) are close but not
111
+ identical to a browser's. And **the viewport is 0×0 until you say otherwise**: call
112
+ `editor.updateViewportScreenBounds({ x: 0, y: 0, w: 1600, h: 1000 })` before
113
+ `zoomToFit()`, or the move is deferred and a warning says so. Importing
114
+ `@mocanvas/mocanvas` in plain Node works; the JS entry stopped importing the
115
+ stylesheet after 4.0.2. Call `editor.dispose()` when the run is over.
116
+
117
+ ---
118
+
119
+ ## 3. Writing
120
+
121
+ `createShapes`, `updateShapes` and `deleteShapes` are the three verbs. Each takes an
122
+ array and returns the editor, so they chain.
123
+
124
+ ```ts
125
+ import { createShapeId, type GeoShape } from "@mocanvas/mocanvas"
126
+
127
+ const id = createShapeId("summary")
128
+
129
+ editor.createShapes<GeoShape>([
130
+ { id, type: "geo", x: 0, y: 0, props: { geo: "rectangle", w: 240, h: 120 } },
131
+ ])
132
+ editor.updateShapes<GeoShape>([{ id, type: "geo", x: 40, props: { w: 300 } }])
133
+ editor.deleteShapes([id])
134
+ ```
135
+
136
+ - **Props you leave out come from `getDefaultProps()`**, which keeps a model's
137
+ output short.
138
+ - **Style props you leave out come from the *instance*.** `createShapes` fills any
139
+ unset style prop from `stylesForNextShape` — the colour and size the last
140
+ interaction left behind. Right for a person, surprising for a program: if the
141
+ same input should give the same document, set style props explicitly.
142
+ - **`updateShapes` merges `props` and `meta` and replaces everything else.** A
143
+ partial naming a shape that is not there is skipped rather than throwing, as is a
144
+ `null` or `undefined` entry.
145
+ - **`deleteShapes` takes descendants with the parent**, skips locked shapes, and
146
+ clears the selection, hover and editing state of anything it removes.
147
+
148
+ ### One gesture, one `run`, one mark
149
+
150
+ ```ts
151
+ editor.markHistoryStoppingPoint("lay out the diagram")
152
+ editor.run(() => {
153
+ editor.createShapes(boxes)
154
+ editor.createShapes(arrows)
155
+ editor.updateShapes(adjustments)
156
+ })
157
+ ```
158
+
159
+ `editor.run(fn)` opens an operation on the *store*, not merely a signal transaction:
160
+ one history entry, one notification, however many writes happen inside. Before 4.8.1
161
+ it was a `transact` alone, so two `updateShapes` calls in one `run` reached a
162
+ listener as two notifications, the first carrying a half-applied state the document
163
+ was never meant to be in — and a sync binding published that state to its peers.
164
+
165
+ `markHistoryStoppingPoint(name?)` puts a mark in the history and returns its id.
166
+ **One gesture should be one mark.** The unit a person expects Cmd+Z to take back is
167
+ the thing they asked for — "draw the plan", not "the fourteenth arrow of the plan" —
168
+ and the mark defines that unit. `editor.mark(name)` is an alias;
169
+ `editor.bailToMark(id)` abandons a half-finished gesture using the returned id.
170
+
171
+ ---
172
+
173
+ ## 4. Reading back
174
+
175
+ The half an agent needs and rarely gets: asking what is on the page, and where,
176
+ without anything having been drawn. All of it works headless, because geometry and
177
+ hit-testing live in the engine rather than in the renderer.
178
+
179
+ ```ts
180
+ editor.getCurrentPageShapes() // Shape[] — store order
181
+ editor.getCurrentPageShapesSorted() // draw order, depth-first through children
182
+ editor.getCurrentPageShapeIds() // Set<ShapeId>
183
+ editor.getShape<GeoShape>(id) // one shape, or undefined
184
+ ```
185
+
186
+ `getShape` takes an id or a shape and returns the discriminated `Shape` union by
187
+ default, so `if (shape.type === "geo")` narrows `shape.props` at the call site. Pass
188
+ a type argument for a specific shape, or `UnknownShape` for a type nothing
189
+ registered.
190
+
191
+ ```ts
192
+ editor.getShapePageBounds(id) // Box | undefined — one shape, in page space
193
+ editor.getShapeGeometryBounds(id) // Box | undefined — the same box, shape-local
194
+ editor.getCurrentPageBounds() // Box | undefined — the union, undefined when empty
195
+ ```
196
+
197
+ `getCurrentPageBounds()` is **geometry, not ink**: the union of
198
+ `getShapePageBounds()`, deliberately excluding the half-stroke pad the engine keeps
199
+ for culling. The pad is per-shape, so including it would inflate the box and shift
200
+ its centre whenever the outermost shapes carried different stroke widths — making
201
+ "centre this on the page" depend on how thickly it was drawn. `Box` gives you
202
+ `center`, `minX`/`maxX`/`minY`/`maxY`, `corners` and `containsPoint`.
203
+
204
+ ```ts
205
+ editor.getShapeAtPoint({ x: 420, y: 260 }) // UnknownShape | undefined, topmost
206
+ editor.getShapesAtPoint({ x: 420, y: 260 }) // UnknownShape[], topmost first
207
+ ```
208
+
209
+ Both call `flushEngine()` first, so a shape created a line earlier is already in the
210
+ spatial index: **no frame has to be rendered for a hit test to be correct**, and none
211
+ is. The default answers "the interior of a filled shape, or the outline of any
212
+ shape", which is what a pointer would hit. `{ hitInside: true }` also counts the
213
+ interior of a *hollow* shape; that case goes the geometry route rather than the
214
+ engine's index, which is a page scan, so it is a deliberate opt-in. Tolerance comes
215
+ from `editor.getHitTestMargin()`, divided by the current zoom — headless with no
216
+ camera moved, that zoom is 1; pass `{ margin }` to fix it yourself if a run should
217
+ not depend on the configured default. `editor.getPointInShapeSpace(shape, point)`
218
+ converts a page point into a shape's local frame, which is what you want before
219
+ comparing against `w` and `h`.
220
+
221
+ ---
222
+
223
+ ## 5. Keeping an agent's writes out of a person's undo stack
224
+
225
+ ```ts
226
+ editor.run(() => {
227
+ editor.updateShapes(layoutCorrections)
228
+ }, { history: "ignore" })
229
+ ```
230
+
231
+ `history: "ignore"` means the writes inside the callback are **not a step in the undo
232
+ stack**. Use it for work the user did not ask for and would not think of as their own
233
+ edit: a derived layout pass, a label re-flow, an annotation an agent maintains in the
234
+ background. Without it, a person's next Cmd+Z takes back your housekeeping instead of
235
+ their last edit.
236
+
237
+ The nesting matters. The history manager is a store *listener*: it reads its ignore
238
+ counter when the notification arrives, which — now that `run` is one operation — is
239
+ after the callback has returned. So the ignore goes outside the operation, not inside
240
+ it:
241
+
242
+ ```ts
243
+ store.atomic(() => history.ignore(fn)) // 4.8.1 — wrong way round
244
+ history.ignore(() => store.atomic(fn)) // 4.8.2 onwards
245
+ ```
246
+
247
+ 4.8.1 had it inside, and ignored writes went into the undo stack after all. If you are
248
+ on 4.8.1, upgrade straight past it.
249
+
250
+ ### What it does not promise
251
+
252
+ This is the part that bites in production. **An ignored write to a record the user
253
+ then undoes is still reverted.**
254
+
255
+ ```ts
256
+ editor.markHistoryStoppingPoint()
257
+ editor.updateShapes([{ id: S, type: "geo", x: 100 }])
258
+
259
+ editor.run(() => editor.updateShapes([{ id: S, type: "geo", y: 250 }]), { history: "ignore" })
260
+
261
+ editor.undo() // S.x back to 0 — and S.y back to 0 as well
262
+ ```
263
+
264
+ Undo restores **whole records, not fields**. Putting `S` back as it was at the mark
265
+ puts back every field of `S`, the ignored one included. `history: "ignore"` promises
266
+ your write is not a *step*; it cannot promise that some other step will not overwrite
267
+ it. This is not a regression and not a mocanvas quirk — it behaves the same on 4.8.0
268
+ and earlier, and the same in the reference implementation this API is shaped after. A
269
+ test in this repository asserts it, so the next person to meet it can tell which of
270
+ the two problems they have.
271
+
272
+ The practical consequence: **if an agent's output must survive a user's undo, put it
273
+ in records the user is not editing.** A separate shape, or an app record type of your
274
+ own, is safe; a field tucked into a shape the user is dragging is not. The same option
275
+ also takes `"record"` (the default) and `"record-preserveRedoStack"`, which records
276
+ the step without discarding a redo stack the user might still want.
277
+
278
+ ---
279
+
280
+ ## 6. Reproducible runs
281
+
282
+ Two runs of the same agent over the same input should produce the same document, or
283
+ you cannot diff them, snapshot them in a test, or tell a model's change from a
284
+ library's. Two things stand in the way; both are controllable.
285
+
286
+ **Ids.** `createShapeId()` with no argument generates a random unique part;
287
+ `createShapeId("box-1")` returns `shape:box-1` verbatim. Name your ids from your input
288
+ and the same plan yields the same records. `createBindingId` is the same.
289
+
290
+ **Index-key jitter.** A record's `index` is a fractional index — an order key that
291
+ sorts lexicographically and between which new keys can always be generated. Plain
292
+ fractional indexing is a pure function of its two neighbours, so two clients inserting
293
+ into the same gap generate the *same* key every single time; `index` is one register to
294
+ a last-writer-wins merge, so one of the two shapes is merged away. mocanvas appends six
295
+ random digits from a 61-character alphabet, so the keys differ while staying in the
296
+ same gap. That is what makes concurrent insertion safe — and it makes every generated
297
+ key unpredictable, which is exactly wrong for a run you want to compare. So jitter is
298
+ off under a test runner. The resolution order, in `packages/store/src/indexKey.ts`:
299
+
300
+ 1. An explicit `setIndexJitterEnabled(true | false)` wins for the rest of the process;
301
+ `null` restores the default.
302
+ 2. Otherwise `MOCANVAS_INDEX_JITTER`, if set. `"0"`, `"false"` and `"off"` mean off;
303
+ anything else means on.
304
+ 3. Otherwise `NODE_ENV === "test"` turns it off.
305
+ 4. Otherwise it is on.
306
+
307
+ The environment variable is the one to reach for in a runner that does not set
308
+ `NODE_ENV`, or that sets it to `test` when you did not want that. Both helpers come
309
+ from `@mocanvas/store`; `@mocanvas/editor` re-exports the index-key *generators* but
310
+ not these two. It is process-wide rather than per-call because those generators are
311
+ free functions reached from everywhere, `Editor` included.
312
+
313
+ ```ts
314
+ import { setIndexJitterEnabled } from "@mocanvas/store"
315
+
316
+ setIndexJitterEnabled(false) // process-wide; pass null to restore the default
317
+ ```
318
+
319
+ With named ids and jitter off, `serializeMocanvasFile(editor)` returns a string you can
320
+ compare byte for byte between runs, and `store.getStoreSnapshot()` returns the records
321
+ and the schema version for a structural assertion.
322
+
323
+ ---
324
+
325
+ ## 7. Validation on the way in
326
+
327
+ Records are checked against their type as they enter the store, so a malformed shape
328
+ from a model throws at `put` — the moment it is written — rather than mounting and
329
+ failing somewhere in geometry or rendering. `Store.put` calls `schema.validateRecord`
330
+ for every record, before any `before*` side effect and before the record is frozen and
331
+ written. The message names the path:
332
+
333
+ ```
334
+ At shape.props.w: Expected number, got undefined
335
+ ```
336
+
337
+ An unregistered shape type is caught earlier still: `createShapes` asks
338
+ `getShapeUtil(partial.type)` first, which throws `No ShapeUtil registered for type
339
+ "flowchart"` without touching the store. What each shape is validated against is its
340
+ util's `static props` map — the same map [CUSTOM_SHAPES.md](CUSTOM_SHAPES.md) shows you
341
+ writing, which is why validation covers your own shapes and not only the built-in ones.
342
+
343
+ Failures throw by default. `StoreSchema.create` accepts an `onValidationFailure`
344
+ handler that can repair a record and return it instead — how a `.tldr` file from an
345
+ older version gets loaded rather than rejected — but `createStore` does not plumb that
346
+ option through, so a store built the ordinary way throws. For an agent that is usually
347
+ right: a model that produced `w: "180"` should be told, not silently corrected.
348
+
349
+ ---
350
+
351
+ ## 8. A worked example: a plan, drawn
352
+
353
+ A plan is data — boxes and arrows with stable names. Turning it into a document is one
354
+ function, and nothing in it is agent-specific: the same code behind a toolbar button
355
+ would be the same code.
356
+
357
+ ```ts
358
+ import { createShapeId, toRichText, type ArrowShape, type Editor, type GeoShape } from "@mocanvas/mocanvas"
359
+
360
+ export interface Plan {
361
+ boxes: { name: string; label: string; x: number; y: number; w?: number; h?: number }[]
362
+ arrows: { from: string; to: string; label?: string }[]
363
+ }
364
+
365
+ export function drawPlan(editor: Editor, plan: Plan): void {
366
+ editor.markHistoryStoppingPoint("draw plan")
367
+
368
+ editor.run(() => {
369
+ editor.createShapes<GeoShape>(
370
+ plan.boxes.map((box) => ({
371
+ id: createShapeId(box.name),
372
+ type: "geo",
373
+ x: box.x,
374
+ y: box.y,
375
+ // Styles are explicit: anything left out would come from
376
+ // `stylesForNextShape`, whatever the last interaction left there.
377
+ props: {
378
+ geo: "rectangle",
379
+ w: box.w ?? 180,
380
+ h: box.h ?? 90,
381
+ richText: toRichText(box.label),
382
+ color: "black",
383
+ fill: "none",
384
+ dash: "draw",
385
+ size: "m",
386
+ },
387
+ })),
388
+ )
389
+
390
+ for (const edge of plan.arrows) {
391
+ const fromId = createShapeId(edge.from)
392
+ const toId = createShapeId(edge.to)
393
+ const from = editor.getShapePageBounds(fromId)
394
+ const to = editor.getShapePageBounds(toId)
395
+ if (!from || !to) continue
396
+
397
+ const arrowId = createShapeId(`${edge.from}->${edge.to}`)
398
+
399
+ // The arrow sits at the page origin, so its local space and page space are the
400
+ // same frame and the two centres go straight in. These are the *static*
401
+ // terminals; the bindings below take over while they exist.
402
+ editor.createShape<ArrowShape>({
403
+ id: arrowId,
404
+ type: "arrow",
405
+ x: 0,
406
+ y: 0,
407
+ props: {
408
+ kind: "arc",
409
+ start: { x: from.center.x, y: from.center.y },
410
+ end: { x: to.center.x, y: to.center.y },
411
+ ...(edge.label ? { richText: toRichText(edge.label) } : {}),
412
+ },
413
+ })
414
+
415
+ editor.createBindings([
416
+ { type: "arrow", fromId: arrowId, toId: fromId, props: { terminal: "start" } },
417
+ { type: "arrow", fromId: arrowId, toId: toId, props: { terminal: "end" } },
418
+ ])
419
+ }
420
+ })
421
+ }
422
+ ```
423
+
424
+ ### Why the bindings, and why the arrow still ends on the edge
425
+
426
+ The two points above are box *centres*, not edge points — and the drawn arrow runs edge
427
+ to edge anyway. That is the binding doing it, not arithmetic you were spared writing.
428
+ `ArrowShapeUtil.getGeometry` resolves its terminals through
429
+ `getArrowTerminalsInArrowSpace` on every read: a bound terminal starts at the anchor
430
+ inside the bound shape's bounds (the default `normalizedAnchor` is `{ x: 0.5, y: 0.5 }`,
431
+ the centre) and is pulled back to where the arrow body first crosses that shape's
432
+ **outline** on its way from the opposite terminal, then back a little further, by
433
+ `ARROW_TERMINAL_GAP_STROKES` (2.7) times the stroke width. An arrowhead resting on a
434
+ border reads as part of that border; a small gap reads as pointing at it.
435
+
436
+ So the geometry is right from the moment the binding exists, before anything is rendered
437
+ — `editor.getShapePageBounds(arrowId)` already reflects it. The static `props.start` /
438
+ `props.end` are a fallback: `ArrowBindingUtil` rewrites them when the bound shape
439
+ changes, and freezes the terminal at its current position if the bound shape is deleted
440
+ or the binding is isolated. Give them sensible values, as above, so an isolated arrow
441
+ does not snap to the origin. The consequence you actually wanted is that moving a box
442
+ afterwards moves the arrows with it, whether the move came from your agent or from
443
+ someone dragging it.
444
+
445
+ ```ts
446
+ drawPlan(editor, {
447
+ boxes: [
448
+ { name: "ingest", label: "Ingest", x: 0, y: 0 },
449
+ { name: "parse", label: "Parse", x: 320, y: 0 },
450
+ { name: "store", label: "Store", x: 640, y: 0 },
451
+ ],
452
+ arrows: [{ from: "ingest", to: "parse" }, { from: "parse", to: "store", label: "validated" }],
453
+ })
454
+
455
+ editor.updateViewportScreenBounds({ x: 0, y: 0, w: 1600, h: 1000 })
456
+ editor.zoomToFit()
457
+
458
+ editor.getCurrentPageBounds() // Box over the three shapes
459
+ editor.getShapeAtPoint({ x: 90, y: 45 })?.id // shape:ingest
460
+ ```
461
+
462
+ Undo takes the whole plan back in one step, because the whole plan was one `run` after
463
+ one mark. That is the property worth protecting as the agent grows: one gesture, one
464
+ mark, whatever the model decided to do inside it.
465
+
466
+ ---
467
+
468
+ ## Where to go next
469
+
470
+ - [CUSTOM_SHAPES.md](CUSTOM_SHAPES.md) — a shape of your own, and the `static props`
471
+ map that validates it.
472
+ - [UI.md](UI.md) — the default interface, for the half a person drives.
473
+ - [ARCHITECTURE.md](ARCHITECTURE.md) — how the store, the command stream and the engine
474
+ fit together.
475
+ - [COMPAT.md](COMPAT.md) — what exists against the tldraw 5.4 API model, and what
476
+ deliberately does not.
package/CUSTOM_SHAPES.md CHANGED
@@ -685,8 +685,13 @@ export function App() {
685
685
  }
686
686
  ```
687
687
 
688
- The default toolbar does not grow a button for your tool. Until UI slots exist,
689
- drive it from your own chrome rendered as `children` of `<Mocanvas>`:
688
+ The default toolbar will grow a button for your tool: register it in the UI
689
+ tool list with `overrides.tools` and the toolbar collects any id outside its own
690
+ layout into an extras group after a divider. [TOOLS.md](TOOLS.md) §10 covers
691
+ that, along with the keyboard shortcut.
692
+
693
+ You can also skip the built-in chrome entirely and drive the tool from your own,
694
+ rendered as `children` of `<Mocanvas>`:
690
695
 
691
696
  ```tsx
692
697
  <Mocanvas shapeUtils={[CalloutShapeUtil]} tools={[CalloutTool]}>