@ngroznykh/papirus 0.9.11 → 0.9.13

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/dist/papirus.js CHANGED
@@ -1689,13 +1689,11 @@ var NavigationManager = class extends EventEmitter {
1689
1689
  x: event.screenX,
1690
1690
  y: event.screenY
1691
1691
  };
1692
- this.renderer.beginLowResPan();
1693
1692
  this.emit("panStart");
1694
1693
  }
1695
1694
  endPan() {
1696
1695
  this.isPanning = false;
1697
1696
  this.panStart = null;
1698
- this.renderer.endLowResPan();
1699
1697
  this.emit("panEnd");
1700
1698
  }
1701
1699
  };
@@ -3222,6 +3220,20 @@ function measureTextLines(ctx, lines) {
3222
3220
  for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
3223
3221
  return maxWidth;
3224
3222
  }
3223
+ function readScale(ctx) {
3224
+ if (typeof ctx.getTransform !== "function") return null;
3225
+ const m = ctx.getTransform();
3226
+ if (m === null || typeof m.a !== "number") return null;
3227
+ return {
3228
+ a: m.a,
3229
+ b: typeof m.b === "number" ? m.b : 0
3230
+ };
3231
+ }
3232
+ function isScreenFontReadable(ctx, fontSize) {
3233
+ const m = readScale(ctx);
3234
+ if (m === null) return true;
3235
+ return fontSize * Math.hypot(m.a, m.b) >= 7;
3236
+ }
3225
3237
  //#endregion
3226
3238
  //#region src/elements/TextLabel.ts
3227
3239
  var DEFAULT_INSET = 8;
@@ -3479,10 +3491,10 @@ var TextLabel = class {
3479
3491
  default: startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
3480
3492
  }
3481
3493
  ctx.fillStyle = this._style.color ?? "#000000";
3494
+ if (!isScreenFontReadable(ctx, this._style.fontSize ?? 14)) return;
3482
3495
  for (let i = 0; i < this._lines.length; i++) {
3483
- const line = this._lines[i];
3484
3496
  const y = startY + i * lineHeight;
3485
- ctx.fillText(line, x, y);
3497
+ ctx.fillText(this._lines[i], x, y);
3486
3498
  }
3487
3499
  }
