@mocanvas/editor 4.13.0 → 4.15.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 +34 -0
- package/MIGRATION.md +1 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +216 -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,40 @@ 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
|
+
|
|
215
|
+
**The zoom in your CSS.** `--mocanvas-zoom` and `--mocanvas-scale` are live on
|
|
216
|
+
the canvas container, but the overlay declares them as constants so that a zoom
|
|
217
|
+
does not restyle every node of every mounted shape — the single largest cost of
|
|
218
|
+
a zoom gesture on a board of DOM cards. A shape whose own CSS reads them says so:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
class CardUtil extends BaseBoxShapeUtil<CardShape> {
|
|
222
|
+
override usesCameraCssVars() {
|
|
223
|
+
return true // this card counter-scales its label
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
That shape then gets the live values on its own wrapper, and its subtree is the
|
|
229
|
+
only one that restyles on a zoom. `options.overlayCameraCssVars: "inherit"`
|
|
230
|
+
turns the mechanism off altogether, for an app that would rather pay than name
|
|
231
|
+
its shapes. Everything outside the overlay — your own chrome, panels, menus —
|
|
232
|
+
reads the live values as before.
|
|
233
|
+
|
|
200
234
|
### `getIndicatorPath` — the selection outline
|
|
201
235
|
|
|
202
236
|
```ts
|
package/MIGRATION.md
CHANGED
|
@@ -273,7 +273,7 @@ it. Two groups are worth knowing about:
|
|
|
273
273
|
|
|
274
274
|
| tldraw token | What you get |
|
|
275
275
|
| --- | --- |
|
|
276
|
-
| `--tl-zoom`, `--tl-scale` | Real values. mocanvas
|
|
276
|
+
| `--tl-zoom`, `--tl-scale` | Real values. mocanvas stamps `--mocanvas-zoom` and `--mocanvas-scale` on the canvas container and restamps them on zoom, so `calc()` rules that hold a constant on-screen size keep working. Inside the DOM overlay they are constants unless the shape's util says `usesCameraCssVars()` — a tldraw shape whose CSS counter-scales with `--tl-scale` needs that line, or `options.overlayCameraCssVars: "inherit"`. |
|
|
277
277
|
| `--tl-color-overlay`, `--tl-color-background-overlay`, `--tl-color-warn` | A static fallback. mocanvas has no equivalent, so these do not follow your theme — set them yourself if they matter. |
|
|
278
278
|
|
|
279
279
|
Everything else maps onto a themed mocanvas variable and follows a theme swap
|
package/dist/index.d.ts
CHANGED
|
@@ -3483,6 +3483,19 @@ declare abstract class ShapeUtil<T extends UnknownShape = UnknownShape, D extend
|
|
|
3483
3483
|
getRenderStyle(_shape: T): StyleWords | null;
|
|
3484
3484
|
/** Whether the shape should be drawn by the DOM overlay even if it has a render style (e.g. while editing). */
|
|
3485
3485
|
needsOverlay(shape: T): boolean;
|
|
3486
|
+
/**
|
|
3487
|
+
* Whether this shape's own CSS reads `--mocanvas-zoom` / `--mocanvas-scale`.
|
|
3488
|
+
*
|
|
3489
|
+
* The overlay stops those variables inheriting into mounted shapes, because a
|
|
3490
|
+
* custom property that changes on every zoom frame restyles every node below
|
|
3491
|
+
* it — the cost of a zoom on a board of DOM cards. A shape that says `true`
|
|
3492
|
+
* here gets the live values on its own wrapper, and pays that restyle for
|
|
3493
|
+
* itself: the counter-scaled label, the hairline that must stay 1px.
|
|
3494
|
+
*
|
|
3495
|
+
* Default `false`. `options.overlayCameraCssVars: "inherit"` turns the whole
|
|
3496
|
+
* mechanism off for an app that would rather not name its shapes.
|
|
3497
|
+
*/
|
|
3498
|
+
usesCameraCssVars(_shape: T): boolean;
|
|
3486
3499
|
/**
|
|
3487
3500
|
* Whether `component` should be rendered in the DOM overlay *in addition to*
|
|
3488
3501
|
* the GPU geometry (e.g. a text label on a filled shape).
|
|
@@ -8313,6 +8326,41 @@ interface EditorConfig extends TldrawOptions {
|
|
|
8313
8326
|
* enough to debounce at all, and how much movement is worth publishing.
|
|
8314
8327
|
*/
|
|
8315
8328
|
debouncedZoomOctaves: number;
|
|
8329
|
+
/**
|
|
8330
|
+
* How many DOM-overlay shapes that left the viewport stay mounted, hidden, so
|
|
8331
|
+
* coming back into view costs a style change instead of a React remount.
|
|
8332
|
+
*
|
|
8333
|
+
* The engine culls the overlay to the viewport exactly, so without this every
|
|
8334
|
+
* card that crosses the viewport edge during a pan or a zoom is unmounted and
|
|
8335
|
+
* rebuilt from nothing when the gesture comes back — on a board of heavy React
|
|
8336
|
+
* cards, dozens of remounts per gesture. Retained shapes are the most recently
|
|
8337
|
+
* culled ones (least recently visible are evicted first), they are listed in
|
|
8338
|
+
* {@link Editor.getOverlayRetainedShapeIds}, and the canvas hides them with
|
|
8339
|
+
* `content-visibility: hidden`, which keeps their DOM and layout but skips
|
|
8340
|
+
* their rendering.
|
|
8341
|
+
*
|
|
8342
|
+
* The bound is the memory cost: up to this many shapes' DOM is kept alive off
|
|
8343
|
+
* screen. `0` turns retention off and unmounts at the edge, as every release before
|
|
8344
|
+
* this option did.
|
|
8345
|
+
*/
|
|
8346
|
+
overlayRetainLimit: number;
|
|
8347
|
+
/**
|
|
8348
|
+
* Whether the DOM overlay lets `--mocanvas-zoom` / `--mocanvas-scale` inherit
|
|
8349
|
+
* into the shapes below it.
|
|
8350
|
+
*
|
|
8351
|
+
* `"cut"` (the default) has the overlay declare them as constants, so the
|
|
8352
|
+
* container's — rewritten on every frame of a zoom — stop there. A changed
|
|
8353
|
+
* inherited custom property restyles every descendant, and on a board of
|
|
8354
|
+
* mounted DOM cards that is every node of every shape: 41 ms of style
|
|
8355
|
+
* recalculation per zoom step over 1,000 live cards, against 0.1 ms with the
|
|
8356
|
+
* inheritance cut (`apps/bench`, `dom-overlay.mjs`).
|
|
8357
|
+
*
|
|
8358
|
+
* A shape that reads the live zoom in its own CSS says so with
|
|
8359
|
+
* `ShapeUtil.usesCameraCssVars()` and gets the real values on its wrapper.
|
|
8360
|
+
* `"inherit"` restores the old behaviour for an app whose shape CSS reads the
|
|
8361
|
+
* variables and that would rather pay than name the shapes.
|
|
8362
|
+
*/
|
|
8363
|
+
overlayCameraCssVars: "cut" | "inherit";
|
|
8316
8364
|
}
|
|
8317
8365
|
declare const DEFAULT_EDITOR_CONFIG: EditorConfig;
|
|
8318
8366
|
interface HitTestOptions {
|
|
@@ -8431,6 +8479,15 @@ declare class Editor extends EventEmitter<EditorEvents> {
|
|
|
8431
8479
|
private readonly _frameEpoch;
|
|
8432
8480
|
private readonly _overlayShapeIds;
|
|
8433
8481
|
private readonly _overlayClips;
|
|
8482
|
+
private readonly _overlayRetained;
|
|
8483
|
+
/**
|
|
8484
|
+
* Overlay shapes the engine culled that are still mounted, oldest first: a
|
|
8485
|
+
* `Map` iterates in insertion order, which is the LRU order eviction wants.
|
|
8486
|
+
* Each carries the clip it had when it was last on screen.
|
|
8487
|
+
*/
|
|
8488
|
+
private readonly overlayRetained;
|
|
8489
|
+
/** Last frame's on-screen overlay shapes (engine list plus kept ones), with their clips. */
|
|
8490
|
+
private overlayOnScreen;
|
|
8434
8491
|
private readonly _isDisposed;
|
|
8435
8492
|
private readonly _lastFrame;
|
|
8436
8493
|
private readonly _cameraOptions;
|
|
@@ -8515,8 +8572,18 @@ declare class Editor extends EventEmitter<EditorEvents> {
|
|
|
8515
8572
|
culled: number;
|
|
8516
8573
|
ms: number;
|
|
8517
8574
|
};
|
|
8518
|
-
/**
|
|
8575
|
+
/**
|
|
8576
|
+
* Shapes the DOM overlay must keep mounted this frame, in draw order: the ones
|
|
8577
|
+
* on screen, plus those listed by {@link getOverlayRetainedShapeIds}, which are
|
|
8578
|
+
* off screen and mounted only so that coming back does not rebuild them.
|
|
8579
|
+
*/
|
|
8519
8580
|
getOverlayShapeIds(): readonly ShapeId[];
|
|
8581
|
+
/**
|
|
8582
|
+
* The subset of {@link getOverlayShapeIds} that the engine culled and the
|
|
8583
|
+
* overlay keeps mounted but hidden — see `options.overlayRetainLimit`. Empty
|
|
8584
|
+
* when nothing is retained.
|
|
8585
|
+
*/
|
|
8586
|
+
getOverlayRetainedShapeIds(): ReadonlySet<ShapeId>;
|
|
8520
8587
|
/**
|
|
8521
8588
|
* Page-space clip rects for the overlay shapes, parallel to
|
|
8522
8589
|
* `getOverlayShapeIds()`; `undefined` where the shape is unclipped.
|
|
@@ -9088,6 +9155,27 @@ declare class Editor extends EventEmitter<EditorEvents> {
|
|
|
9088
9155
|
* ancestors give it, on screen or off, so its DOM never changes shape at the viewport edge.
|
|
9089
9156
|
*/
|
|
9090
9157
|
private keepUncullableOverlay;
|
|
9158
|
+
/**
|
|
9159
|
+
* Keep the overlay shapes that just left the viewport mounted, up to
|
|
9160
|
+
* `options.overlayRetainLimit`, and put the whole list in paint order.
|
|
9161
|
+
*
|
|
9162
|
+
* A shape that the engine stops reporting moves into a bounded LRU; one that
|
|
9163
|
+
* comes back moves out of it. Either way its entry in the list — and so its
|
|
9164
|
+
* React element and DOM — is untouched; only its retained flag flips. The list
|
|
9165
|
+
* is sorted by paint rank whenever anything is retained, so a shape crossing
|
|
9166
|
+
* the viewport edge never changes position among its siblings either: React
|
|
9167
|
+
* would move its DOM for that, and moving a node with an iframe in it reloads
|
|
9168
|
+
* the iframe.
|
|
9169
|
+
*
|
|
9170
|
+
* Returns the retained ids, which the canvas hides.
|
|
9171
|
+
*/
|
|
9172
|
+
private retainCulledOverlay;
|
|
9173
|
+
/**
|
|
9174
|
+
* Every shape on the current page by paint order. Recomputed when a shape
|
|
9175
|
+
* record changes — not when the camera moves, which is when the overlay
|
|
9176
|
+
* needs it.
|
|
9177
|
+
*/
|
|
9178
|
+
private readonly _paintRank;
|
|
9091
9179
|
/**
|
|
9092
9180
|
* Device-pixel scale at which rasterized textures (text labels) should be
|
|
9093
9181
|
* 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,12 @@ 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,
|
|
7857
|
+
overlayCameraCssVars: "cut"
|
|
7853
7858
|
};
|
|
7854
7859
|
var RAD_PER_DEG = Math.PI / 180;
|
|
7855
7860
|
var WHEEL_ZOOM_DELTA_CAP = 50;
|
|
@@ -7949,6 +7954,15 @@ var Editor = class extends EventEmitter {
|
|
|
7949
7954
|
_frameEpoch;
|
|
7950
7955
|
_overlayShapeIds;
|
|
7951
7956
|
_overlayClips;
|
|
7957
|
+
_overlayRetained;
|
|
7958
|
+
/**
|
|
7959
|
+
* Overlay shapes the engine culled that are still mounted, oldest first: a
|
|
7960
|
+
* `Map` iterates in insertion order, which is the LRU order eviction wants.
|
|
7961
|
+
* Each carries the clip it had when it was last on screen.
|
|
7962
|
+
*/
|
|
7963
|
+
overlayRetained = /* @__PURE__ */ new Map();
|
|
7964
|
+
/** Last frame's on-screen overlay shapes (engine list plus kept ones), with their clips. */
|
|
7965
|
+
overlayOnScreen = /* @__PURE__ */ new Map();
|
|
7952
7966
|
_isDisposed;
|
|
7953
7967
|
_lastFrame;
|
|
7954
7968
|
_cameraOptions;
|
|
@@ -8015,6 +8029,7 @@ var Editor = class extends EventEmitter {
|
|
|
8015
8029
|
});
|
|
8016
8030
|
this._overlayShapeIds = atom("editor.overlayShapeIds", []);
|
|
8017
8031
|
this._overlayClips = atom("editor.overlayClips", []);
|
|
8032
|
+
this._overlayRetained = atom("editor.overlayRetained", EMPTY_ID_SET);
|
|
8018
8033
|
this._isDisposed = atom("editor.isDisposed", false);
|
|
8019
8034
|
this._isMounted = atom("editor.isMounted", false);
|
|
8020
8035
|
this._lastFrame = atom("editor.lastFrame", { drawn: 0, culled: 0, ms: 0 });
|
|
@@ -8209,10 +8224,22 @@ var Editor = class extends EventEmitter {
|
|
|
8209
8224
|
getLastFrameStats() {
|
|
8210
8225
|
return this._lastFrame.get();
|
|
8211
8226
|
}
|
|
8212
|
-
/**
|
|
8227
|
+
/**
|
|
8228
|
+
* Shapes the DOM overlay must keep mounted this frame, in draw order: the ones
|
|
8229
|
+
* on screen, plus those listed by {@link getOverlayRetainedShapeIds}, which are
|
|
8230
|
+
* off screen and mounted only so that coming back does not rebuild them.
|
|
8231
|
+
*/
|
|
8213
8232
|
getOverlayShapeIds() {
|
|
8214
8233
|
return this._overlayShapeIds.get();
|
|
8215
8234
|
}
|
|
8235
|
+
/**
|
|
8236
|
+
* The subset of {@link getOverlayShapeIds} that the engine culled and the
|
|
8237
|
+
* overlay keeps mounted but hidden — see `options.overlayRetainLimit`. Empty
|
|
8238
|
+
* when nothing is retained.
|
|
8239
|
+
*/
|
|
8240
|
+
getOverlayRetainedShapeIds() {
|
|
8241
|
+
return this._overlayRetained.get();
|
|
8242
|
+
}
|
|
8216
8243
|
/**
|
|
8217
8244
|
* Page-space clip rects for the overlay shapes, parallel to
|
|
8218
8245
|
* `getOverlayShapeIds()`; `undefined` where the shape is unclipped.
|
|
@@ -10350,10 +10377,12 @@ var Editor = class extends EventEmitter {
|
|
|
10350
10377
|
}
|
|
10351
10378
|
}
|
|
10352
10379
|
unsafe__withoutCapture(() => this.keepUncullableOverlay(ids, clips));
|
|
10380
|
+
const retained = unsafe__withoutCapture(() => this.retainCulledOverlay(ids, clips));
|
|
10353
10381
|
unsafe__withoutCapture(() => {
|
|
10354
10382
|
const prev = this._overlayShapeIds.get();
|
|
10355
10383
|
if (prev.length !== ids.length || prev.some((id, i) => id !== ids[i])) this._overlayShapeIds.set(ids);
|
|
10356
10384
|
if (!sameClips(this._overlayClips.get(), clips)) this._overlayClips.set(clips);
|
|
10385
|
+
if (!sameIdSet(this._overlayRetained.get(), retained)) this._overlayRetained.set(retained);
|
|
10357
10386
|
const ms = performance.now() - t0;
|
|
10358
10387
|
this._lastFrame.set({ drawn: frame.drawn, culled: frame.culled, ms });
|
|
10359
10388
|
this.emit("frame", { drawn: frame.drawn, culled: frame.culled, ms });
|
|
@@ -10383,6 +10412,69 @@ var Editor = class extends EventEmitter {
|
|
|
10383
10412
|
clips.push(entry.clip);
|
|
10384
10413
|
}
|
|
10385
10414
|
}
|
|
10415
|
+
/**
|
|
10416
|
+
* Keep the overlay shapes that just left the viewport mounted, up to
|
|
10417
|
+
* `options.overlayRetainLimit`, and put the whole list in paint order.
|
|
10418
|
+
*
|
|
10419
|
+
* A shape that the engine stops reporting moves into a bounded LRU; one that
|
|
10420
|
+
* comes back moves out of it. Either way its entry in the list — and so its
|
|
10421
|
+
* React element and DOM — is untouched; only its retained flag flips. The list
|
|
10422
|
+
* is sorted by paint rank whenever anything is retained, so a shape crossing
|
|
10423
|
+
* the viewport edge never changes position among its siblings either: React
|
|
10424
|
+
* would move its DOM for that, and moving a node with an iframe in it reloads
|
|
10425
|
+
* the iframe.
|
|
10426
|
+
*
|
|
10427
|
+
* Returns the retained ids, which the canvas hides.
|
|
10428
|
+
*/
|
|
10429
|
+
retainCulledOverlay(ids, clips) {
|
|
10430
|
+
const limit = Math.max(0, Math.floor(this.options.overlayRetainLimit));
|
|
10431
|
+
const lastOnScreen = this.overlayOnScreen;
|
|
10432
|
+
const onScreen = /* @__PURE__ */ new Map();
|
|
10433
|
+
for (let i = 0; i < ids.length; i++) onScreen.set(ids[i], clips[i]);
|
|
10434
|
+
this.overlayOnScreen = onScreen;
|
|
10435
|
+
const retainedMap = this.overlayRetained;
|
|
10436
|
+
if (limit === 0) {
|
|
10437
|
+
retainedMap.clear();
|
|
10438
|
+
return EMPTY_ID_SET;
|
|
10439
|
+
}
|
|
10440
|
+
for (const id of onScreen.keys()) retainedMap.delete(id);
|
|
10441
|
+
for (const [id, clip] of lastOnScreen) {
|
|
10442
|
+
if (onScreen.has(id)) continue;
|
|
10443
|
+
retainedMap.delete(id);
|
|
10444
|
+
retainedMap.set(id, clip);
|
|
10445
|
+
}
|
|
10446
|
+
if (retainedMap.size === 0) return EMPTY_ID_SET;
|
|
10447
|
+
const pageId = this.getCurrentPageId();
|
|
10448
|
+
for (const id of [...retainedMap.keys()]) {
|
|
10449
|
+
const shape = this.getShape(id);
|
|
10450
|
+
if (!shape || !this.overlayFlaggedIds.has(id) || this.getAncestorPageId(shape) !== pageId || isShapeHidden(this, shape)) {
|
|
10451
|
+
retainedMap.delete(id);
|
|
10452
|
+
}
|
|
10453
|
+
}
|
|
10454
|
+
while (retainedMap.size > limit) retainedMap.delete(retainedMap.keys().next().value);
|
|
10455
|
+
if (retainedMap.size === 0) return EMPTY_ID_SET;
|
|
10456
|
+
const rank = this._paintRank.get();
|
|
10457
|
+
const entries = [];
|
|
10458
|
+
for (const [id, clip] of onScreen) entries.push({ id, clip });
|
|
10459
|
+
for (const [id, clip] of retainedMap) entries.push({ id, clip });
|
|
10460
|
+
const rankOf = (id) => rank.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
10461
|
+
entries.sort((a, b) => rankOf(a.id) - rankOf(b.id));
|
|
10462
|
+
ids.length = 0;
|
|
10463
|
+
clips.length = 0;
|
|
10464
|
+
for (const entry of entries) {
|
|
10465
|
+
ids.push(entry.id);
|
|
10466
|
+
clips.push(entry.clip);
|
|
10467
|
+
}
|
|
10468
|
+
return new Set(retainedMap.keys());
|
|
10469
|
+
}
|
|
10470
|
+
/**
|
|
10471
|
+
* Every shape on the current page by paint order. Recomputed when a shape
|
|
10472
|
+
* record changes — not when the camera moves, which is when the overlay
|
|
10473
|
+
* needs it.
|
|
10474
|
+
*/
|
|
10475
|
+
_paintRank = computed("editor.paintRank", () => {
|
|
10476
|
+
return new Map(this.getCurrentPageShapesSorted().map((shape, index) => [shape.id, index]));
|
|
10477
|
+
});
|
|
10386
10478
|
// ---- textures ----------------------------------------------------------
|
|
10387
10479
|
/**
|
|
10388
10480
|
* Device-pixel scale at which rasterized textures (text labels) should be
|
|
@@ -11622,6 +11714,13 @@ var Editor = class extends EventEmitter {
|
|
|
11622
11714
|
/** Which menus are open right now. Backed by the `instance` session record. */
|
|
11623
11715
|
menus = new MenuManager(this);
|
|
11624
11716
|
};
|
|
11717
|
+
var EMPTY_ID_SET = /* @__PURE__ */ new Set();
|
|
11718
|
+
function sameIdSet(a, b) {
|
|
11719
|
+
if (a === b) return true;
|
|
11720
|
+
if (a.size !== b.size) return false;
|
|
11721
|
+
for (const id of a) if (!b.has(id)) return false;
|
|
11722
|
+
return true;
|
|
11723
|
+
}
|
|
11625
11724
|
function sameClips(a, b) {
|
|
11626
11725
|
if (a.length !== b.length) return false;
|
|
11627
11726
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -13813,6 +13912,21 @@ var ShapeUtil = class {
|
|
|
13813
13912
|
needsOverlay(shape) {
|
|
13814
13913
|
return this.editor.getEditingShapeId() === shape.id;
|
|
13815
13914
|
}
|
|
13915
|
+
/**
|
|
13916
|
+
* Whether this shape's own CSS reads `--mocanvas-zoom` / `--mocanvas-scale`.
|
|
13917
|
+
*
|
|
13918
|
+
* The overlay stops those variables inheriting into mounted shapes, because a
|
|
13919
|
+
* custom property that changes on every zoom frame restyles every node below
|
|
13920
|
+
* it — the cost of a zoom on a board of DOM cards. A shape that says `true`
|
|
13921
|
+
* here gets the live values on its own wrapper, and pays that restyle for
|
|
13922
|
+
* itself: the counter-scaled label, the hairline that must stay 1px.
|
|
13923
|
+
*
|
|
13924
|
+
* Default `false`. `options.overlayCameraCssVars: "inherit"` turns the whole
|
|
13925
|
+
* mechanism off for an app that would rather not name its shapes.
|
|
13926
|
+
*/
|
|
13927
|
+
usesCameraCssVars(_shape) {
|
|
13928
|
+
return false;
|
|
13929
|
+
}
|
|
13816
13930
|
/**
|
|
13817
13931
|
* Whether `component` should be rendered in the DOM overlay *in addition to*
|
|
13818
13932
|
* the GPU geometry (e.g. a text label on a filled shape).
|
|
@@ -15174,10 +15288,8 @@ function CanvasBody({ editor, className, style, children, components, indicatorO
|
|
|
15174
15288
|
const cursor = useValue("cursor", () => cssCursor(editor.getInstanceState().cursor.type), [editor]);
|
|
15175
15289
|
const hasChromeContextMenu = useEditorComponents().ContextMenu != null;
|
|
15176
15290
|
const themeVars = useThemeCssVars(editor);
|
|
15177
|
-
|
|
15291
|
+
useCameraCssVarsOnElement(editor, containerRef, style);
|
|
15178
15292
|
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
15293
|
const Grid = components?.Grid === void 0 ? DefaultGrid : components.Grid;
|
|
15182
15294
|
const Indicators = components?.Indicators ?? DefaultIndicators;
|
|
15183
15295
|
const Brush = components?.Brush ?? (hasOverlay(editor, "brush") ? null : DefaultBrush);
|
|
@@ -15188,7 +15300,7 @@ function CanvasBody({ editor, className, style, children, components, indicatorO
|
|
|
15188
15300
|
{
|
|
15189
15301
|
ref: containerRef,
|
|
15190
15302
|
className: className ? `mocanvas ${className}` : "mocanvas",
|
|
15191
|
-
style: { ...containerStyle, background: backgroundFor(editor), ...themeVars, ...
|
|
15303
|
+
style: { ...containerStyle, background: backgroundFor(editor), ...themeVars, ...style, cursor },
|
|
15192
15304
|
tabIndex: 0,
|
|
15193
15305
|
onPointerDown: events.onPointerDown,
|
|
15194
15306
|
onPointerMove: events.onPointerMove,
|
|
@@ -15198,7 +15310,7 @@ function CanvasBody({ editor, className, style, children, components, indicatorO
|
|
|
15198
15310
|
"data-testid": "mocanvas-container",
|
|
15199
15311
|
children: [
|
|
15200
15312
|
Background ? /* @__PURE__ */ jsx(Background, { editor }) : null,
|
|
15201
|
-
Grid !== null && isGridMode ? /* @__PURE__ */ jsx(
|
|
15313
|
+
Grid !== null && isGridMode ? /* @__PURE__ */ jsx(GridLayer, { editor, Grid }) : null,
|
|
15202
15314
|
/* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { ...layerStyle, display: "block" } }),
|
|
15203
15315
|
/* @__PURE__ */ jsx(OverlayLayer, { editor }),
|
|
15204
15316
|
/* @__PURE__ */ jsx(IndicatorCanvas, { editor, util: indicatorOverlayUtil ?? ShapeIndicatorCompositor }),
|
|
@@ -15264,45 +15376,102 @@ function IndicatorCanvas({ editor, util }) {
|
|
|
15264
15376
|
}, [editor, overlay]);
|
|
15265
15377
|
return /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { ...layerStyle, pointerEvents: "none", display: "block" } });
|
|
15266
15378
|
}
|
|
15379
|
+
function useCameraCssVarsOnElement(editor, ref, style) {
|
|
15380
|
+
const own = style;
|
|
15381
|
+
const ownZoom = own?.["--mocanvas-zoom"] !== void 0;
|
|
15382
|
+
const ownScale = own?.["--mocanvas-scale"] !== void 0;
|
|
15383
|
+
useLayoutEffect(() => {
|
|
15384
|
+
const el = ref.current;
|
|
15385
|
+
if (!el) return;
|
|
15386
|
+
return react("canvas.cameraCssVars", () => {
|
|
15387
|
+
const vars = getCameraCssVars(editor.getZoomLevel());
|
|
15388
|
+
if (!ownZoom) el.style.setProperty("--mocanvas-zoom", vars["--mocanvas-zoom"]);
|
|
15389
|
+
if (!ownScale) el.style.setProperty("--mocanvas-scale", vars["--mocanvas-scale"]);
|
|
15390
|
+
});
|
|
15391
|
+
}, [editor, ref, ownZoom, ownScale]);
|
|
15392
|
+
}
|
|
15393
|
+
function useCameraCssVarsOnShape(editor, enabled) {
|
|
15394
|
+
const ref = useRef(null);
|
|
15395
|
+
useLayoutEffect(() => {
|
|
15396
|
+
const el = ref.current;
|
|
15397
|
+
if (!el) return;
|
|
15398
|
+
if (!enabled) {
|
|
15399
|
+
el.style.removeProperty("--mocanvas-zoom");
|
|
15400
|
+
el.style.removeProperty("--mocanvas-scale");
|
|
15401
|
+
return;
|
|
15402
|
+
}
|
|
15403
|
+
return react("canvas.shapeCameraCssVars", () => {
|
|
15404
|
+
const vars = getCameraCssVars(editor.getZoomLevel());
|
|
15405
|
+
el.style.setProperty("--mocanvas-zoom", vars["--mocanvas-zoom"]);
|
|
15406
|
+
el.style.setProperty("--mocanvas-scale", vars["--mocanvas-scale"]);
|
|
15407
|
+
});
|
|
15408
|
+
}, [editor, enabled]);
|
|
15409
|
+
return ref;
|
|
15410
|
+
}
|
|
15411
|
+
var GridLayer = track$1(function GridLayer2({ editor, Grid }) {
|
|
15412
|
+
const camera = editor.getCamera();
|
|
15413
|
+
const size = editor.getDocumentSettings().gridSize;
|
|
15414
|
+
return /* @__PURE__ */ jsx(Grid, { x: camera.x, y: camera.y, z: camera.z, size });
|
|
15415
|
+
});
|
|
15267
15416
|
var OverlayLayer = track$1(function OverlayLayer2({ editor }) {
|
|
15268
15417
|
const ids = editor.getOverlayShapeIds();
|
|
15269
15418
|
const clips = editor.getOverlayClips();
|
|
15270
|
-
const
|
|
15419
|
+
const retained = editor.getOverlayRetainedShapeIds();
|
|
15271
15420
|
const editingId = editor.getEditingShapeId();
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15421
|
+
const ref = useRef(null);
|
|
15422
|
+
useLayoutEffect(() => {
|
|
15423
|
+
const el = ref.current;
|
|
15424
|
+
if (!el) return;
|
|
15425
|
+
return react("canvas.overlayTransform", () => {
|
|
15426
|
+
const cam = editor.getCamera();
|
|
15427
|
+
el.style.transform = `scale(${cam.z}) translate(${cam.x}px, ${cam.y}px)`;
|
|
15428
|
+
});
|
|
15429
|
+
}, [editor]);
|
|
15430
|
+
const cut = editor.options.overlayCameraCssVars !== "inherit";
|
|
15431
|
+
return /* @__PURE__ */ jsx("div", { ref, className: "mocanvas-overlay", style: cut ? overlayLayerStyleCut : overlayLayerStyle, children: ids.map((id, i) => {
|
|
15432
|
+
const shape = editor.getShape(id);
|
|
15433
|
+
if (!shape) return null;
|
|
15434
|
+
return /* @__PURE__ */ jsx(
|
|
15435
|
+
OverlayShape,
|
|
15436
|
+
{
|
|
15437
|
+
editor,
|
|
15438
|
+
shape,
|
|
15439
|
+
isEditing: editingId === id,
|
|
15440
|
+
clip: clips[i],
|
|
15441
|
+
isRetained: retained.has(id)
|
|
15281
15442
|
},
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
|
|
15285
|
-
return /* @__PURE__ */ jsx(OverlayShape, { editor, shape, isEditing: editingId === id, clip: clips[i] }, id);
|
|
15286
|
-
})
|
|
15287
|
-
}
|
|
15288
|
-
);
|
|
15443
|
+
id
|
|
15444
|
+
);
|
|
15445
|
+
}) });
|
|
15289
15446
|
});
|
|
15447
|
+
var overlayLayerStyle = { ...layerStyle, pointerEvents: "none", transformOrigin: "0 0" };
|
|
15448
|
+
var overlayLayerStyleCut = {
|
|
15449
|
+
...overlayLayerStyle,
|
|
15450
|
+
["--mocanvas-zoom"]: "1",
|
|
15451
|
+
["--mocanvas-scale"]: "1"
|
|
15452
|
+
};
|
|
15453
|
+
var RETAINED_STYLE = { contentVisibility: "hidden", visibility: "hidden" };
|
|
15290
15454
|
var OverlayShape = track$1(function OverlayShape2({
|
|
15291
15455
|
editor,
|
|
15292
15456
|
shape,
|
|
15293
15457
|
isEditing,
|
|
15294
|
-
clip
|
|
15458
|
+
clip,
|
|
15459
|
+
isRetained
|
|
15295
15460
|
}) {
|
|
15296
15461
|
const util = editor.getShapeUtil(shape);
|
|
15297
15462
|
const m = editor.getShapePageTransform(shape);
|
|
15298
15463
|
const bounds = editor.getShapeGeometryBounds(shape);
|
|
15464
|
+
const asksForCamera = util.usesCameraCssVars?.(shape) === true;
|
|
15465
|
+
const cameraRef = useCameraCssVarsOnShape(editor, asksForCamera);
|
|
15299
15466
|
const offset = clip ? `translate(${-clip[0]}px, ${-clip[1]}px) ` : "";
|
|
15300
15467
|
const el = /* @__PURE__ */ jsx(
|
|
15301
15468
|
"div",
|
|
15302
15469
|
{
|
|
15470
|
+
ref: cameraRef,
|
|
15303
15471
|
className: "mocanvas-shape",
|
|
15304
15472
|
"data-shape-id": shape.id,
|
|
15305
15473
|
"data-shape-type": shape.type,
|
|
15474
|
+
"data-retained": isRetained && !clip ? "true" : void 0,
|
|
15306
15475
|
style: {
|
|
15307
15476
|
position: "absolute",
|
|
15308
15477
|
left: 0,
|
|
@@ -15312,7 +15481,8 @@ var OverlayShape = track$1(function OverlayShape2({
|
|
|
15312
15481
|
transformOrigin: "0 0",
|
|
15313
15482
|
transform: `${offset}matrix(${m.a}, ${m.b}, ${m.c}, ${m.d}, ${m.e}, ${m.f})`,
|
|
15314
15483
|
opacity: shape.opacity,
|
|
15315
|
-
pointerEvents: isEditing ? "auto" : "none"
|
|
15484
|
+
pointerEvents: isEditing ? "auto" : "none",
|
|
15485
|
+
...isRetained && !clip ? RETAINED_STYLE : null
|
|
15316
15486
|
},
|
|
15317
15487
|
children: /* @__PURE__ */ jsxs(ShapeBoundary, { shape, children: [
|
|
15318
15488
|
/* @__PURE__ */ jsx(ShapeBody, { editor, shape }),
|
|
@@ -15325,6 +15495,7 @@ var OverlayShape = track$1(function OverlayShape2({
|
|
|
15325
15495
|
"div",
|
|
15326
15496
|
{
|
|
15327
15497
|
className: "mocanvas-shape-clip",
|
|
15498
|
+
"data-retained": isRetained ? "true" : void 0,
|
|
15328
15499
|
style: {
|
|
15329
15500
|
position: "absolute",
|
|
15330
15501
|
left: clip[0],
|
|
@@ -15332,7 +15503,8 @@ var OverlayShape = track$1(function OverlayShape2({
|
|
|
15332
15503
|
width: Math.max(0, clip[2] - clip[0]),
|
|
15333
15504
|
height: Math.max(0, clip[3] - clip[1]),
|
|
15334
15505
|
overflow: "hidden",
|
|
15335
|
-
pointerEvents: "none"
|
|
15506
|
+
pointerEvents: "none",
|
|
15507
|
+
...isRetained ? RETAINED_STYLE : null
|
|
15336
15508
|
},
|
|
15337
15509
|
children: el
|
|
15338
15510
|
}
|
|
@@ -15374,9 +15546,10 @@ var DefaultIndicators = track$1(function DefaultIndicators2({ editor }) {
|
|
|
15374
15546
|
const bounds = editor.getSelectionPageBounds();
|
|
15375
15547
|
const hovered = editor.getHoveredShape();
|
|
15376
15548
|
const selected = editor.getSelectedShapes();
|
|
15549
|
+
const tool = editor.getCurrentToolId();
|
|
15550
|
+
if (!bounds && selected.length === 0 && !(hovered && tool === "select")) return null;
|
|
15377
15551
|
const cam = editor.getCamera();
|
|
15378
15552
|
const z = cam.z;
|
|
15379
|
-
const tool = editor.getCurrentToolId();
|
|
15380
15553
|
const toScreen = (x, y) => [(x + cam.x) * z, (y + cam.y) * z];
|
|
15381
15554
|
const items = [];
|
|
15382
15555
|
const indicatorFor = (shape, key, stroke) => {
|
|
@@ -15502,9 +15675,19 @@ var DefaultBrush = track$1(function DefaultBrush2({ editor }) {
|
|
|
15502
15675
|
}
|
|
15503
15676
|
);
|
|
15504
15677
|
});
|
|
15505
|
-
var ShapeBody = track$1(
|
|
15506
|
-
|
|
15507
|
-
})
|
|
15678
|
+
var ShapeBody = track$1(
|
|
15679
|
+
memo(
|
|
15680
|
+
function ShapeBody2({ editor, shape }) {
|
|
15681
|
+
return /* @__PURE__ */ jsx(Fragment, { children: editor.getShapeUtil(shape).component(shape) });
|
|
15682
|
+
},
|
|
15683
|
+
// A body is a function of its shape's content, not of where the shape is:
|
|
15684
|
+
// the wrapper above carries position, rotation and opacity as a transform.
|
|
15685
|
+
// Comparing the whole record re-rendered every card under a drag on every
|
|
15686
|
+
// frame, though nothing it draws had changed. Signals the body reads still
|
|
15687
|
+
// re-render it through `track`.
|
|
15688
|
+
(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
|
|
15689
|
+
)
|
|
15690
|
+
);
|
|
15508
15691
|
function ShapeBoundary({ shape, children }) {
|
|
15509
15692
|
const Fallback = useErrorSlot("ShapeErrorFallback", DefaultShapeErrorFallback);
|
|
15510
15693
|
return /* @__PURE__ */ jsx(
|