@mocanvas/editor 4.12.4 → 4.14.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 +7 -3
- package/COMPAT.md +4 -1
- package/CUSTOM_SHAPES.md +15 -0
- package/dist/index.d.ts +59 -1
- package/dist/index.js +173 -33
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/ARCHITECTURE.md
CHANGED
|
@@ -233,9 +233,13 @@ engine.selection_bounds(ptr_handles, len) -> ptr to 4 f32
|
|
|
233
233
|
level-of-detail quads until a later frame reaches them.
|
|
234
234
|
4. Append transformed vertices into the frame vertex buffer, batching by
|
|
235
235
|
texture. Shapes flagged `OVERLAY` skip the mesh and go into the overlay list.
|
|
236
|
-
5. TS uploads the buffers (unless the engine reports them unchanged) and draws.
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
5. TS uploads the buffers (unless the engine reports them unchanged) and draws. The overlay list
|
|
237
|
+
gains the shapes that left the viewport recently (a bounded LRU,
|
|
238
|
+
`options.overlayRetainLimit`) and is diffed against the React overlay layer,
|
|
239
|
+
which positions DOM shapes with `transform`, hides the retained ones with
|
|
240
|
+
`content-visibility: hidden`, and passes pointer events through except when
|
|
241
|
+
editing. The camera is a transform on the layer, written by a signal
|
|
242
|
+
reaction: a pan or zoom frame is no React render at all.
|
|
239
243
|
|
|
240
244
|
Stroke widths are in page units and scale with zoom, so meshes never need
|
|
241
245
|
retessellation while zooming. Dash patterns (`dash` style word: 0 solid,
|
package/COMPAT.md
CHANGED
|
@@ -98,7 +98,10 @@ work is**, not in the symbol tables above.
|
|
|
98
98
|
|
|
99
99
|
`canCull` is absent from the class but honoured where it matters: a util that
|
|
100
100
|
implements it and returns `false` keeps its shape in the DOM overlay while the
|
|
101
|
-
shape is off screen (4.12.4), and out of `getCulledShapes()`.
|
|
101
|
+
shape is off screen (4.12.4), and out of `getCulledShapes()`. Every other overlay
|
|
102
|
+
shape that leaves the viewport stays mounted but hidden until
|
|
103
|
+
`options.overlayRetainLimit` newer ones displace it; the option is mocanvas's
|
|
104
|
+
own.
|
|
102
105
|
|
|
103
106
|
You can measure a real application against this surface rather than trusting the
|
|
104
107
|
tables: `scripts/compat-parity.mjs` type-checks a consumer app with `tldraw`
|
package/CUSTOM_SHAPES.md
CHANGED
|
@@ -197,6 +197,21 @@ geometry bounds, with `pointerEvents: "none"` unless the shape is being edited.
|
|
|
197
197
|
Render in shape-local space starting at `(0, 0)`; do not apply the camera or
|
|
198
198
|
the shape transform yourself.
|
|
199
199
|
|
|
200
|
+
**When it re-renders.** The body is a function of the shape's `props` and
|
|
201
|
+
`meta`: it re-renders when either changes, and when a signal it read during its
|
|
202
|
+
last render changes (`useValue`, `editor.get*`). Moving, rotating or fading the
|
|
203
|
+
shape changes only the wrapper's transform and opacity, so a body that reads
|
|
204
|
+
`shape.x`, `shape.y`, `shape.rotation` or `shape.opacity` sees the value from its
|
|
205
|
+
last render. Read the position through a signal if you need it.
|
|
206
|
+
A pan or zoom re-renders nothing: the camera is a transform on the overlay layer.
|
|
207
|
+
|
|
208
|
+
**Off screen.** A body is mounted while its shape is on screen. When the shape
|
|
209
|
+
leaves the viewport it stays mounted, hidden with `content-visibility: hidden`,
|
|
210
|
+
until `options.overlayRetainLimit` (default 256) more recently culled shapes
|
|
211
|
+
push it out; coming back on screen then costs a style change, not a remount.
|
|
212
|
+
`0` unmounts at the viewport edge. A util whose `canCull()` returns `false` is
|
|
213
|
+
never unmounted and never hidden for being off screen.
|
|
214
|
+
|
|
200
215
|
### `getIndicatorPath` — the selection outline
|
|
201
216
|
|
|
202
217
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -8313,6 +8313,24 @@ interface EditorConfig extends TldrawOptions {
|
|
|
8313
8313
|
* enough to debounce at all, and how much movement is worth publishing.
|
|
8314
8314
|
*/
|
|
8315
8315
|
debouncedZoomOctaves: number;
|
|
8316
|
+
/**
|
|
8317
|
+
* How many DOM-overlay shapes that left the viewport stay mounted, hidden, so
|
|
8318
|
+
* coming back into view costs a style change instead of a React remount.
|
|
8319
|
+
*
|
|
8320
|
+
* The engine culls the overlay to the viewport exactly, so without this every
|
|
8321
|
+
* card that crosses the viewport edge during a pan or a zoom is unmounted and
|
|
8322
|
+
* rebuilt from nothing when the gesture comes back — on a board of heavy React
|
|
8323
|
+
* cards, dozens of remounts per gesture. Retained shapes are the most recently
|
|
8324
|
+
* culled ones (least recently visible are evicted first), they are listed in
|
|
8325
|
+
* {@link Editor.getOverlayRetainedShapeIds}, and the canvas hides them with
|
|
8326
|
+
* `content-visibility: hidden`, which keeps their DOM and layout but skips
|
|
8327
|
+
* their rendering.
|
|
8328
|
+
*
|
|
8329
|
+
* The bound is the memory cost: up to this many shapes' DOM is kept alive off
|
|
8330
|
+
* screen. `0` turns retention off and unmounts at the edge, as every release before
|
|
8331
|
+
* this option did.
|
|
8332
|
+
*/
|
|
8333
|
+
overlayRetainLimit: number;
|
|
8316
8334
|
}
|
|
8317
8335
|
declare const DEFAULT_EDITOR_CONFIG: EditorConfig;
|
|
8318
8336
|
interface HitTestOptions {
|
|
@@ -8431,6 +8449,15 @@ declare class Editor extends EventEmitter<EditorEvents> {
|
|
|
8431
8449
|
private readonly _frameEpoch;
|
|
8432
8450
|
private readonly _overlayShapeIds;
|
|
8433
8451
|
private readonly _overlayClips;
|
|
8452
|
+
private readonly _overlayRetained;
|
|
8453
|
+
/**
|
|
8454
|
+
* Overlay shapes the engine culled that are still mounted, oldest first: a
|
|
8455
|
+
* `Map` iterates in insertion order, which is the LRU order eviction wants.
|
|
8456
|
+
* Each carries the clip it had when it was last on screen.
|
|
8457
|
+
*/
|
|
8458
|
+
private readonly overlayRetained;
|
|
8459
|
+
/** Last frame's on-screen overlay shapes (engine list plus kept ones), with their clips. */
|
|
8460
|
+
private overlayOnScreen;
|
|
8434
8461
|
private readonly _isDisposed;
|
|
8435
8462
|
private readonly _lastFrame;
|
|
8436
8463
|
private readonly _cameraOptions;
|
|
@@ -8515,8 +8542,18 @@ declare class Editor extends EventEmitter<EditorEvents> {
|
|
|
8515
8542
|
culled: number;
|
|
8516
8543
|
ms: number;
|
|
8517
8544
|
};
|
|
8518
|
-
/**
|
|
8545
|
+
/**
|
|
8546
|
+
* Shapes the DOM overlay must keep mounted this frame, in draw order: the ones
|
|
8547
|
+
* on screen, plus those listed by {@link getOverlayRetainedShapeIds}, which are
|
|
8548
|
+
* off screen and mounted only so that coming back does not rebuild them.
|
|
8549
|
+
*/
|
|
8519
8550
|
getOverlayShapeIds(): readonly ShapeId[];
|
|
8551
|
+
/**
|
|
8552
|
+
* The subset of {@link getOverlayShapeIds} that the engine culled and the
|
|
8553
|
+
* overlay keeps mounted but hidden — see `options.overlayRetainLimit`. Empty
|
|
8554
|
+
* when nothing is retained.
|
|
8555
|
+
*/
|
|
8556
|
+
getOverlayRetainedShapeIds(): ReadonlySet<ShapeId>;
|
|
8520
8557
|
/**
|
|
8521
8558
|
* Page-space clip rects for the overlay shapes, parallel to
|
|
8522
8559
|
* `getOverlayShapeIds()`; `undefined` where the shape is unclipped.
|
|
@@ -9088,6 +9125,27 @@ declare class Editor extends EventEmitter<EditorEvents> {
|
|
|
9088
9125
|
* ancestors give it, on screen or off, so its DOM never changes shape at the viewport edge.
|
|
9089
9126
|
*/
|
|
9090
9127
|
private keepUncullableOverlay;
|
|
9128
|
+
/**
|
|
9129
|
+
* Keep the overlay shapes that just left the viewport mounted, up to
|
|
9130
|
+
* `options.overlayRetainLimit`, and put the whole list in paint order.
|
|
9131
|
+
*
|
|
9132
|
+
* A shape that the engine stops reporting moves into a bounded LRU; one that
|
|
9133
|
+
* comes back moves out of it. Either way its entry in the list — and so its
|
|
9134
|
+
* React element and DOM — is untouched; only its retained flag flips. The list
|
|
9135
|
+
* is sorted by paint rank whenever anything is retained, so a shape crossing
|
|
9136
|
+
* the viewport edge never changes position among its siblings either: React
|
|
9137
|
+
* would move its DOM for that, and moving a node with an iframe in it reloads
|
|
9138
|
+
* the iframe.
|
|
9139
|
+
*
|
|
9140
|
+
* Returns the retained ids, which the canvas hides.
|
|
9141
|
+
*/
|
|
9142
|
+
private retainCulledOverlay;
|
|
9143
|
+
/**
|
|
9144
|
+
* Every shape on the current page by paint order. Recomputed when a shape
|
|
9145
|
+
* record changes — not when the camera moves, which is when the overlay
|
|
9146
|
+
* needs it.
|
|
9147
|
+
*/
|
|
9148
|
+
private readonly _paintRank;
|
|
9091
9149
|
/**
|
|
9092
9150
|
* Device-pixel scale at which rasterized textures (text labels) should be
|
|
9093
9151
|
* drawn: the device pixel ratio times the zoom, bucketed to powers of two.
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { atom, computed, unsafe__withoutCapture, transact
|
|
1
|
+
import { atom, react, computed, unsafe__withoutCapture, transact } from '@mocanvas/state';
|
|
2
2
|
export { AtomMap, atom, computed, react, reactor, transact, transaction } from '@mocanvas/state';
|
|
3
3
|
import { createRecordType, isIndexKey, createMigrationIds, uniqueId, isRecordsDiffEmpty, squashRecordDiffs, reverseRecordsDiff, sortByIndex, getIndexAbove, ZERO_INDEX_KEY, getIndexBetween, getIndexBelow, getIndicesAbove, indexKeyToZKey, parseMigrationId, createMigrationSequence, StoreSchema, Store } from '@mocanvas/store';
|
|
4
4
|
export { ZERO_INDEX_KEY, createComputedCache, getGraphemeLength, getGraphemes, getIndexAbove, getIndexBelow, getIndexBetween, getIndices, getIndicesAbove, getIndicesBelow, getIndicesBetween, iterateGraphemes, sortByIndex } from '@mocanvas/store';
|
|
5
5
|
import { PATH_OP, FLAG, GEO_FLAG, EngineBridge, VERTEX_FLOATS, BATCH_WORDS, readClip } from '@mocanvas/wasm';
|
|
6
6
|
export { EngineBridge, FLAG, GEO_FLAG, GEO_KIND, PATH_OP, getLoadedEngine, loadEngine, loadEngineSync } from '@mocanvas/wasm';
|
|
7
|
-
import { createContext, useEffect, useRef, useLayoutEffect, useContext, Component, useMemo, useId, useCallback, useState } from 'react';
|
|
7
|
+
import { createContext, useEffect, useRef, useLayoutEffect, memo, useContext, Component, useMemo, useId, useCallback, useState } from 'react';
|
|
8
8
|
import { track as track$1, useValue } from '@mocanvas/state/react';
|
|
9
9
|
export { track, useAtom, useComputed, useQuickReactor, useReactor, useStateTracking, useValue } from '@mocanvas/state/react';
|
|
10
10
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -7849,7 +7849,11 @@ var DEFAULT_EDITOR_CONFIG = {
|
|
|
7849
7849
|
backgroundColor: [0.976, 0.98, 0.984, 1],
|
|
7850
7850
|
// SEMANTICS-ASSUMED: half an octave is the smallest zoom change a rasteriser
|
|
7851
7851
|
// should care about.
|
|
7852
|
-
debouncedZoomOctaves: 0.5
|
|
7852
|
+
debouncedZoomOctaves: 0.5,
|
|
7853
|
+
// SEMANTICS-ASSUMED: a few screens' worth of cards on a dense board. Measured
|
|
7854
|
+
// in apps/bench (`dom-overlay.mjs`): a pan or zoom there and back stops
|
|
7855
|
+
// remounting at this size; the cost is the DOM of this many off-screen shapes.
|
|
7856
|
+
overlayRetainLimit: 256
|
|
7853
7857
|
};
|
|
7854
7858
|
var RAD_PER_DEG = Math.PI / 180;
|
|
7855
7859
|
var WHEEL_ZOOM_DELTA_CAP = 50;
|
|
@@ -7949,6 +7953,15 @@ var Editor = class extends EventEmitter {
|
|
|
7949
7953
|
_frameEpoch;
|
|
7950
7954
|
_overlayShapeIds;
|
|
7951
7955
|
_overlayClips;
|
|
7956
|
+
_overlayRetained;
|
|
7957
|
+
/**
|
|
7958
|
+
* Overlay shapes the engine culled that are still mounted, oldest first: a
|
|
7959
|
+
* `Map` iterates in insertion order, which is the LRU order eviction wants.
|
|
7960
|
+
* Each carries the clip it had when it was last on screen.
|
|
7961
|
+
*/
|
|
7962
|
+
overlayRetained = /* @__PURE__ */ new Map();
|
|
7963
|
+
/** Last frame's on-screen overlay shapes (engine list plus kept ones), with their clips. */
|
|
7964
|
+
overlayOnScreen = /* @__PURE__ */ new Map();
|
|
7952
7965
|
_isDisposed;
|
|
7953
7966
|
_lastFrame;
|
|
7954
7967
|
_cameraOptions;
|
|
@@ -8015,6 +8028,7 @@ var Editor = class extends EventEmitter {
|
|
|
8015
8028
|
});
|
|
8016
8029
|
this._overlayShapeIds = atom("editor.overlayShapeIds", []);
|
|
8017
8030
|
this._overlayClips = atom("editor.overlayClips", []);
|
|
8031
|
+
this._overlayRetained = atom("editor.overlayRetained", EMPTY_ID_SET);
|
|
8018
8032
|
this._isDisposed = atom("editor.isDisposed", false);
|
|
8019
8033
|
this._isMounted = atom("editor.isMounted", false);
|
|
8020
8034
|
this._lastFrame = atom("editor.lastFrame", { drawn: 0, culled: 0, ms: 0 });
|
|
@@ -8209,10 +8223,22 @@ var Editor = class extends EventEmitter {
|
|
|
8209
8223
|
getLastFrameStats() {
|
|
8210
8224
|
return this._lastFrame.get();
|
|
8211
8225
|
}
|
|
8212
|
-
/**
|
|
8226
|
+
/**
|
|
8227
|
+
* Shapes the DOM overlay must keep mounted this frame, in draw order: the ones
|
|
8228
|
+
* on screen, plus those listed by {@link getOverlayRetainedShapeIds}, which are
|
|
8229
|
+
* off screen and mounted only so that coming back does not rebuild them.
|
|
8230
|
+
*/
|
|
8213
8231
|
getOverlayShapeIds() {
|
|
8214
8232
|
return this._overlayShapeIds.get();
|
|
8215
8233
|
}
|
|
8234
|
+
/**
|
|
8235
|
+
* The subset of {@link getOverlayShapeIds} that the engine culled and the
|
|
8236
|
+
* overlay keeps mounted but hidden — see `options.overlayRetainLimit`. Empty
|
|
8237
|
+
* when nothing is retained.
|
|
8238
|
+
*/
|
|
8239
|
+
getOverlayRetainedShapeIds() {
|
|
8240
|
+
return this._overlayRetained.get();
|
|
8241
|
+
}
|
|
8216
8242
|
/**
|
|
8217
8243
|
* Page-space clip rects for the overlay shapes, parallel to
|
|
8218
8244
|
* `getOverlayShapeIds()`; `undefined` where the shape is unclipped.
|
|
@@ -10350,10 +10376,12 @@ var Editor = class extends EventEmitter {
|
|
|
10350
10376
|
}
|
|
10351
10377
|
}
|
|
10352
10378
|
unsafe__withoutCapture(() => this.keepUncullableOverlay(ids, clips));
|
|
10379
|
+
const retained = unsafe__withoutCapture(() => this.retainCulledOverlay(ids, clips));
|
|
10353
10380
|
unsafe__withoutCapture(() => {
|
|
10354
10381
|
const prev = this._overlayShapeIds.get();
|
|
10355
10382
|
if (prev.length !== ids.length || prev.some((id, i) => id !== ids[i])) this._overlayShapeIds.set(ids);
|
|
10356
10383
|
if (!sameClips(this._overlayClips.get(), clips)) this._overlayClips.set(clips);
|
|
10384
|
+
if (!sameIdSet(this._overlayRetained.get(), retained)) this._overlayRetained.set(retained);
|
|
10357
10385
|
const ms = performance.now() - t0;
|
|
10358
10386
|
this._lastFrame.set({ drawn: frame.drawn, culled: frame.culled, ms });
|
|
10359
10387
|
this.emit("frame", { drawn: frame.drawn, culled: frame.culled, ms });
|
|
@@ -10383,6 +10411,69 @@ var Editor = class extends EventEmitter {
|
|
|
10383
10411
|
clips.push(entry.clip);
|
|
10384
10412
|
}
|
|
10385
10413
|
}
|
|
10414
|
+
/**
|
|
10415
|
+
* Keep the overlay shapes that just left the viewport mounted, up to
|
|
10416
|
+
* `options.overlayRetainLimit`, and put the whole list in paint order.
|
|
10417
|
+
*
|
|
10418
|
+
* A shape that the engine stops reporting moves into a bounded LRU; one that
|
|
10419
|
+
* comes back moves out of it. Either way its entry in the list — and so its
|
|
10420
|
+
* React element and DOM — is untouched; only its retained flag flips. The list
|
|
10421
|
+
* is sorted by paint rank whenever anything is retained, so a shape crossing
|
|
10422
|
+
* the viewport edge never changes position among its siblings either: React
|
|
10423
|
+
* would move its DOM for that, and moving a node with an iframe in it reloads
|
|
10424
|
+
* the iframe.
|
|
10425
|
+
*
|
|
10426
|
+
* Returns the retained ids, which the canvas hides.
|
|
10427
|
+
*/
|
|
10428
|
+
retainCulledOverlay(ids, clips) {
|
|
10429
|
+
const limit = Math.max(0, Math.floor(this.options.overlayRetainLimit));
|
|
10430
|
+
const lastOnScreen = this.overlayOnScreen;
|
|
10431
|
+
const onScreen = /* @__PURE__ */ new Map();
|
|
10432
|
+
for (let i = 0; i < ids.length; i++) onScreen.set(ids[i], clips[i]);
|
|
10433
|
+
this.overlayOnScreen = onScreen;
|
|
10434
|
+
const retainedMap = this.overlayRetained;
|
|
10435
|
+
if (limit === 0) {
|
|
10436
|
+
retainedMap.clear();
|
|
10437
|
+
return EMPTY_ID_SET;
|
|
10438
|
+
}
|
|
10439
|
+
for (const id of onScreen.keys()) retainedMap.delete(id);
|
|
10440
|
+
for (const [id, clip] of lastOnScreen) {
|
|
10441
|
+
if (onScreen.has(id)) continue;
|
|
10442
|
+
retainedMap.delete(id);
|
|
10443
|
+
retainedMap.set(id, clip);
|
|
10444
|
+
}
|
|
10445
|
+
if (retainedMap.size === 0) return EMPTY_ID_SET;
|
|
10446
|
+
const pageId = this.getCurrentPageId();
|
|
10447
|
+
for (const id of [...retainedMap.keys()]) {
|
|
10448
|
+
const shape = this.getShape(id);
|
|
10449
|
+
if (!shape || !this.overlayFlaggedIds.has(id) || this.getAncestorPageId(shape) !== pageId || isShapeHidden(this, shape)) {
|
|
10450
|
+
retainedMap.delete(id);
|
|
10451
|
+
}
|
|
10452
|
+
}
|
|
10453
|
+
while (retainedMap.size > limit) retainedMap.delete(retainedMap.keys().next().value);
|
|
10454
|
+
if (retainedMap.size === 0) return EMPTY_ID_SET;
|
|
10455
|
+
const rank = this._paintRank.get();
|
|
10456
|
+
const entries = [];
|
|
10457
|
+
for (const [id, clip] of onScreen) entries.push({ id, clip });
|
|
10458
|
+
for (const [id, clip] of retainedMap) entries.push({ id, clip });
|
|
10459
|
+
const rankOf = (id) => rank.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
10460
|
+
entries.sort((a, b) => rankOf(a.id) - rankOf(b.id));
|
|
10461
|
+
ids.length = 0;
|
|
10462
|
+
clips.length = 0;
|
|
10463
|
+
for (const entry of entries) {
|
|
10464
|
+
ids.push(entry.id);
|
|
10465
|
+
clips.push(entry.clip);
|
|
10466
|
+
}
|
|
10467
|
+
return new Set(retainedMap.keys());
|
|
10468
|
+
}
|
|
10469
|
+
/**
|
|
10470
|
+
* Every shape on the current page by paint order. Recomputed when a shape
|
|
10471
|
+
* record changes — not when the camera moves, which is when the overlay
|
|
10472
|
+
* needs it.
|
|
10473
|
+
*/
|
|
10474
|
+
_paintRank = computed("editor.paintRank", () => {
|
|
10475
|
+
return new Map(this.getCurrentPageShapesSorted().map((shape, index) => [shape.id, index]));
|
|
10476
|
+
});
|
|
10386
10477
|
// ---- textures ----------------------------------------------------------
|
|
10387
10478
|
/**
|
|
10388
10479
|
* Device-pixel scale at which rasterized textures (text labels) should be
|
|
@@ -11622,6 +11713,13 @@ var Editor = class extends EventEmitter {
|
|
|
11622
11713
|
/** Which menus are open right now. Backed by the `instance` session record. */
|
|
11623
11714
|
menus = new MenuManager(this);
|
|
11624
11715
|
};
|
|
11716
|
+
var EMPTY_ID_SET = /* @__PURE__ */ new Set();
|
|
11717
|
+
function sameIdSet(a, b) {
|
|
11718
|
+
if (a === b) return true;
|
|
11719
|
+
if (a.size !== b.size) return false;
|
|
11720
|
+
for (const id of a) if (!b.has(id)) return false;
|
|
11721
|
+
return true;
|
|
11722
|
+
}
|
|
11625
11723
|
function sameClips(a, b) {
|
|
11626
11724
|
if (a.length !== b.length) return false;
|
|
11627
11725
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -15174,10 +15272,8 @@ function CanvasBody({ editor, className, style, children, components, indicatorO
|
|
|
15174
15272
|
const cursor = useValue("cursor", () => cssCursor(editor.getInstanceState().cursor.type), [editor]);
|
|
15175
15273
|
const hasChromeContextMenu = useEditorComponents().ContextMenu != null;
|
|
15176
15274
|
const themeVars = useThemeCssVars(editor);
|
|
15177
|
-
|
|
15275
|
+
useCameraCssVarsOnElement(editor, containerRef, style);
|
|
15178
15276
|
const isGridMode = useValue("isGridMode", () => editor.getInstanceState().isGridMode, [editor]);
|
|
15179
|
-
const camera = useValue("camera", () => editor.getCamera(), [editor]);
|
|
15180
|
-
const gridSize = useValue("gridSize", () => editor.getDocumentSettings().gridSize, [editor]);
|
|
15181
15277
|
const Grid = components?.Grid === void 0 ? DefaultGrid : components.Grid;
|
|
15182
15278
|
const Indicators = components?.Indicators ?? DefaultIndicators;
|
|
15183
15279
|
const Brush = components?.Brush ?? (hasOverlay(editor, "brush") ? null : DefaultBrush);
|
|
@@ -15188,7 +15284,7 @@ function CanvasBody({ editor, className, style, children, components, indicatorO
|
|
|
15188
15284
|
{
|
|
15189
15285
|
ref: containerRef,
|
|
15190
15286
|
className: className ? `mocanvas ${className}` : "mocanvas",
|
|
15191
|
-
style: { ...containerStyle, background: backgroundFor(editor), ...themeVars, ...
|
|
15287
|
+
style: { ...containerStyle, background: backgroundFor(editor), ...themeVars, ...style, cursor },
|
|
15192
15288
|
tabIndex: 0,
|
|
15193
15289
|
onPointerDown: events.onPointerDown,
|
|
15194
15290
|
onPointerMove: events.onPointerMove,
|
|
@@ -15198,7 +15294,7 @@ function CanvasBody({ editor, className, style, children, components, indicatorO
|
|
|
15198
15294
|
"data-testid": "mocanvas-container",
|
|
15199
15295
|
children: [
|
|
15200
15296
|
Background ? /* @__PURE__ */ jsx(Background, { editor }) : null,
|
|
15201
|
-
Grid !== null && isGridMode ? /* @__PURE__ */ jsx(
|
|
15297
|
+
Grid !== null && isGridMode ? /* @__PURE__ */ jsx(GridLayer, { editor, Grid }) : null,
|
|
15202
15298
|
/* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { ...layerStyle, display: "block" } }),
|
|
15203
15299
|
/* @__PURE__ */ jsx(OverlayLayer, { editor }),
|
|
15204
15300
|
/* @__PURE__ */ jsx(IndicatorCanvas, { editor, util: indicatorOverlayUtil ?? ShapeIndicatorCompositor }),
|
|
@@ -15264,34 +15360,63 @@ function IndicatorCanvas({ editor, util }) {
|
|
|
15264
15360
|
}, [editor, overlay]);
|
|
15265
15361
|
return /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { ...layerStyle, pointerEvents: "none", display: "block" } });
|
|
15266
15362
|
}
|
|
15363
|
+
function useCameraCssVarsOnElement(editor, ref, style) {
|
|
15364
|
+
const own = style;
|
|
15365
|
+
const ownZoom = own?.["--mocanvas-zoom"] !== void 0;
|
|
15366
|
+
const ownScale = own?.["--mocanvas-scale"] !== void 0;
|
|
15367
|
+
useLayoutEffect(() => {
|
|
15368
|
+
const el = ref.current;
|
|
15369
|
+
if (!el) return;
|
|
15370
|
+
return react("canvas.cameraCssVars", () => {
|
|
15371
|
+
const vars = getCameraCssVars(editor.getZoomLevel());
|
|
15372
|
+
if (!ownZoom) el.style.setProperty("--mocanvas-zoom", vars["--mocanvas-zoom"]);
|
|
15373
|
+
if (!ownScale) el.style.setProperty("--mocanvas-scale", vars["--mocanvas-scale"]);
|
|
15374
|
+
});
|
|
15375
|
+
}, [editor, ref, ownZoom, ownScale]);
|
|
15376
|
+
}
|
|
15377
|
+
var GridLayer = track$1(function GridLayer2({ editor, Grid }) {
|
|
15378
|
+
const camera = editor.getCamera();
|
|
15379
|
+
const size = editor.getDocumentSettings().gridSize;
|
|
15380
|
+
return /* @__PURE__ */ jsx(Grid, { x: camera.x, y: camera.y, z: camera.z, size });
|
|
15381
|
+
});
|
|
15267
15382
|
var OverlayLayer = track$1(function OverlayLayer2({ editor }) {
|
|
15268
15383
|
const ids = editor.getOverlayShapeIds();
|
|
15269
15384
|
const clips = editor.getOverlayClips();
|
|
15270
|
-
const
|
|
15385
|
+
const retained = editor.getOverlayRetainedShapeIds();
|
|
15271
15386
|
const editingId = editor.getEditingShapeId();
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15387
|
+
const ref = useRef(null);
|
|
15388
|
+
useLayoutEffect(() => {
|
|
15389
|
+
const el = ref.current;
|
|
15390
|
+
if (!el) return;
|
|
15391
|
+
return react("canvas.overlayTransform", () => {
|
|
15392
|
+
const cam = editor.getCamera();
|
|
15393
|
+
el.style.transform = `scale(${cam.z}) translate(${cam.x}px, ${cam.y}px)`;
|
|
15394
|
+
});
|
|
15395
|
+
}, [editor]);
|
|
15396
|
+
return /* @__PURE__ */ jsx("div", { ref, className: "mocanvas-overlay", style: overlayLayerStyle, children: ids.map((id, i) => {
|
|
15397
|
+
const shape = editor.getShape(id);
|
|
15398
|
+
if (!shape) return null;
|
|
15399
|
+
return /* @__PURE__ */ jsx(
|
|
15400
|
+
OverlayShape,
|
|
15401
|
+
{
|
|
15402
|
+
editor,
|
|
15403
|
+
shape,
|
|
15404
|
+
isEditing: editingId === id,
|
|
15405
|
+
clip: clips[i],
|
|
15406
|
+
isRetained: retained.has(id)
|
|
15281
15407
|
},
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
|
|
15285
|
-
return /* @__PURE__ */ jsx(OverlayShape, { editor, shape, isEditing: editingId === id, clip: clips[i] }, id);
|
|
15286
|
-
})
|
|
15287
|
-
}
|
|
15288
|
-
);
|
|
15408
|
+
id
|
|
15409
|
+
);
|
|
15410
|
+
}) });
|
|
15289
15411
|
});
|
|
15412
|
+
var overlayLayerStyle = { ...layerStyle, pointerEvents: "none", transformOrigin: "0 0" };
|
|
15413
|
+
var RETAINED_STYLE = { contentVisibility: "hidden", visibility: "hidden" };
|
|
15290
15414
|
var OverlayShape = track$1(function OverlayShape2({
|
|
15291
15415
|
editor,
|
|
15292
15416
|
shape,
|
|
15293
15417
|
isEditing,
|
|
15294
|
-
clip
|
|
15418
|
+
clip,
|
|
15419
|
+
isRetained
|
|
15295
15420
|
}) {
|
|
15296
15421
|
const util = editor.getShapeUtil(shape);
|
|
15297
15422
|
const m = editor.getShapePageTransform(shape);
|
|
@@ -15303,6 +15428,7 @@ var OverlayShape = track$1(function OverlayShape2({
|
|
|
15303
15428
|
className: "mocanvas-shape",
|
|
15304
15429
|
"data-shape-id": shape.id,
|
|
15305
15430
|
"data-shape-type": shape.type,
|
|
15431
|
+
"data-retained": isRetained && !clip ? "true" : void 0,
|
|
15306
15432
|
style: {
|
|
15307
15433
|
position: "absolute",
|
|
15308
15434
|
left: 0,
|
|
@@ -15312,7 +15438,8 @@ var OverlayShape = track$1(function OverlayShape2({
|
|
|
15312
15438
|
transformOrigin: "0 0",
|
|
15313
15439
|
transform: `${offset}matrix(${m.a}, ${m.b}, ${m.c}, ${m.d}, ${m.e}, ${m.f})`,
|
|
15314
15440
|
opacity: shape.opacity,
|
|
15315
|
-
pointerEvents: isEditing ? "auto" : "none"
|
|
15441
|
+
pointerEvents: isEditing ? "auto" : "none",
|
|
15442
|
+
...isRetained && !clip ? RETAINED_STYLE : null
|
|
15316
15443
|
},
|
|
15317
15444
|
children: /* @__PURE__ */ jsxs(ShapeBoundary, { shape, children: [
|
|
15318
15445
|
/* @__PURE__ */ jsx(ShapeBody, { editor, shape }),
|
|
@@ -15325,6 +15452,7 @@ var OverlayShape = track$1(function OverlayShape2({
|
|
|
15325
15452
|
"div",
|
|
15326
15453
|
{
|
|
15327
15454
|
className: "mocanvas-shape-clip",
|
|
15455
|
+
"data-retained": isRetained ? "true" : void 0,
|
|
15328
15456
|
style: {
|
|
15329
15457
|
position: "absolute",
|
|
15330
15458
|
left: clip[0],
|
|
@@ -15332,7 +15460,8 @@ var OverlayShape = track$1(function OverlayShape2({
|
|
|
15332
15460
|
width: Math.max(0, clip[2] - clip[0]),
|
|
15333
15461
|
height: Math.max(0, clip[3] - clip[1]),
|
|
15334
15462
|
overflow: "hidden",
|
|
15335
|
-
pointerEvents: "none"
|
|
15463
|
+
pointerEvents: "none",
|
|
15464
|
+
...isRetained ? RETAINED_STYLE : null
|
|
15336
15465
|
},
|
|
15337
15466
|
children: el
|
|
15338
15467
|
}
|
|
@@ -15374,9 +15503,10 @@ var DefaultIndicators = track$1(function DefaultIndicators2({ editor }) {
|
|
|
15374
15503
|
const bounds = editor.getSelectionPageBounds();
|
|
15375
15504
|
const hovered = editor.getHoveredShape();
|
|
15376
15505
|
const selected = editor.getSelectedShapes();
|
|
15506
|
+
const tool = editor.getCurrentToolId();
|
|
15507
|
+
if (!bounds && selected.length === 0 && !(hovered && tool === "select")) return null;
|
|
15377
15508
|
const cam = editor.getCamera();
|
|
15378
15509
|
const z = cam.z;
|
|
15379
|
-
const tool = editor.getCurrentToolId();
|
|
15380
15510
|
const toScreen = (x, y) => [(x + cam.x) * z, (y + cam.y) * z];
|
|
15381
15511
|
const items = [];
|
|
15382
15512
|
const indicatorFor = (shape, key, stroke) => {
|
|
@@ -15502,9 +15632,19 @@ var DefaultBrush = track$1(function DefaultBrush2({ editor }) {
|
|
|
15502
15632
|
}
|
|
15503
15633
|
);
|
|
15504
15634
|
});
|
|
15505
|
-
var ShapeBody = track$1(
|
|
15506
|
-
|
|
15507
|
-
})
|
|
15635
|
+
var ShapeBody = track$1(
|
|
15636
|
+
memo(
|
|
15637
|
+
function ShapeBody2({ editor, shape }) {
|
|
15638
|
+
return /* @__PURE__ */ jsx(Fragment, { children: editor.getShapeUtil(shape).component(shape) });
|
|
15639
|
+
},
|
|
15640
|
+
// A body is a function of its shape's content, not of where the shape is:
|
|
15641
|
+
// the wrapper above carries position, rotation and opacity as a transform.
|
|
15642
|
+
// Comparing the whole record re-rendered every card under a drag on every
|
|
15643
|
+
// frame, though nothing it draws had changed. Signals the body reads still
|
|
15644
|
+
// re-render it through `track`.
|
|
15645
|
+
(prev, next) => prev.editor === next.editor && prev.shape.id === next.shape.id && prev.shape.type === next.shape.type && prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
|
|
15646
|
+
)
|
|
15647
|
+
);
|
|
15508
15648
|
function ShapeBoundary({ shape, children }) {
|
|
15509
15649
|
const Fallback = useErrorSlot("ShapeErrorFallback", DefaultShapeErrorFallback);
|
|
15510
15650
|
return /* @__PURE__ */ jsx(
|