@ngroznykh/papirus 0.9.12 → 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/core/DiagramRenderer.d.ts +9 -0
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/InteractionManager.d.ts +5 -0
- package/dist/core/InteractionManager.d.ts.map +1 -1
- package/dist/core/overlays/GridOverlay.d.ts.map +1 -1
- package/dist/core/overlays/MiniMap.d.ts +8 -0
- package/dist/core/overlays/MiniMap.d.ts.map +1 -1
- package/dist/core/overlays/RulersOverlay.d.ts +4 -0
- package/dist/core/overlays/RulersOverlay.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/Group.d.ts.map +1 -1
- package/dist/elements/TextLabel.d.ts.map +1 -1
- package/dist/elements/composite/CText.d.ts.map +1 -1
- package/dist/elements/edge/EdgeLabelRenderer.d.ts.map +1 -1
- package/dist/papirus.js +333 -105
- package/dist/papirus.js.map +1 -1
- package/dist/utils/adaptiveGridStep.d.ts +10 -0
- package/dist/utils/adaptiveGridStep.d.ts.map +1 -0
- package/dist/utils/pathViewport.d.ts +12 -0
- package/dist/utils/pathViewport.d.ts.map +1 -0
- package/dist/utils/screenText.d.ts +4 -0
- package/dist/utils/screenText.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/papirus.js
CHANGED
|
@@ -3220,6 +3220,20 @@ function measureTextLines(ctx, lines) {
|
|
|
3220
3220
|
for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
|
|
3221
3221
|
return maxWidth;
|
|
3222
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
|
+
}
|
|
3223
3237
|
//#endregion
|
|
3224
3238
|
//#region src/elements/TextLabel.ts
|
|
3225
3239
|
var DEFAULT_INSET = 8;
|
|
@@ -3477,10 +3491,10 @@ var TextLabel = class {
|
|
|
3477
3491
|
default: startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
|
|
3478
3492
|
}
|
|
3479
3493
|
ctx.fillStyle = this._style.color ?? "#000000";
|
|
3494
|
+
if (!isScreenFontReadable(ctx, this._style.fontSize ?? 14)) return;
|
|
3480
3495
|
for (let i = 0; i < this._lines.length; i++) {
|
|
3481
|
-
const line = this._lines[i];
|
|
3482
3496
|
const y = startY + i * lineHeight;
|
|
3483
|
-
ctx.fillText(
|
|
3497
|
+
ctx.fillText(this._lines[i], x, y);
|
|
3484
3498
|
}
|
|
3485
3499
|
}
|
|
3486
3500
|
/**
|
|
@@ -6411,6 +6425,7 @@ function drawLegacyArrowHead(ctx, from, to) {
|
|
|
6411
6425
|
function renderEdgeLabel(ctx, options) {
|
|
6412
6426
|
const { label, path, background } = options;
|
|
6413
6427
|
if (!label || path.length < 2) return;
|
|
6428
|
+
if (!isScreenFontReadable(ctx, label.style.fontSize ?? 14)) return;
|
|
6414
6429
|
const labelCenter = getEdgeLabelPosition(options);
|
|
6415
6430
|
if (!labelCenter) return;
|
|
6416
6431
|
const rotation = getEdgeLabelRotation(options);
|
|
@@ -7060,7 +7075,7 @@ var Edge = class extends Element {
|
|
|
7060
7075
|
}
|
|
7061
7076
|
return pts;
|
|
7062
7077
|
};
|
|
7063
|
-
if (!(this._label !== void 0 && this._labelLineGap)) {
|
|
7078
|
+
if (!(this._label !== void 0 && this._labelLineGap && isScreenFontReadable(ctx, this._label.style.fontSize ?? 14))) {
|
|
7064
7079
|
const start = path[0];
|
|
7065
7080
|
const startNext = path[1];
|
|
7066
7081
|
const startAngle = Math.atan2(startNext.y - start.y, startNext.x - start.x);
|
|
@@ -7547,6 +7562,7 @@ var InteractionManager = class {
|
|
|
7547
7562
|
this.handledScrollbarMouseDown = false;
|
|
7548
7563
|
this.overlayDragSession = null;
|
|
7549
7564
|
this.handledOverlayMouseDown = false;
|
|
7565
|
+
this.editHoverSuspendedForPan = false;
|
|
7550
7566
|
this.renderer = options.renderer;
|
|
7551
7567
|
this.navigationOnly = options.navigationOnly ?? false;
|
|
7552
7568
|
this.inputHandler = new InputHandler({
|
|
@@ -7730,7 +7746,7 @@ var InteractionManager = class {
|
|
|
7730
7746
|
setupEvents() {
|
|
7731
7747
|
this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
|
|
7732
7748
|
this.selectionManager.renderSelectionRect(ctx);
|
|
7733
|
-
if (!this.navigationOnly) {
|
|
7749
|
+
if (!this.navigationOnly && !this.navigationManager.panning) {
|
|
7734
7750
|
this.dragManager.renderAlignmentGuides(ctx);
|
|
7735
7751
|
this.connectionManager.renderPreview(ctx);
|
|
7736
7752
|
this.connectionManager.renderHoverAnchors(ctx);
|
|
@@ -7901,7 +7917,7 @@ var InteractionManager = class {
|
|
|
7901
7917
|
if (event.button === 0) {
|
|
7902
7918
|
if (this.navigationOnly) {
|
|
7903
7919
|
this.selectionManager.clearSelection();
|
|
7904
|
-
this.
|
|
7920
|
+
this.beginBackgroundPan(event);
|
|
7905
7921
|
return;
|
|
7906
7922
|
}
|
|
7907
7923
|
if (hitElement === void 0) {
|
|
@@ -7910,11 +7926,14 @@ var InteractionManager = class {
|
|
|
7910
7926
|
return;
|
|
7911
7927
|
}
|
|
7912
7928
|
this.selectionManager.clearSelection();
|
|
7913
|
-
this.
|
|
7929
|
+
this.beginBackgroundPan(event);
|
|
7914
7930
|
return;
|
|
7915
7931
|
}
|
|
7916
7932
|
}
|
|
7917
|
-
if (this.navigationManager.handleMouseDown(event))
|
|
7933
|
+
if (this.navigationManager.handleMouseDown(event)) {
|
|
7934
|
+
this.suspendEditHover();
|
|
7935
|
+
return;
|
|
7936
|
+
}
|
|
7918
7937
|
if (this.navigationOnly) return;
|
|
7919
7938
|
if (this.dragManager.handleMouseDown(event)) return;
|
|
7920
7939
|
}
|
|
@@ -7928,7 +7947,11 @@ var InteractionManager = class {
|
|
|
7928
7947
|
this.renderer.setScrollbarActiveAxis(null);
|
|
7929
7948
|
this.scrollbarDragState = null;
|
|
7930
7949
|
}
|
|
7931
|
-
if (this.navigationManager.panning) this.
|
|
7950
|
+
if (this.navigationManager.panning) this.finishBackgroundPan(event);
|
|
7951
|
+
}
|
|
7952
|
+
if (this.navigationManager.panning) {
|
|
7953
|
+
this.navigationManager.handleMouseMove(event);
|
|
7954
|
+
return;
|
|
7932
7955
|
}
|
|
7933
7956
|
const overScrollbar = this.renderer.updateScrollbarHover(event.screenX, event.screenY);
|
|
7934
7957
|
const overOverlayPointerSink = this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY);
|
|
@@ -7990,7 +8013,7 @@ var InteractionManager = class {
|
|
|
7990
8013
|
this.selectionManager.endSelectionRect();
|
|
7991
8014
|
return;
|
|
7992
8015
|
}
|
|
7993
|
-
this.
|
|
8016
|
+
this.finishBackgroundPan(event);
|
|
7994
8017
|
}
|
|
7995
8018
|
handleClick(event) {
|
|
7996
8019
|
if (this.handledOverlayMouseDown) {
|
|
@@ -8094,6 +8117,24 @@ var InteractionManager = class {
|
|
|
8094
8117
|
}
|
|
8095
8118
|
return closestEdge;
|
|
8096
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
|
+
}
|
|
8097
8138
|
handleWheel(event) {
|
|
8098
8139
|
if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
|
|
8099
8140
|
this.navigationManager.handleWheel(event);
|
|
@@ -8132,6 +8173,7 @@ var InteractionManager = class {
|
|
|
8132
8173
|
}
|
|
8133
8174
|
handleKeyUp(event) {
|
|
8134
8175
|
this.navigationManager.handleKeyUp(event);
|
|
8176
|
+
if (!this.navigationManager.panning) this.resumeEditHover();
|
|
8135
8177
|
}
|
|
8136
8178
|
handleViewportNavigationKey(event) {
|
|
8137
8179
|
switch (event.key) {
|
|
@@ -8316,6 +8358,66 @@ var InteractionManager = class {
|
|
|
8316
8358
|
}
|
|
8317
8359
|
};
|
|
8318
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
|
|
8319
8421
|
//#region src/core/AnimationManager.ts
|
|
8320
8422
|
var DEFAULT_OPTIONS$3 = {
|
|
8321
8423
|
enabled: false,
|
|
@@ -9294,6 +9396,8 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
9294
9396
|
this._dirty = true;
|
|
9295
9397
|
this._styleDirty = true;
|
|
9296
9398
|
this._contentDirty = true;
|
|
9399
|
+
this._contentRevision = 0;
|
|
9400
|
+
this._contentBoundsCache = null;
|
|
9297
9401
|
this._destroyed = false;
|
|
9298
9402
|
this._contextLost = false;
|
|
9299
9403
|
this._canvasRect = null;
|
|
@@ -9730,7 +9834,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
9730
9834
|
addGroup(group) {
|
|
9731
9835
|
group.setDirtyListener(() => this.markDirty());
|
|
9732
9836
|
this._groups.set(group.id, group);
|
|
9733
|
-
this.
|
|
9837
|
+
this.markContentDirty();
|
|
9734
9838
|
}
|
|
9735
9839
|
/**
|
|
9736
9840
|
* Remove a group from the diagram
|
|
@@ -9740,7 +9844,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
9740
9844
|
if (group === void 0) return false;
|
|
9741
9845
|
this._groups.delete(groupId);
|
|
9742
9846
|
group.setDirtyListener(void 0);
|
|
9743
|
-
this.
|
|
9847
|
+
this.markContentDirty();
|
|
9744
9848
|
return true;
|
|
9745
9849
|
}
|
|
9746
9850
|
/**
|
|
@@ -9814,10 +9918,18 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
9814
9918
|
*/
|
|
9815
9919
|
markContentDirty() {
|
|
9816
9920
|
this._contentDirty = true;
|
|
9921
|
+
this._contentRevision += 1;
|
|
9817
9922
|
this._nodeObstaclesCache = null;
|
|
9818
9923
|
this._dirty = true;
|
|
9819
9924
|
}
|
|
9820
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
|
+
/**
|
|
9821
9933
|
* Visible world rectangle plus padding so labels and arrow markers are not culled early.
|
|
9822
9934
|
*/
|
|
9823
9935
|
getViewportWorldBounds() {
|
|
@@ -9835,10 +9947,13 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
9835
9947
|
}
|
|
9836
9948
|
/**
|
|
9837
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.
|
|
9838
9951
|
*/
|
|
9839
9952
|
edgeIntersectsViewport(edge, viewport) {
|
|
9840
|
-
|
|
9841
|
-
|
|
9953
|
+
return edgePathIntersectsViewport({
|
|
9954
|
+
path: edge.path,
|
|
9955
|
+
type: edge.type
|
|
9956
|
+
}, viewport);
|
|
9842
9957
|
}
|
|
9843
9958
|
/**
|
|
9844
9959
|
* Force an immediate render
|
|
@@ -9938,13 +10053,13 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
9938
10053
|
applyStyleManagerToElements(this.styleManager, this._groups.values(), this._edges.values(), this._nodes.values());
|
|
9939
10054
|
this._styleDirty = false;
|
|
9940
10055
|
}
|
|
9941
|
-
for (const group of this._groups.values()) group.recalculateBounds();
|
|
9942
10056
|
const viewport = this.getViewportWorldBounds();
|
|
9943
10057
|
for (const group of this._groups.values()) if (group.visible && this.intersectsViewport(group.getBounds(), viewport)) {
|
|
9944
10058
|
this.renderElementWithAnimation(ctx, group, () => group.render(ctx));
|
|
9945
10059
|
group.clearDirty();
|
|
9946
10060
|
}
|
|
9947
10061
|
if (this._contentDirty) {
|
|
10062
|
+
for (const group of this._groups.values()) group.recalculateBounds();
|
|
9948
10063
|
this.edgeEndpointUpdater.updateAll();
|
|
9949
10064
|
this._contentDirty = false;
|
|
9950
10065
|
}
|
|
@@ -10007,11 +10122,17 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
10007
10122
|
return this._nodeObstaclesCache;
|
|
10008
10123
|
}
|
|
10009
10124
|
getContentBounds() {
|
|
10010
|
-
return
|
|
10125
|
+
if (this._contentBoundsCache?.revision === this._contentRevision) return this._contentBoundsCache.bounds;
|
|
10126
|
+
const bounds = getContentBounds({
|
|
10011
10127
|
nodes: this._nodes.values(),
|
|
10012
10128
|
edges: this._edges.values(),
|
|
10013
10129
|
groups: this._groups.values()
|
|
10014
10130
|
});
|
|
10131
|
+
this._contentBoundsCache = {
|
|
10132
|
+
revision: this._contentRevision,
|
|
10133
|
+
bounds
|
|
10134
|
+
};
|
|
10135
|
+
return bounds;
|
|
10015
10136
|
}
|
|
10016
10137
|
notifyScrollbarInteraction() {
|
|
10017
10138
|
this.scrollbarController.notifyInteraction();
|
|
@@ -11947,6 +12068,7 @@ var Group = class Group extends Element {
|
|
|
11947
12068
|
*/
|
|
11948
12069
|
recalculateBounds() {
|
|
11949
12070
|
if (this._children.length === 0) {
|
|
12071
|
+
if (this._x === 0 && this._y === 0 && this._width === 0 && this._height === 0) return;
|
|
11950
12072
|
this._x = 0;
|
|
11951
12073
|
this._y = 0;
|
|
11952
12074
|
this._width = 0;
|
|
@@ -11965,10 +12087,15 @@ var Group = class Group extends Element {
|
|
|
11965
12087
|
maxX = Math.max(maxX, bounds.x + bounds.width);
|
|
11966
12088
|
maxY = Math.max(maxY, bounds.y + bounds.height);
|
|
11967
12089
|
}
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
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;
|
|
11972
12099
|
this.markDirty();
|
|
11973
12100
|
}
|
|
11974
12101
|
hitTest(point) {
|
|
@@ -12856,6 +12983,7 @@ var CText = class extends CompositeComponentBase {
|
|
|
12856
12983
|
if (this.style.visible === false) return;
|
|
12857
12984
|
const opacity = this.style.opacity ?? 1;
|
|
12858
12985
|
if (opacity <= 0) return;
|
|
12986
|
+
if (!isScreenFontReadable(ctx, this._fontSize)) return;
|
|
12859
12987
|
ctx.save();
|
|
12860
12988
|
ctx.globalAlpha *= opacity;
|
|
12861
12989
|
ctx.font = this.getFont();
|
|
@@ -12908,8 +13036,14 @@ var CText = class extends CompositeComponentBase {
|
|
|
12908
13036
|
ctx.rotate(this._rotation * Math.PI / 180);
|
|
12909
13037
|
const offsetX = xBase - (textBounds.x + textBounds.width / 2);
|
|
12910
13038
|
const offsetY = yStart - (textBounds.y + textBounds.height / 2);
|
|
12911
|
-
for (let i = 0; i < lines.length; i++)
|
|
12912
|
-
|
|
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
|
+
}
|
|
12913
13047
|
ctx.restore();
|
|
12914
13048
|
}
|
|
12915
13049
|
serialize() {
|
|
@@ -15546,6 +15680,24 @@ function distributeNodes(nodes, type) {
|
|
|
15546
15680
|
}
|
|
15547
15681
|
}
|
|
15548
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
|
+
}
|
|
15549
15701
|
//#endregion
|
|
15550
15702
|
//#region src/core/overlays/BaseOverlay.ts
|
|
15551
15703
|
/**
|
|
@@ -15584,20 +15736,20 @@ var GridOverlay = class extends BaseOverlay {
|
|
|
15584
15736
|
if (!this.enabled) return;
|
|
15585
15737
|
const width = renderer.width;
|
|
15586
15738
|
const height = renderer.height;
|
|
15587
|
-
const
|
|
15588
|
-
const
|
|
15589
|
-
const startX = Math.floor(-renderer.offsetX /
|
|
15590
|
-
const startY = Math.floor(-renderer.offsetY /
|
|
15591
|
-
const endX = Math.ceil((width - renderer.offsetX) /
|
|
15592
|
-
const endY = Math.ceil((height - renderer.offsetY) /
|
|
15593
|
-
ctx.strokeStyle = color;
|
|
15594
|
-
ctx.lineWidth = 1 /
|
|
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;
|
|
15595
15747
|
ctx.beginPath();
|
|
15596
|
-
for (let x = startX; x <= endX; x +=
|
|
15748
|
+
for (let x = startX; x <= endX; x += step) {
|
|
15597
15749
|
ctx.moveTo(x, startY);
|
|
15598
15750
|
ctx.lineTo(x, endY);
|
|
15599
15751
|
}
|
|
15600
|
-
for (let y = startY; y <= endY; y +=
|
|
15752
|
+
for (let y = startY; y <= endY; y += step) {
|
|
15601
15753
|
ctx.moveTo(startX, y);
|
|
15602
15754
|
ctx.lineTo(endX, y);
|
|
15603
15755
|
}
|
|
@@ -15610,51 +15762,114 @@ var GridOverlay = class extends BaseOverlay {
|
|
|
15610
15762
|
var RulersOverlay = class extends BaseOverlay {
|
|
15611
15763
|
constructor(options = {}) {
|
|
15612
15764
|
super(options.enabled ?? true);
|
|
15765
|
+
this.tickBake = null;
|
|
15766
|
+
this.tickBakeGeneration = 0;
|
|
15613
15767
|
this.thickness = options.thickness ?? 20;
|
|
15614
15768
|
this.backgroundColor = options.backgroundColor ?? "#f4f4f5";
|
|
15615
15769
|
this.textColor = options.textColor ?? "#52525b";
|
|
15616
15770
|
this.tickColor = options.tickColor ?? "#a1a1aa";
|
|
15617
15771
|
}
|
|
15772
|
+
getTickBakeGeneration() {
|
|
15773
|
+
return this.tickBakeGeneration;
|
|
15774
|
+
}
|
|
15618
15775
|
install(renderer) {
|
|
15619
15776
|
this.removeOverlay = renderer.addOverlayRenderer((ctx) => {
|
|
15620
15777
|
if (!this.enabled) return;
|
|
15621
15778
|
const thickness = this.thickness;
|
|
15779
|
+
const zoom = renderer.zoom;
|
|
15780
|
+
const worldLeft = -renderer.offsetX / zoom;
|
|
15781
|
+
const worldTop = -renderer.offsetY / zoom;
|
|
15622
15782
|
ctx.save();
|
|
15623
15783
|
ctx.setTransform(renderer.pixelRatio, 0, 0, renderer.pixelRatio, 0, 0);
|
|
15624
15784
|
ctx.fillStyle = this.backgroundColor;
|
|
15625
15785
|
ctx.fillRect(0, 0, renderer.width, thickness);
|
|
15626
15786
|
ctx.fillRect(0, 0, thickness, renderer.height);
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
ctx.
|
|
15632
|
-
|
|
15633
|
-
const worldLeft = -renderer.offsetX / renderer.zoom;
|
|
15634
|
-
const worldTop = -renderer.offsetY / renderer.zoom;
|
|
15635
|
-
const worldRight = worldLeft + renderer.width / renderer.zoom;
|
|
15636
|
-
const worldBottom = worldTop + renderer.height / renderer.zoom;
|
|
15637
|
-
ctx.beginPath();
|
|
15638
|
-
for (let x = Math.floor(worldLeft / step) * step; x <= worldRight; x += step) {
|
|
15639
|
-
const screenX = x * renderer.zoom + renderer.offsetX;
|
|
15640
|
-
ctx.moveTo(screenX, thickness);
|
|
15641
|
-
ctx.lineTo(screenX, thickness - 6);
|
|
15642
|
-
ctx.fillText(`${Math.round(x)}`, screenX + 2, 2);
|
|
15643
|
-
}
|
|
15644
|
-
for (let y = Math.floor(worldTop / step) * step; y <= worldBottom; y += step) {
|
|
15645
|
-
const screenY = y * renderer.zoom + renderer.offsetY;
|
|
15646
|
-
ctx.moveTo(thickness, screenY);
|
|
15647
|
-
ctx.lineTo(thickness - 6, screenY);
|
|
15648
|
-
ctx.save();
|
|
15649
|
-
ctx.translate(2, screenY + 2);
|
|
15650
|
-
ctx.rotate(-Math.PI / 2);
|
|
15651
|
-
ctx.fillText(`${Math.round(y)}`, 0, 0);
|
|
15652
|
-
ctx.restore();
|
|
15653
|
-
}
|
|
15654
|
-
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);
|
|
15655
15793
|
ctx.restore();
|
|
15656
15794
|
});
|
|
15657
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
|
+
}
|
|
15658
15873
|
getTickStep(zoom) {
|
|
15659
15874
|
const rawStep = 80 / zoom;
|
|
15660
15875
|
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
|
|
@@ -15710,6 +15925,7 @@ var GuidesOverlay = class extends BaseOverlay {
|
|
|
15710
15925
|
var MiniMap = class extends BaseOverlay {
|
|
15711
15926
|
constructor(options = {}) {
|
|
15712
15927
|
super(options.enabled ?? true);
|
|
15928
|
+
this.contentBake = null;
|
|
15713
15929
|
this.options = {
|
|
15714
15930
|
width: options.width ?? 180,
|
|
15715
15931
|
height: options.height ?? 120,
|
|
@@ -15721,12 +15937,16 @@ var MiniMap = class extends BaseOverlay {
|
|
|
15721
15937
|
anchor: options.anchor ?? "bottom-right"
|
|
15722
15938
|
};
|
|
15723
15939
|
}
|
|
15940
|
+
/** Cached bake projection; stable across pan if only the viewport moved. */
|
|
15941
|
+
getContentBakeProjectionKey() {
|
|
15942
|
+
return this.contentBake?.projectionKey ?? null;
|
|
15943
|
+
}
|
|
15724
15944
|
install(renderer) {
|
|
15725
15945
|
this.removeOverlay = renderer.addTopOverlayRenderer((ctx) => {
|
|
15726
15946
|
if (!this.enabled) return;
|
|
15727
15947
|
const layout = this.computeLayout(renderer);
|
|
15728
15948
|
if (!layout) return;
|
|
15729
|
-
const { x, y, width, height,
|
|
15949
|
+
const { x, y, width, height, viewRect } = layout;
|
|
15730
15950
|
ctx.save();
|
|
15731
15951
|
ctx.setTransform(renderer.pixelRatio, 0, 0, renderer.pixelRatio, 0, 0);
|
|
15732
15952
|
ctx.setLineDash([]);
|
|
@@ -15736,42 +15956,61 @@ var MiniMap = class extends BaseOverlay {
|
|
|
15736
15956
|
ctx.strokeStyle = this.options.borderColor;
|
|
15737
15957
|
ctx.lineWidth = 1;
|
|
15738
15958
|
ctx.strokeRect(x, y, width, height);
|
|
15739
|
-
|
|
15740
|
-
ctx.
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
for (const edge of renderer.edges.values()) {
|
|
15748
|
-
if (!edge.visible) continue;
|
|
15749
|
-
const path = edge.path;
|
|
15750
|
-
if (path.length < 2) continue;
|
|
15751
|
-
ctx.beginPath();
|
|
15752
|
-
ctx.moveTo(path[0].x, path[0].y);
|
|
15753
|
-
for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y);
|
|
15754
|
-
ctx.stroke();
|
|
15755
|
-
}
|
|
15756
|
-
for (const node of renderer.nodes.values()) {
|
|
15757
|
-
if (!node.visible) continue;
|
|
15758
|
-
const b = node.getBounds();
|
|
15759
|
-
ctx.fillStyle = "#e2e8f0";
|
|
15760
|
-
ctx.fillRect(b.x, b.y, b.width, b.height);
|
|
15761
|
-
}
|
|
15762
|
-
ctx.restore();
|
|
15763
|
-
if (viewRect.width < 0 || viewRect.height < 0) {
|
|
15764
|
-
ctx.restore();
|
|
15765
|
-
return;
|
|
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);
|
|
15766
15967
|
}
|
|
15767
|
-
ctx.strokeStyle = this.options.viewportColor;
|
|
15768
|
-
ctx.lineWidth = 1.5;
|
|
15769
|
-
ctx.setLineDash([]);
|
|
15770
|
-
ctx.lineDashOffset = 0;
|
|
15771
|
-
ctx.strokeRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
|
|
15772
15968
|
ctx.restore();
|
|
15773
15969
|
});
|
|
15774
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
|
+
}
|
|
15775
16014
|
beginOverlayDrag(renderer, screenX, screenY) {
|
|
15776
16015
|
const hit = this.hitTestMiniMap(renderer, screenX, screenY);
|
|
15777
16016
|
if (!hit) return null;
|
|
@@ -15833,12 +16072,11 @@ var MiniMap = class extends BaseOverlay {
|
|
|
15833
16072
|
computeLayout(renderer) {
|
|
15834
16073
|
const rawBounds = getContentBounds({
|
|
15835
16074
|
nodes: renderer.nodes.values(),
|
|
15836
|
-
edges: renderer.edges.values(),
|
|
15837
16075
|
groups: renderer.groups.values()
|
|
15838
16076
|
});
|
|
15839
16077
|
if (!rawBounds) return null;
|
|
15840
16078
|
const margin = this.options.contentMargin;
|
|
15841
|
-
const
|
|
16079
|
+
const bounds = {
|
|
15842
16080
|
x: rawBounds.x - margin,
|
|
15843
16081
|
y: rawBounds.y - margin,
|
|
15844
16082
|
width: rawBounds.width + 2 * margin,
|
|
@@ -15848,17 +16086,7 @@ var MiniMap = class extends BaseOverlay {
|
|
|
15848
16086
|
const x = anchor === "bottom-left" || anchor === "top-left" ? padding : renderer.width - width - padding;
|
|
15849
16087
|
const y = anchor === "top-left" || anchor === "top-right" ? padding : renderer.height - height - padding;
|
|
15850
16088
|
const viewportBounds = this.getViewportBounds(renderer);
|
|
15851
|
-
const
|
|
15852
|
-
const unionMinY = Math.min(expandedContentBounds.y, viewportBounds.y);
|
|
15853
|
-
const unionMaxX = Math.max(expandedContentBounds.x + expandedContentBounds.width, viewportBounds.x + viewportBounds.width);
|
|
15854
|
-
const unionMaxY = Math.max(expandedContentBounds.y + expandedContentBounds.height, viewportBounds.y + viewportBounds.height);
|
|
15855
|
-
const bounds = {
|
|
15856
|
-
x: unionMinX,
|
|
15857
|
-
y: unionMinY,
|
|
15858
|
-
width: unionMaxX - unionMinX,
|
|
15859
|
-
height: unionMaxY - unionMinY
|
|
15860
|
-
};
|
|
15861
|
-
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));
|
|
15862
16090
|
const mapOffsetX = x + (width - bounds.width * scale) / 2;
|
|
15863
16091
|
const mapOffsetY = y + (height - bounds.height * scale) / 2;
|
|
15864
16092
|
let viewX = mapOffsetX + (viewportBounds.x - bounds.x) * scale;
|