@mocanvas/editor 4.12.3 → 4.13.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/COMPAT.md CHANGED
@@ -96,6 +96,10 @@ predicates (`canTabTo`, `canResizeChildren`, `canBeLaidOut`, `canCull`,
96
96
  `canEditInReadonly`, …) and `createShapeForAsset`. **That is where the remaining
97
97
  work is**, not in the symbol tables above.
98
98
 
99
+ `canCull` is absent from the class but honoured where it matters: a util that
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()`.
102
+
99
103
  You can measure a real application against this surface rather than trusting the
100
104
  tables: `scripts/compat-parity.mjs` type-checks a consumer app with `tldraw`
101
105
  remapped onto `@mocanvas/compat` and reports what breaks, grouped by symbol.
package/dist/index.d.ts CHANGED
@@ -8438,6 +8438,8 @@ declare class Editor extends EventEmitter<EditorEvents> {
8438
8438
  private syncedPageId;
8439
8439
  /** Shapes whose last write to the engine threw; they are warned about once. */
8440
8440
  private readonly brokenShapeIds;
8441
+ /** Shapes on the synced page the engine reports to the DOM overlay (`FLAG.OVERLAY` or `FLAG.LABEL`). */
8442
+ private readonly overlayFlaggedIds;
8441
8443
  /** Handle of the camera animation in flight, if any. */
8442
8444
  private cameraAnimation;
8443
8445
  /**
@@ -8614,6 +8616,18 @@ declare class Editor extends EventEmitter<EditorEvents> {
8614
8616
  updateCurrentPageState(partial: Partial<Omit<InstancePageState, "id" | "typeName" | "pageId">>): this;
8615
8617
  private readonly _allShapes;
8616
8618
  private readonly _currentPageShapes;
8619
+ /**
8620
+ * The DOM-overlay shapes whose util opts out of culling (`canCull() === false`), each with the
8621
+ * clip its ancestors give it, plus the paint rank of every shape on the page; `null` when there
8622
+ * are none.
8623
+ *
8624
+ * The engine reports only the overlay shapes inside the viewport, so without this a shape that
8625
+ * must outlive the viewport — a live embed, a playing video — was unmounted the moment it was
8626
+ * panned or zoomed out of view, and rebuilt when it came back. Recomputed whenever a shape record
8627
+ * on the page changes — every frame of a drag, but never for a camera move — and never at all
8628
+ * when no registered util implements `canCull`.
8629
+ */
8630
+ private readonly _keptOverlay;
8617
8631
  private readonly _currentPageShapeIds;
8618
8632
  /**
8619
8633
  * One shape by id.
@@ -9066,6 +9080,14 @@ declare class Editor extends EventEmitter<EditorEvents> {
9066
9080
  private writeEngineGeometry;
9067
9081
  /** Build and draw one frame. Called by the canvas component inside rAF. */
9068
9082
  renderFrame(backend: RenderBackend): FrameBuffers;
9083
+ /**
9084
+ * Add the overlay shapes that may not be culled to this frame's overlay list, off screen or not,
9085
+ * and put the whole list in paint order. The order must not depend on what is on screen: a shape
9086
+ * that changes place among its siblings is moved in the DOM by React, which restyles and lays out
9087
+ * all of it — the very cost keeping it mounted avoids. A kept shape always carries the clip its
9088
+ * ancestors give it, on screen or off, so its DOM never changes shape at the viewport edge.
9089
+ */
9090
+ private keepUncullableOverlay;
9069
9091
  /**
9070
9092
  * Device-pixel scale at which rasterized textures (text labels) should be
9071
9093
  * drawn: the device pixel ratio times the zoom, bucketed to powers of two.
package/dist/index.js CHANGED
@@ -7956,6 +7956,8 @@ var Editor = class extends EventEmitter {
7956
7956
  syncedPageId = null;
7957
7957
  /** Shapes whose last write to the engine threw; they are warned about once. */
7958
7958
  brokenShapeIds = /* @__PURE__ */ new Set();
7959
+ /** Shapes on the synced page the engine reports to the DOM overlay (`FLAG.OVERLAY` or `FLAG.LABEL`). */
7960
+ overlayFlaggedIds = /* @__PURE__ */ new Set();
7959
7961
  /** Handle of the camera animation in flight, if any. */
7960
7962
  cameraAnimation;
7961
7963
  /**
@@ -8441,6 +8443,45 @@ var Editor = class extends EventEmitter {
8441
8443
  const pageId = this.getCurrentPageId();
8442
8444
  return this._allShapes.get().filter((s) => this.getAncestorPageId(s) === pageId);
8443
8445
  });
8446
+ /**
8447
+ * The DOM-overlay shapes whose util opts out of culling (`canCull() === false`), each with the
8448
+ * clip its ancestors give it, plus the paint rank of every shape on the page; `null` when there
8449
+ * are none.
8450
+ *
8451
+ * The engine reports only the overlay shapes inside the viewport, so without this a shape that
8452
+ * must outlive the viewport — a live embed, a playing video — was unmounted the moment it was
8453
+ * panned or zoomed out of view, and rebuilt when it came back. Recomputed whenever a shape record
8454
+ * on the page changes — every frame of a drag, but never for a camera move — and never at all
8455
+ * when no registered util implements `canCull`.
8456
+ */
8457
+ _keptOverlay = computed("editor.keptOverlay", () => {
8458
+ const optsOut = Object.values(this.shapeUtils).some(
8459
+ (util) => typeof util.canCull === "function"
8460
+ );
8461
+ if (!optsOut) return null;
8462
+ const sorted = this.getCurrentPageShapesSorted();
8463
+ const ids = [];
8464
+ const clips = /* @__PURE__ */ new Map();
8465
+ for (const shape of sorted) {
8466
+ if (!this.overlayFlaggedIds.has(shape.id)) continue;
8467
+ const util = this.shapeUtils[shape.type];
8468
+ if (!util?.canCull || util.canCull(shape)) continue;
8469
+ if (isShapeHidden(this, shape)) continue;
8470
+ const mask = this.getShapeMask(shape);
8471
+ let clip;
8472
+ if (mask !== void 0) {
8473
+ if (mask.length === 0) continue;
8474
+ const box = Box.FromPoints(mask.map((p) => new Vec(p.x, p.y)));
8475
+ const bounds = this.getShapePageBounds(shape);
8476
+ if (!bounds || !bounds.collides(box)) continue;
8477
+ clip = [box.minX, box.minY, box.maxX, box.maxY];
8478
+ }
8479
+ ids.push(shape.id);
8480
+ clips.set(shape.id, clip);
8481
+ }
8482
+ if (ids.length === 0) return null;
8483
+ return { ids, clips, rank: new Map(sorted.map((shape, index) => [shape.id, index])) };
8484
+ });
8444
8485
  _currentPageShapeIds = computed("editor.currentPageShapeIds", () => {
8445
8486
  return new Set(this._currentPageShapes.get().map((s) => s.id));
8446
8487
  });
@@ -10142,6 +10183,7 @@ var Editor = class extends EventEmitter {
10142
10183
  }
10143
10184
  for (const rec of Object.values(changes.removed)) {
10144
10185
  if (rec.typeName !== "shape") continue;
10186
+ this.overlayFlaggedIds.delete(rec.id);
10145
10187
  const h = this.handles.release(rec.id);
10146
10188
  if (h !== void 0) {
10147
10189
  this.engine.cmd.remove(h);
@@ -10158,6 +10200,7 @@ var Editor = class extends EventEmitter {
10158
10200
  if (next.typeName !== "shape" || prev.typeName !== "shape") continue;
10159
10201
  const onPage = this.getAncestorPageId(next) === pageId;
10160
10202
  if (!onPage) {
10203
+ this.overlayFlaggedIds.delete(next.id);
10161
10204
  const h = this.handles.release(next.id);
10162
10205
  if (h !== void 0) {
10163
10206
  this.engine.cmd.remove(h);
@@ -10195,6 +10238,7 @@ var Editor = class extends EventEmitter {
10195
10238
  syncPage(pageId) {
10196
10239
  this.syncedPageId = pageId;
10197
10240
  this.handles.clear();
10241
+ this.overlayFlaggedIds.clear();
10198
10242
  this.engine.cmd.clear();
10199
10243
  for (const shape of this.getCurrentPageShapesSorted()) {
10200
10244
  this.writeShapeToEngine(shape, true);
@@ -10244,6 +10288,8 @@ var Editor = class extends EventEmitter {
10244
10288
  } else {
10245
10289
  flags |= FLAG.OVERLAY;
10246
10290
  }
10291
+ if (flags & (FLAG.OVERLAY | FLAG.LABEL)) this.overlayFlaggedIds.add(shape.id);
10292
+ else this.overlayFlaggedIds.delete(shape.id);
10247
10293
  const bw = engineGeometry?.w ?? geometry?.bounds.w ?? 0;
10248
10294
  const bh = engineGeometry?.h ?? geometry?.bounds.h ?? 0;
10249
10295
  this.engine.cmd.upsert(h, this.kindId(shape.type), parent, zlo, zhi, flags, shape.x, shape.y, shape.rotation, bw, bh);
@@ -10303,6 +10349,7 @@ var Editor = class extends EventEmitter {
10303
10349
  clips.push(o.clip);
10304
10350
  }
10305
10351
  }
10352
+ unsafe__withoutCapture(() => this.keepUncullableOverlay(ids, clips));
10306
10353
  unsafe__withoutCapture(() => {
10307
10354
  const prev = this._overlayShapeIds.get();
10308
10355
  if (prev.length !== ids.length || prev.some((id, i) => id !== ids[i])) this._overlayShapeIds.set(ids);
@@ -10314,6 +10361,28 @@ var Editor = class extends EventEmitter {
10314
10361
  });
10315
10362
  return frame;
10316
10363
  }
10364
+ /**
10365
+ * Add the overlay shapes that may not be culled to this frame's overlay list, off screen or not,
10366
+ * and put the whole list in paint order. The order must not depend on what is on screen: a shape
10367
+ * that changes place among its siblings is moved in the DOM by React, which restyles and lays out
10368
+ * all of it — the very cost keeping it mounted avoids. A kept shape always carries the clip its
10369
+ * ancestors give it, on screen or off, so its DOM never changes shape at the viewport edge.
10370
+ */
10371
+ keepUncullableOverlay(ids, clips) {
10372
+ const kept = this._keptOverlay.get();
10373
+ if (!kept) return;
10374
+ const entries = ids.map((id, i) => ({ id, clip: kept.clips.has(id) ? kept.clips.get(id) : clips[i] }));
10375
+ const present = new Set(ids);
10376
+ for (const id of kept.ids) if (!present.has(id)) entries.push({ id, clip: kept.clips.get(id) });
10377
+ const rank = (id) => kept.rank.get(id) ?? Number.MAX_SAFE_INTEGER;
10378
+ entries.sort((a, b) => rank(a.id) - rank(b.id));
10379
+ ids.length = 0;
10380
+ clips.length = 0;
10381
+ for (const entry of entries) {
10382
+ ids.push(entry.id);
10383
+ clips.push(entry.clip);
10384
+ }
10385
+ }
10317
10386
  // ---- textures ----------------------------------------------------------
10318
10387
  /**
10319
10388
  * Device-pixel scale at which rasterized textures (text labels) should be