3488
3500
  /**
@@ -6413,6 +6425,7 @@ function drawLegacyArrowHead(ctx, from, to) {
6413
6425
  function renderEdgeLabel(ctx, options) {
6414
6426
  const { label, path, background } = options;
6415
6427
  if (!label || path.length < 2) return;
6428
+ if (!isScreenFontReadable(ctx, label.style.fontSize ?? 14)) return;
6416
6429
  const labelCenter = getEdgeLabelPosition(options);
6417
6430
  if (!labelCenter) return;
6418
6431
  const rotation = getEdgeLabelRotation(options);
@@ -7062,7 +7075,7 @@ var Edge = class extends Element {
7062
7075
  }
7063
7076
  return pts;
7064
7077
  };
7065
- if (!(this._label !== void 0 && this._labelLineGap)) {
7078
+ if (!(this._label !== void 0 && this._labelLineGap && isScreenFontReadable(ctx, this._label.style.fontSize ?? 14))) {
7066
7079
  const start = path[0];
7067
7080
  const startNext = path[1];
7068
7081
  const startAngle = Math.atan2(startNext.y - start.y, startNext.x - start.x);
@@ -7549,6 +7562,7 @@ var InteractionManager = class {
7549
7562
  this.handledScrollbarMouseDown = false;
7550
7563
  this.overlayDragSession = null;
7551
7564
  this.handledOverlayMouseDown = false;
7565
+ this.editHoverSuspendedForPan = false;
7552
7566
  this.renderer = options.renderer;
7553
7567
  this.navigationOnly = options.navigationOnly ?? false;
7554
7568
  this.inputHandler = new InputHandler({
@@ -7732,7 +7746,7 @@ var InteractionManager = class {
7732
7746
  setupEvents() {
7733
7747
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
7734
7748
  this.selectionManager.renderSelectionRect(ctx);
7735
- if (!this.navigationOnly) {
7749
+ if (!this.navigationOnly && !this.navigationManager.panning) {
7736
7750
  this.dragManager.renderAlignmentGuides(ctx);
7737
7751
  this.connectionManager.renderPreview(ctx);
7738
7752
  this.connectionManager.renderHoverAnchors(ctx);
@@ -7903,7 +7917,7 @@ var InteractionManager = class {
7903
7917
  if (event.button === 0) {
7904
7918
  if (this.navigationOnly) {
7905
7919
  this.selectionManager.clearSelection();
7906
- this.navigationManager.startPan(event);
7920
+ this.beginBackgroundPan(event);
7907
7921
  return;
7908
7922
  }
7909
7923
  if (hitElement === void 0) {
@@ -7912,11 +7926,14 @@ var InteractionManager = class {
7912
7926
  return;
7913
7927
  }
7914
7928
  this.selectionManager.clearSelection();
7915
- this.navigationManager.startPan(event);
7929
+ this.beginBackgroundPan(event);
7916
7930
  return;
7917
7931
  }
7918
7932
  }
7919
- if (this.navigationManager.handleMouseDown(event)) return;
7933
+ if (this.navigationManager.handleMouseDown(event)) {
7934
+ this.suspendEditHover();
7935
+ return;
7936
+ }
7920
7937
  if (this.navigationOnly) return;
7921
7938
  if (this.dragManager.handleMouseDown(event)) return;
7922
7939
  }
@@ -7930,7 +7947,11 @@ var InteractionManager = class {
7930
7947
  this.renderer.setScrollbarActiveAxis(null);
7931
7948
  this.scrollbarDragState = null;
7932
7949
  }
7933
- if (this.navigationManager.panning) this.navigationManager.handleMouseUp(event);
7950
+ if (this.navigationManager.panning) this.finishBackgroundPan(event);
7951
+ }
7952
+ if (this.navigationManager.panning) {
7953
+ this.navigationManager.handleMouseMove(event);
7954
+ return;
7934
7955
  }
7935
7956
  const overScrollbar = this.renderer.updateScrollbarHover(event.screenX, event.screenY);
7936
7957
  const overOverlayPointerSink = this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY);
@@ -7992,7 +8013,7 @@ var InteractionManager = class {
7992
8013
  this.selectionManager.endSelectionRect();
7993
8014
  return;
7994
8015
  }
7995
- this.navigationManager.handleMouseUp(event);
8016
+ this.finishBackgroundPan(event);
7996
8017
  }
7997
8018
  handleClick(event) {
7998
8019
  if (this.handledOverlayMouseDown) {
@@ -8096,6 +8117,24 @@ var InteractionManager = class {
8096
8117
  }
8097
8118
  return closestEdge;
8098
8119
  }
8120
+ suspendEditHover() {
8121
+ if (this.navigationOnly || this.editHoverSuspendedForPan) return;
8122
+ this.connectionManager.disableHover();
8123
+ this.editHoverSuspendedForPan = true;
8124
+ }
8125
+ resumeEditHover() {
8126
+ if (!this.editHoverSuspendedForPan) return;
8127
+ this.connectionManager.enableHover();
8128
+ this.editHoverSuspendedForPan = false;
8129
+ }
8130
+ beginBackgroundPan(event) {
8131
+ this.suspendEditHover();
8132
+ this.navigationManager.startPan(event);
8133
+ }
8134
+ finishBackgroundPan(event) {
8135
+ this.navigationManager.handleMouseUp(event);
8136
+ this.resumeEditHover();
8137
+ }
8099
8138
  handleWheel(event) {
8100
8139
  if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
8101
8140
  this.navigationManager.handleWheel(event);
@@ -8134,6 +8173,7 @@ var InteractionManager = class {
8134
8173
  }
8135
8174
  handleKeyUp(event) {
8136
8175
  this.navigationManager.handleKeyUp(event);
8176
+ if (!this.navigationManager.panning) this.resumeEditHover();
8137
8177
  }
8138
8178
  handleViewportNavigationKey(event) {
8139
8179
  switch (event.key) {
@@ -8318,6 +8358,66 @@ var InteractionManager = class {
8318
8358
  }
8319
8359
  };
8320
8360
  //#endregion
8361
+ //#region src/utils/pathViewport.ts
8362
+ /**
8363
+ * True when the drawn edge path meets the viewport.
8364
+ * AABB-only cull keeps fat bezier control boxes on the walk even when the
8365
+ * curve never enters the view. Empty path stays visible for the first layout.
8366
+ */
8367
+ function edgePathIntersectsViewport(input, viewport) {
8368
+ const path = input.path;
8369
+ if (path.length === 0) return true;
8370
+ const aabb = boundsFromPoints(path);
8371
+ if (aabb === null || !rectsIntersect(aabb, viewport)) return false;
8372
+ if (input.type === "bezier" && path.length >= 4) return polylineIntersectsRect(sampleBezier(path), viewport);
8373
+ return polylineIntersectsRect(path, viewport);
8374
+ }
8375
+ function boundsFromPoints(points) {
8376
+ if (points.length === 0) return null;
8377
+ let minX = Infinity;
8378
+ let minY = Infinity;
8379
+ let maxX = -Infinity;
8380
+ let maxY = -Infinity;
8381
+ for (const point of points) {
8382
+ minX = Math.min(minX, point.x);
8383
+ minY = Math.min(minY, point.y);
8384
+ maxX = Math.max(maxX, point.x);
8385
+ maxY = Math.max(maxY, point.y);
8386
+ }
8387
+ return {
8388
+ x: minX,
8389
+ y: minY,
8390
+ width: maxX - minX,
8391
+ height: maxY - minY
8392
+ };
8393
+ }
8394
+ function polylineIntersectsRect(points, rect) {
8395
+ if (points.length === 1) return pointInRect(points[0], rect);
8396
+ for (let i = 0; i < points.length - 1; i++) {
8397
+ const start = points[i];
8398
+ const end = points[i + 1];
8399
+ if (pointInRect(start, rect) || pointInRect(end, rect)) return true;
8400
+ if (segmentRectIntersections(start, end, rect).length > 0) return true;
8401
+ }
8402
+ return false;
8403
+ }
8404
+ function sampleBezier(path) {
8405
+ const samples = [];
8406
+ const steps = 20;
8407
+ for (let i = 1; i + 2 < path.length; i += 3) {
8408
+ const p0 = path[i - 1];
8409
+ const p1 = path[i];
8410
+ const p2 = path[i + 1];
8411
+ const p3 = path[i + 2];
8412
+ for (let s = 0; s <= steps; s++) {
8413
+ const t = s / steps;
8414
+ if (samples.length > 0 && t === 0) continue;
8415
+ samples.push(bezierPoint(p0, p1, p2, p3, t));
8416
+ }
8417
+ }
8418
+ return samples;
8419
+ }
8420
+ //#endregion
8321
8421
  //#region src/core/AnimationManager.ts
