@mocanvas/editor 4.12.2 → 4.12.4

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
  });
@@ -9130,7 +9171,9 @@ var Editor = class extends EventEmitter {
9130
9171
  }
9131
9172
  if (patches.length) this.updateShapes(patches);
9132
9173
  });
9133
- return ids.map((id) => idMap.get(id));
9174
+ const duplicates = ids.map((id) => idMap.get(id));
9175
+ this.setSelectedShapes(duplicates);
9176
+ return duplicates;
9134
9177
  }
9135
9178
  /** Serializable content for the clipboard: shapes (with descendants) and bindings between them. */
9136
9179
  getContentFromCurrentPage(ids) {
@@ -10140,6 +10183,7 @@ var Editor = class extends EventEmitter {
10140
10183
  }
10141
10184
  for (const rec of Object.values(changes.removed)) {
10142
10185
  if (rec.typeName !== "shape") continue;
10186
+ this.overlayFlaggedIds.delete(rec.id);
10143
10187
  const h = this.handles.release(rec.id);
10144
10188
  if (h !== void 0) {
10145
10189
  this.engine.cmd.remove(h);
@@ -10156,6 +10200,7 @@ var Editor = class extends EventEmitter {
10156
10200
  if (next.typeName !== "shape" || prev.typeName !== "shape") continue;
10157
10201
  const onPage = this.getAncestorPageId(next) === pageId;
10158
10202
  if (!onPage) {
10203
+ this.overlayFlaggedIds.delete(next.id);
10159
10204
  const h = this.handles.release(next.id);
10160
10205
  if (h !== void 0) {
10161
10206
  this.engine.cmd.remove(h);
@@ -10193,6 +10238,7 @@ var Editor = class extends EventEmitter {
10193
10238
  syncPage(pageId) {
10194
10239
  this.syncedPageId = pageId;
10195
10240
  this.handles.clear();
10241
+ this.overlayFlaggedIds.clear();
10196
10242
  this.engine.cmd.clear();
10197
10243
  for (const shape of this.getCurrentPageShapesSorted()) {
10198
10244
  this.writeShapeToEngine(shape, true);
@@ -10242,6 +10288,8 @@ var Editor = class extends EventEmitter {
10242
10288
  } else {
10243
10289
  flags |= FLAG.OVERLAY;
10244
10290
  }
10291
+ if (flags & (FLAG.OVERLAY | FLAG.LABEL)) this.overlayFlaggedIds.add(shape.id);
10292
+ else this.overlayFlaggedIds.delete(shape.id);
10245
10293
  const bw = engineGeometry?.w ?? geometry?.bounds.w ?? 0;
10246
10294
  const bh = engineGeometry?.h ?? geometry?.bounds.h ?? 0;
10247
10295
  this.engine.cmd.upsert(h, this.kindId(shape.type), parent, zlo, zhi, flags, shape.x, shape.y, shape.rotation, bw, bh);
@@ -10301,6 +10349,7 @@ var Editor = class extends EventEmitter {
10301
10349
  clips.push(o.clip);
10302
10350
  }
10303
10351
  }
10352
+ unsafe__withoutCapture(() => this.keepUncullableOverlay(ids, clips));
10304
10353
  unsafe__withoutCapture(() => {
10305
10354
  const prev = this._overlayShapeIds.get();
10306
10355
  if (prev.length !== ids.length || prev.some((id, i) => id !== ids[i])) this._overlayShapeIds.set(ids);
@@ -10312,6 +10361,28 @@ var Editor = class extends EventEmitter {
10312
10361
  });
10313
10362
  return frame;
10314
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
+ }
10315
10386
  // ---- textures ----------------------------------------------------------
10316
10387
  /**
10317
10388
  * Device-pixel scale at which rasterized textures (text labels) should be
@@ -14868,9 +14939,11 @@ function useCanvasEvents(editor) {
14868
14939
  };
14869
14940
  const isHandled = (e) => editor.isEventHandled(e);
14870
14941
  const isInFrontOfCanvas = (target) => target instanceof Element && target.closest(IN_FRONT_OF_CANVAS_SELECTOR) !== null;
14942
+ const isElsewhere = (e) => e.currentTarget instanceof Node && e.target instanceof Node && !e.currentTarget.contains(e.target);
14943
+ const ownPointers = /* @__PURE__ */ new Set();
14871
14944
  return {
14872
14945
  onPointerDown(e) {
14873
- if (isHandled(e) || isInFrontOfCanvas(e.target)) return;
14946
+ if (isHandled(e) || isInFrontOfCanvas(e.target) || isElsewhere(e)) return;
14874
14947
  if (e.button === 2) {
14875
14948
  editor.dispatch(pointerInfo(e, "right_click"));
14876
14949
  return;
@@ -14878,6 +14951,7 @@ function useCanvasEvents(editor) {
14878
14951
  if (e.button === 1) {
14879
14952
  editor.dispatch(pointerInfo(e, "middle_click"));
14880
14953
  }
14954
+ ownPointers.add(e.pointerId);
14881
14955
  e.currentTarget.setPointerCapture(e.pointerId);
14882
14956
  editor.getContainer().focus({ preventScroll: true });
14883
14957
  const down = pointerInfo(e, "pointer_down");
@@ -14885,16 +14959,18 @@ function useCanvasEvents(editor) {
14885
14959
  editor.dispatch(down);
14886
14960
  },
14887
14961
  onPointerMove(e) {
14888
- if (isHandled(e) || isInFrontOfCanvas(e.target)) return;
14962
+ if (isHandled(e) || isInFrontOfCanvas(e.target) || isElsewhere(e)) return;
14889
14963
  if (e.pointerType === "mouse" && e.buttons === 0 && editor.inputs.isPointing) {
14964
+ ownPointers.delete(e.pointerId);
14890
14965
  editor.dispatch(pointerInfo(e, "pointer_up"));
14891
14966
  return;
14892
14967
  }
14893
14968
  editor.dispatch(pointerInfo(e, "pointer_move"));
14894
14969
  },
14895
14970
  onPointerUp(e) {
14896
- if (isHandled(e) || isInFrontOfCanvas(e.target)) return;
14971
+ if (isHandled(e) || isInFrontOfCanvas(e.target) || isElsewhere(e)) return;
14897
14972
  if (e.button === 2) return;
14973
+ if (!ownPointers.delete(e.pointerId)) return;
14898
14974
  const el = e.currentTarget;
14899
14975
  if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
14900
14976
  const up = pointerInfo(e, "pointer_up");
@@ -14902,12 +14978,13 @@ function useCanvasEvents(editor) {
14902
14978
  editor.click.handlePointerEvent(up);
14903
14979
  },
14904
14980
  onPointerCancel(e) {
14905
- if (isHandled(e) || isInFrontOfCanvas(e.target)) return;
14981
+ if (isHandled(e) || isInFrontOfCanvas(e.target) || isElsewhere(e)) return;
14982
+ if (!ownPointers.delete(e.pointerId)) return;
14906
14983
  editor.dispatch(pointerInfo(e, "pointer_up"));
14907
14984
  editor.cancel();
14908
14985
  },
14909
14986
  onWheel(e) {
14910
- if (isHandled(e) || isInFrontOfCanvas(e.target)) return;
14987
+ if (isHandled(e) || isInFrontOfCanvas(e.target) || isElsewhere(e)) return;
14911
14988
  e.preventDefault();
14912
14989
  const point = localPoint(editor, e);
14913
14990
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1;
@@ -14921,7 +14998,7 @@ function useCanvasEvents(editor) {
14921
14998
  editor.dispatch(info);
14922
14999
  },
14923
15000
  onContextMenu(e) {
14924
- if (isInFrontOfCanvas(e.target)) return;
15001
+ if (isInFrontOfCanvas(e.target) || isElsewhere(e)) return;
14925
15002
  e.preventDefault();
14926
15003
  },
14927
15004
  onKeyDown(e) {