8322
8422
  var DEFAULT_OPTIONS$3 = {
8323
8423
  enabled: false,
@@ -9296,9 +9396,10 @@ var DiagramRenderer = class extends EventEmitter {
9296
9396
  this._dirty = true;
9297
9397
  this._styleDirty = true;
9298
9398
  this._contentDirty = true;
9399
+ this._contentRevision = 0;
9400
+ this._contentBoundsCache = null;
9299
9401
  this._destroyed = false;
9300
9402
  this._contextLost = false;
9301
- this._lowResPan = false;
9302
9403
  this._canvasRect = null;
9303
9404
  this._nodeObstaclesCache = null;
9304
9405
  this.animationFrameId = null;
@@ -9463,26 +9564,6 @@ var DiagramRenderer = class extends EventEmitter {
9463
9564
  return this.devicePixelRatio;
9464
9565
  }
9465
9566
  /**
9466
- * Shrink a retina backing store to 1x for the duration of a pan gesture.
9467
- * Does not touch `canvas.width` while the 2D context is lost.
9468
- */
9469
- beginLowResPan() {
9470
- if (this._lowResPan || this._contextLost) return;
9471
- if (this.nativePixelRatio() <= 1) return;
9472
- this._lowResPan = true;
9473
- this.setupCanvas();
9474
- this.markDirty();
9475
- }
9476
- /**
9477
- * Restore the native backing-store ratio after a pan gesture.
9478
- */
9479
- endLowResPan() {
9480
- if (!this._lowResPan) return;
9481
- this._lowResPan = false;
9482
- if (!this._contextLost) this.setupCanvas();
9483
- this.markDirty();
9484
- }
9485
- /**
9486
9567
  * Access the underlying canvas element
9487
9568
  */
9488
9569
  getCanvas() {
@@ -9753,7 +9834,7 @@ var DiagramRenderer = class extends EventEmitter {
9753
9834
  addGroup(group) {
9754
9835
  group.setDirtyListener(() => this.markDirty());
9755
9836
  this._groups.set(group.id, group);
9756
- this.markDirty();
9837
+ this.markContentDirty();
9757
9838
  }
9758
9839
  /**
9759
9840
  * Remove a group from the diagram
@@ -9763,7 +9844,7 @@ var DiagramRenderer = class extends EventEmitter {
9763
9844
  if (group === void 0) return false;
9764
9845
  this._groups.delete(groupId);
9765
9846
  group.setDirtyListener(void 0);
9766
- this.markDirty();
9847
+ this.markContentDirty();
9767
9848
  return true;
9768
9849
  }
9769
9850
  /**
@@ -9837,10 +9918,18 @@ var DiagramRenderer = class extends EventEmitter {
9837
9918
  */
9838
9919
  markContentDirty() {
9839
9920
  this._contentDirty = true;
9921
+ this._contentRevision += 1;
9840
9922
  this._nodeObstaclesCache = null;
9841
9923
  this._dirty = true;
9842
9924
  }
9843
9925
  /**
9926
+ * Monotonic counter bumped when nodes/edges/layout change (not on pan/zoom).
9927
+ * Used by MiniMap to cache its content bake across viewport moves.
9928
+ */
9929
+ getContentRevision() {
9930
+ return this._contentRevision;
9931
+ }
9932
+ /**
9844
9933
  * Visible world rectangle plus padding so labels and arrow markers are not culled early.
9845
9934
  */
9846
9935
  getViewportWorldBounds() {
@@ -9858,10 +9947,13 @@ var DiagramRenderer = class extends EventEmitter {
9858
9947
  }
9859
9948
  /**
9860
9949
  * Edges without a computed path stay drawable so the first layout pass can paint them.
9950
+ * Bezier control-box AABB is not enough — walk the actual path.
9861
9951
  */
9862
9952
  edgeIntersectsViewport(edge, viewport) {
9863
- if (edge.path.length === 0) return true;
9864
- return this.intersectsViewport(edge.getBounds(), viewport);
9953
+ return edgePathIntersectsViewport({
9954
+ path: edge.path,
9955
+ type: edge.type
9956
+ }, viewport);
9865
9957
  }
9866
9958
  /**
9867
9959
  * Force an immediate render
@@ -9910,8 +10002,7 @@ var DiagramRenderer = class extends EventEmitter {
9910
10002
  return this.options.retina ? window.devicePixelRatio || 1 : 1;
9911
10003
  }
9912
10004
  resolvePixelRatio() {
9913
- const native = this.nativePixelRatio();
9914
- return this._lowResPan ? 1 : native;
10005
+ return this.nativePixelRatio();
9915
10006
  }
9916
10007
  setupCanvas() {
9917
10008
  const { width, height } = this.options;
@@ -9962,13 +10053,13 @@ var DiagramRenderer = class extends EventEmitter {
9962
10053
  applyStyleManagerToElements(this.styleManager, this._groups.values(), this._edges.values(), this._nodes.values());
9963
10054
  this._styleDirty = false;
9964
10055
  }
9965
- for (const group of this._groups.values()) group.recalculateBounds();
9966
10056
  const viewport = this.getViewportWorldBounds();
9967
10057
  for (const group of this._groups.values()) if (group.visible && this.intersectsViewport(group.getBounds(), viewport)) {
9968
10058
  this.renderElementWithAnimation(ctx, group, () => group.render(ctx));
9969
10059
  group.clearDirty();
9970
10060
  }
9971
10061
  if (this._contentDirty) {
10062
+ for (const group of this._groups.values()) group.recalculateBounds();
9972
10063
  this.edgeEndpointUpdater.updateAll();
9973
10064
  this._contentDirty = false;
9974
10065
  }
@@ -10031,11 +10122,17 @@ var DiagramRenderer = class extends EventEmitter {
10031
10122
  return this._nodeObstaclesCache;
10032
10123
  }
10033
10124
  getContentBounds() {
10034
- return getContentBounds({
10125
+ if (this._contentBoundsCache?.revision === this._contentRevision) return this._contentBoundsCache.bounds;
10126
+ const bounds = getContentBounds({
10035
10127
  nodes: this._nodes.values(),
10036
10128
  edges: this._edges.values(),
10037
10129
  groups: this._groups.values()
10038
10130
  });
10131
+ this._contentBoundsCache = {
10132
+ revision: this._contentRevision,
10133
+ bounds
10134
+ };
10135
+ return bounds;
10039
10136
  }
10040
10137
  notifyScrollbarInteraction() {
10041
10138
  this.scrollbarController.notifyInteraction();
@@ -11971,6 +12068,7 @@ var Group = class Group extends Element {
11971
12068
  */
11972
12069
  recalculateBounds() {
11973
12070
  if (this._children.length === 0) {
12071
+ if (this._x === 0 && this._y === 0 && this._width === 0 && this._height === 0) return;
11974
12072
  this._x = 0;
11975
12073
  this._y = 0;
11976
12074
  this._width = 0;
@@ -11989,10 +12087,15 @@ var Group = class Group extends Element {
11989
12087
  maxX = Math.max(maxX, bounds.x + bounds.width);
11990
12088
  maxY = Math.max(maxY, bounds.y + bounds.height);
11991
12089
  }
11992
- this._x = minX - this._padding;
11993
- this._y = minY - this._padding;
11994
- this._width = maxX - minX + this._padding * 2;
11995
- this._height = maxY - minY + this._padding * 2;
12090
+ const nextX = minX - this._padding;
12091
+ const nextY = minY - this._padding;
12092
+ const nextWidth = maxX - minX + this._padding * 2;
12093
+ const nextHeight = maxY - minY + this._padding * 2;
12094
+ if (this._x === nextX && this._y === nextY && this._width === nextWidth && this._height === nextHeight) return;
12095
+ this._x = nextX;
12096
+ this._y = nextY;
12097
+ this._width = nextWidth;
12098
+ this._height = nextHeight;
11996
12099
  this.markDirty();
11997
12100
  }
11998
12101
  hitTest(point) {
@@ -12880,6 +12983,7 @@ var CText = class extends CompositeComponentBase {
12880
12983
  if (this.style.visible === false) return;
12881
12984
  const opacity = this.style.opacity ?? 1;
12882
12985
  if (opacity <= 0) return;
12986
+ if (!isScreenFontReadable(ctx, this._fontSize)) return;
12883
12987
  ctx.save();
12884
12988
  ctx.globalAlpha *= opacity;
12885
12989
  ctx.font = this.getFont();
@@ -12932,8 +13036,14 @@ var CText = class extends CompositeComponentBase {
12932
13036
  ctx.rotate(this._rotation * Math.PI / 180);
12933
13037
  const offsetX = xBase - (textBounds.x + textBounds.width / 2);
12934
13038
  const offsetY = yStart - (textBounds.y + textBounds.height / 2);
12935
- for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], offsetX, offsetY + i * lineHeightPx);
12936
- } else for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], xBase, yStart + i * lineHeightPx);
13039
+ for (let i = 0; i < lines.length; i++) {
13040
+ const y = offsetY + i * lineHeightPx;
13041
+ ctx.fillText(lines[i], offsetX, y);
13042
+ }
13043
+ } else for (let i = 0; i < lines.length; i++) {
13044
+ const y = yStart + i * lineHeightPx;
13045
+ ctx.fillText(lines[i], xBase, y);
13046
+ }
12937
13047
  ctx.restore();
12938
13048
  }
12939
13049
  serialize() {
@@ -15570,6 +15680,24 @@ function distributeNodes(nodes, type) {
15570
15680
  }
15571
15681
  }
15572
15682
  }
15683
+ /**
15684
+ * World step that keeps the visible grid cell in a Miro-like range.
15685
+ * The base `gridSize` is doubled or halved so the screen size stays ~16–32 px.
15686
+ */
15687
+ function adaptiveGridStep(gridSize, zoom) {
15688
+ if (!Number.isFinite(gridSize) || !Number.isFinite(zoom) || gridSize <= 0 || zoom <= 0) return gridSize;
15689
+ let step = gridSize;
15690
+ let screen = step * zoom;
15691
+ while (screen < 16) {
15692
+ step *= 2;
15693
+ screen *= 2;
15694
+ }
15695
+ while (screen > 32) {
15696
+ step /= 2;
15697
+ screen /= 2;
15698
+ }
15699
+ return step;
15700
+ }
15573
15701
  //#endregion
15574
15702
  //#region src/core/overlays/BaseOverlay.ts
15575
15703
  /**
@@ -15608,20 +15736,20 @@ var GridOverlay = class extends BaseOverlay {
15608
15736
  if (!this.enabled) return;
15609
15737
  const width = renderer.width;
15610
15738
  const height = renderer.height;
15611
- const gridSize = this.gridSize;
15612
- const color = this.color;
15613
- const startX = Math.floor(-renderer.offsetX / renderer.zoom / gridSize) * gridSize;
15614
- const startY = Math.floor(-renderer.offsetY / renderer.zoom / gridSize) * gridSize;
15615
- const endX = Math.ceil((width - renderer.offsetX) / renderer.zoom / gridSize) * gridSize;
15616
- const endY = Math.ceil((height - renderer.offsetY) / renderer.zoom / gridSize) * gridSize;
15617
- ctx.strokeStyle = color;
15618
- ctx.lineWidth = 1 / renderer.zoom;
15739
+ const zoom = renderer.zoom;
15740
+ const step = adaptiveGridStep(this.gridSize, zoom);
15741
+ const startX = Math.floor(-renderer.offsetX / zoom / step) * step;
15742
+ const startY = Math.floor(-renderer.offsetY / zoom / step) * step;
15743
+ const endX = Math.ceil((width - renderer.offsetX) / zoom / step) * step;
15744
+ const endY = Math.ceil((height - renderer.offsetY) / zoom / step) * step;
15745
+ ctx.strokeStyle = this.color;
15746
+ ctx.lineWidth = 1 / zoom;
15619
15747
  ctx.beginPath();
15620
- for (let x = startX; x <= endX; x += gridSize) {
15748
+ for (let x = startX; x <= endX; x += step) {
15621
15749
  ctx.moveTo(x, startY);
15622
15750
  ctx.lineTo(x, endY);
15623
15751
  }
15624
- for (let y = startY; y <= endY; y += gridSize) {
15752
+ for (let y = startY; y <= endY; y += step) {
15625
15753
  ctx.moveTo(startX, y);
15626
15754
  ctx.lineTo(endX, y);
15627
15755
  }
@@ -15634,51 +15762,114 @@ var GridOverlay = class extends BaseOverlay {
15634
15762
  var RulersOverlay = class extends BaseOverlay {
15635
15763
  constructor(options = {}) {
15636
15764
  super(options.enabled ?? true);
15765
+ this.tickBake = null;
15766
+ this.tickBakeGeneration = 0;
15637
15767
  this.thickness = options.thickness ?? 20;
15638
15768
  this.backgroundColor = options.backgroundColor ?? "#f4f4f5";
15639
15769
  this.textColor = options.textColor ?? "#52525b";
15640
15770
  this.tickColor = options.tickColor ?? "#a1a1aa";
15641
15771
  }
15772
+ getTickBakeGeneration() {
15773
+ return this.tickBakeGeneration;
15774
+ }
15642
15775
  install(renderer) {
15643
15776
  this.removeOverlay = renderer.addOverlayRenderer((ctx) => {
15644
15777
  if (!this.enabled) return;
15645
15778
  const thickness = this.thickness;
15779
+ const zoom = renderer.zoom;
15780
+ const worldLeft = -renderer.offsetX / zoom;
15781
+ const worldTop = -renderer.offsetY / zoom;
15646
15782
  ctx.save();
15647
15783
  ctx.setTransform(renderer.pixelRatio, 0, 0, renderer.pixelRatio, 0, 0);
15648
15784
  ctx.fillStyle = this.backgroundColor;
15649
15785
  ctx.fillRect(0, 0, renderer.width, thickness);
15650
15786
  ctx.fillRect(0, 0, thickness, renderer.height);
15651
- ctx.strokeStyle = this.tickColor;
15652
- ctx.fillStyle = this.textColor;
15653
- ctx.font = "10px sans-serif";
15654
- ctx.textAlign = "left";
15655
- ctx.textBaseline = "top";
15656
- const step = this.getTickStep(renderer.zoom);
15657
- const worldLeft = -renderer.offsetX / renderer.zoom;
15658
- const worldTop = -renderer.offsetY / renderer.zoom;
15659
- const worldRight = worldLeft + renderer.width / renderer.zoom;
15660
- const worldBottom = worldTop + renderer.height / renderer.zoom;
15661
- ctx.beginPath();
15662
- for (let x = Math.floor(worldLeft / step) * step; x <= worldRight; x += step) {
15663
- const screenX = x * renderer.zoom + renderer.offsetX;
15664
- ctx.moveTo(screenX, thickness);
15665
- ctx.lineTo(screenX, thickness - 6);
15666
- ctx.fillText(`${Math.round(x)}`, screenX + 2, 2);
15667
- }
15668
- for (let y = Math.floor(worldTop / step) * step; y <= worldBottom; y += step) {
15669
- const screenY = y * renderer.zoom + renderer.offsetY;
15670
- ctx.moveTo(thickness, screenY);
15671
- ctx.lineTo(thickness - 6, screenY);
15672
- ctx.save();
15673
- ctx.translate(2, screenY + 2);
15674
- ctx.rotate(-Math.PI / 2);
15675
- ctx.fillText(`${Math.round(y)}`, 0, 0);
15676
- ctx.restore();
15677
- }
15678
- ctx.stroke();
15787
+ const bake = this.ensureTickBake(renderer);
15788
+ const srcX = (worldLeft - bake.worldLeft) * zoom;
15789
+ const srcY = (worldTop - bake.worldTop) * zoom;
15790
+ const pr = renderer.pixelRatio;
15791
+ ctx.drawImage(bake.horizontal, srcX * pr, 0, renderer.width * pr, thickness * pr, 0, 0, renderer.width, thickness);
15792
+ ctx.drawImage(bake.vertical, 0, srcY * pr, thickness * pr, renderer.height * pr, 0, 0, thickness, renderer.height);
15679
15793
  ctx.restore();
15680
15794
  });
15681
15795
  }
15796
+ ensureTickBake(renderer) {
15797
+ const zoom = renderer.zoom;
15798
+ const worldLeft = -renderer.offsetX / zoom;
15799
+ const worldTop = -renderer.offsetY / zoom;
15800
+ const worldRight = worldLeft + renderer.width / zoom;
15801
+ const worldBottom = worldTop + renderer.height / zoom;
15802
+ if (this.tickBake !== null && this.tickBake.zoom === zoom && this.tickBake.pixelRatio === renderer.pixelRatio && worldLeft >= this.tickBake.worldLeft && worldRight <= this.tickBake.worldRight && worldTop >= this.tickBake.worldTop && worldBottom <= this.tickBake.worldBottom) return this.tickBake;
15803
+ const extraX = renderer.width / zoom;
15804
+ const extraY = renderer.height / zoom;
15805
+ const bakeLeft = worldLeft - extraX;
15806
+ const bakeTop = worldTop - extraY;
15807
+ const bakeRight = worldRight + extraX;
15808
+ const bakeBottom = worldBottom + extraY;
15809
+ const pr = Math.max(1, renderer.pixelRatio);
15810
+ const thickness = this.thickness;
15811
+ const step = this.getTickStep(zoom);
15812
+ const horizontal = this.tickBake?.horizontal ?? document.createElement("canvas");
15813
+ const vertical = this.tickBake?.vertical ?? document.createElement("canvas");
15814
+ const hWidth = Math.max(1, Math.ceil((bakeRight - bakeLeft) * zoom));
15815
+ const vHeight = Math.max(1, Math.ceil((bakeBottom - bakeTop) * zoom));
15816
+ horizontal.width = Math.max(1, Math.ceil(hWidth * pr));
15817
+ horizontal.height = Math.max(1, Math.ceil(thickness * pr));
15818
+ vertical.width = Math.max(1, Math.ceil(thickness * pr));
15819
+ vertical.height = Math.max(1, Math.ceil(vHeight * pr));
15820
+ const hCtx = horizontal.getContext("2d");
15821
+ const vCtx = vertical.getContext("2d");
15822
+ if (hCtx !== null) {
15823
+ hCtx.setTransform(pr, 0, 0, pr, 0, 0);
15824
+ hCtx.clearRect(0, 0, hWidth, thickness);
15825
+ hCtx.strokeStyle = this.tickColor;
15826
+ hCtx.fillStyle = this.textColor;
15827
+ hCtx.font = "10px sans-serif";
15828
+ hCtx.textAlign = "left";
15829
+ hCtx.textBaseline = "top";
15830
+ hCtx.beginPath();
15831
+ for (let x = Math.floor(bakeLeft / step) * step; x <= bakeRight; x += step) {
15832
+ const screenX = (x - bakeLeft) * zoom;
15833
+ hCtx.moveTo(screenX, thickness);
15834
+ hCtx.lineTo(screenX, thickness - 6);
15835
+ hCtx.fillText(`${Math.round(x)}`, screenX + 2, 2);
15836
+ }
15837
+ hCtx.stroke();
15838
+ }
15839
+ if (vCtx !== null) {
15840
+ vCtx.setTransform(pr, 0, 0, pr, 0, 0);
15841
+ vCtx.clearRect(0, 0, thickness, vHeight);
15842
+ vCtx.strokeStyle = this.tickColor;
15843
+ vCtx.fillStyle = this.textColor;
15844
+ vCtx.font = "10px sans-serif";
15845
+ vCtx.textAlign = "left";
15846
+ vCtx.textBaseline = "top";
15847
+ vCtx.beginPath();
15848
+ for (let y = Math.floor(bakeTop / step) * step; y <= bakeBottom; y += step) {
15849
+ const screenY = (y - bakeTop) * zoom;
15850
+ vCtx.moveTo(thickness, screenY);
15851
+ vCtx.lineTo(thickness - 6, screenY);
15852
+ vCtx.save();
15853
+ vCtx.translate(2, screenY + 2);
15854
+ vCtx.rotate(-Math.PI / 2);
15855
+ vCtx.fillText(`${Math.round(y)}`, 0, 0);
15856
+ vCtx.restore();
15857
+ }
15858
+ vCtx.stroke();
15859
+ }
15860
+ this.tickBakeGeneration += 1;
15861
+ this.tickBake = {
15862
+ zoom,
15863
+ pixelRatio: renderer.pixelRatio,
15864
+ worldLeft: bakeLeft,
15865
+ worldTop: bakeTop,
15866
+ worldRight: bakeRight,
15867
+ worldBottom: bakeBottom,
15868
+ horizontal,
15869
+ vertical
15870
+ };
15871
+ return this.tickBake;
15872
+ }
15682
15873
  getTickStep(zoom) {
15683
15874
  const rawStep = 80 / zoom;
15684
15875
  const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
@@ -15734,6 +15925,7 @@ var GuidesOverlay = class extends BaseOverlay {
15734
15925
  var MiniMap = class extends BaseOverlay {
15735
15926
  constructor(options = {}) {
15736
15927
  super(options.enabled ?? true);
15928
+ this.contentBake = null;
15737
15929
  this.options = {
15738
15930
  width: options.width ?? 180,
15739
15931
  height: options.height ?? 120,
@@ -15745,12 +15937,16 @@ var MiniMap = class extends BaseOverlay {
15745
15937
  anchor: options.anchor ?? "bottom-right"
15746
15938
  };
15747
15939
  }
15940
+ /** Cached bake projection; stable across pan if only the viewport moved. */
15941
+ getContentBakeProjectionKey() {
15942
+ return this.contentBake?.projectionKey ?? null;
15943
+ }
15748
15944
  install(renderer) {
15749
15945
  this.removeOverlay = renderer.addTopOverlayRenderer((ctx) => {
15750
15946
  if (!this.enabled) return;
15751
15947
  const layout = this.computeLayout(renderer);
15752
15948
  if (!layout) return;
15753
- const { x, y, width, height, bounds, scale, mapOffsetX, mapOffsetY, viewRect } = layout;
15949
+ const { x, y, width, height, viewRect } = layout;
15754
15950
  ctx.save();
15755
15951
  ctx.setTransform(renderer.pixelRatio, 0, 0, renderer.pixelRatio, 0, 0);
15756
15952
  ctx.setLineDash([]);
@@ -15760,42 +15956,61 @@ var MiniMap = class extends BaseOverlay {
15760
15956
  ctx.strokeStyle = this.options.borderColor;
15761
15957
  ctx.lineWidth = 1;
15762
15958
  ctx.strokeRect(x, y, width, height);
15763
- ctx.save();
15764
- ctx.translate(mapOffsetX, mapOffsetY);
15765
- ctx.scale(scale, scale);
15766
- ctx.translate(-bounds.x, -bounds.y);
15767
- ctx.setLineDash([]);
15768
- ctx.lineDashOffset = 0;
15769
- ctx.strokeStyle = "#94a3b8";
15770
- ctx.lineWidth = 1 / scale;
15771
- for (const edge of renderer.edges.values()) {
15772
- if (!edge.visible) continue;
15773
- const path = edge.path;
15774
- if (path.length < 2) continue;
15775
- ctx.beginPath();
15776
- ctx.moveTo(path[0].x, path[0].y);
15777
- for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y);
15778
- ctx.stroke();
15779
- }
15780
- for (const node of renderer.nodes.values()) {
15781
- if (!node.visible) continue;
15782
- const b = node.getBounds();
15783
- ctx.fillStyle = "#e2e8f0";
15784
- ctx.fillRect(b.x, b.y, b.width, b.height);
15959
+ const bake = this.ensureContentBake(renderer, layout);
15960
+ ctx.drawImage(bake, x, y, width, height);
15961
+ if (viewRect.width >= 0 && viewRect.height >= 0) {
15962
+ ctx.strokeStyle = this.options.viewportColor;
15963
+ ctx.lineWidth = 1.5;
15964
+ ctx.setLineDash([]);
15965
+ ctx.lineDashOffset = 0;
15966
+ ctx.strokeRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
15785
15967
  }
15786
15968
  ctx.restore();
15787
- if (viewRect.width < 0 || viewRect.height < 0) {
15788
- ctx.restore();
15789
- return;
15790
- }
15791
- ctx.strokeStyle = this.options.viewportColor;
15792
- ctx.lineWidth = 1.5;
15793
- ctx.setLineDash([]);
15794
- ctx.lineDashOffset = 0;
15795
- ctx.strokeRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
15796
- ctx.restore();
15797
15969
  });
15798
15970
  }
15971
+ ensureContentBake(renderer, layout) {
15972
+ const { x, y, width, height, bounds, scale, mapOffsetX, mapOffsetY } = layout;
15973
+ const revision = renderer.getContentRevision();
15974
+ const projectionKey = [
15975
+ width,
15976
+ height,
15977
+ Math.round(bounds.x),
15978
+ Math.round(bounds.y),
15979
+ Math.round(bounds.width),
15980
+ Math.round(bounds.height),
15981
+ scale.toFixed(5),
15982
+ Math.round(mapOffsetX),
15983
+ Math.round(mapOffsetY)
15984
+ ].join("|");
15985
+ if (this.contentBake !== null && this.contentBake.revision === revision && this.contentBake.projectionKey === projectionKey) return this.contentBake.canvas;
15986
+ const canvas = this.contentBake?.canvas ?? document.createElement("canvas");
15987
+ const pixelRatio = Math.max(1, Math.min(2, renderer.pixelRatio));
15988
+ canvas.width = Math.max(1, Math.ceil(width * pixelRatio));
15989
+ canvas.height = Math.max(1, Math.ceil(height * pixelRatio));
15990
+ const ctx = canvas.getContext("2d");
15991
+ if (ctx === null) return canvas;
15992
+ ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
15993
+ ctx.clearRect(0, 0, width, height);
15994
+ ctx.save();
15995
+ ctx.translate(mapOffsetX - x, mapOffsetY - y);
15996
+ ctx.scale(scale, scale);
15997
+ ctx.translate(-bounds.x, -bounds.y);
15998
+ ctx.setLineDash([]);
15999
+ ctx.lineDashOffset = 0;
16000
+ for (const node of renderer.nodes.values()) {
16001
+ if (!node.visible) continue;
16002
+ const b = node.getBounds();
16003
+ ctx.fillStyle = "#e2e8f0";
16004
+ ctx.fillRect(b.x, b.y, b.width, b.height);
16005
+ }
16006
+ ctx.restore();
16007
+ this.contentBake = {
16008
+ revision,
16009
+ projectionKey,
16010
+ canvas
16011
+ };
16012
+ return canvas;
16013
+ }
15799
16014
  beginOverlayDrag(renderer, screenX, screenY) {
15800
16015
  const hit = this.hitTestMiniMap(renderer, screenX, screenY);
15801
16016
  if (!hit) return null;
@@ -15857,12 +16072,11 @@ var MiniMap = class extends BaseOverlay {
15857
16072
  computeLayout(renderer) {
15858
16073
  const rawBounds = getContentBounds({
15859
16074
  nodes: renderer.nodes.values(),
15860
- edges: renderer.edges.values(),
15861
16075
  groups: renderer.groups.values()
15862
16076
  });
15863
16077
  if (!rawBounds) return null;
15864
16078
  const margin = this.options.contentMargin;
15865
- const expandedContentBounds = {
16079
+ const bounds = {
15866
16080
  x: rawBounds.x - margin,
15867
16081
  y: rawBounds.y - margin,
15868
16082
  width: rawBounds.width + 2 * margin,
@@ -15872,17 +16086,7 @@ var MiniMap = class extends BaseOverlay {
15872
16086
  const x = anchor === "bottom-left" || anchor === "top-left" ? padding : renderer.width - width - padding;
15873
16087
  const y = anchor === "top-left" || anchor === "top-right" ? padding : renderer.height - height - padding;
15874
16088
  const viewportBounds = this.getViewportBounds(renderer);
15875
- const unionMinX = Math.min(expandedContentBounds.x, viewportBounds.x);
15876
- const unionMinY = Math.min(expandedContentBounds.y, viewportBounds.y);
15877
- const unionMaxX = Math.max(expandedContentBounds.x + expandedContentBounds.width, viewportBounds.x + viewportBounds.width);
15878
- const unionMaxY = Math.max(expandedContentBounds.y + expandedContentBounds.height, viewportBounds.y + viewportBounds.height);
15879
- const bounds = {
15880
- x: unionMinX,
15881
- y: unionMinY,
15882
- width: unionMaxX - unionMinX,
15883
- height: unionMaxY - unionMinY
15884
- };
15885
- const scale = Math.min(width / bounds.width, height / bounds.height);
16089
+ const scale = Math.min(width / Math.max(bounds.width, 1), height / Math.max(bounds.height, 1));
15886
16090
  const mapOffsetX = x + (width - bounds.width * scale) / 2;
15887
16091
  const mapOffsetY = y + (height - bounds.height * scale) / 2;
15888
16092
  let viewX = mapOffsetX + (viewportBounds.x - bounds.x) * scale;