@sequent-org/moodboard 1.3.5 → 1.4.1
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/package.json +6 -1
- package/src/assets/icons/mindmap.svg +3 -0
- package/src/core/SaveManager.js +44 -15
- package/src/core/commands/MindmapStatePatchCommand.js +85 -0
- package/src/core/commands/UpdateContentCommand.js +47 -4
- package/src/core/flows/LayerAndViewportFlow.js +87 -14
- package/src/core/flows/ObjectLifecycleFlow.js +7 -2
- package/src/core/flows/SaveFlow.js +10 -7
- package/src/core/flows/TransformFlow.js +2 -2
- package/src/core/index.js +81 -11
- package/src/core/rendering/ObjectRenderer.js +7 -2
- package/src/grid/BaseGrid.js +65 -0
- package/src/grid/CrossGrid.js +89 -24
- package/src/grid/CrossGridZoomPhases.js +167 -0
- package/src/grid/DotGrid.js +117 -34
- package/src/grid/DotGridZoomPhases.js +214 -16
- package/src/grid/GridDiagnostics.js +80 -0
- package/src/grid/GridFactory.js +13 -11
- package/src/grid/LineGrid.js +176 -37
- package/src/grid/LineGridZoomPhases.js +163 -0
- package/src/grid/ScreenGridPhaseMachine.js +51 -0
- package/src/mindmap/MindmapCompoundContract.js +235 -0
- package/src/moodboard/ActionHandler.js +1 -0
- package/src/moodboard/DataManager.js +57 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +21 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +82 -1
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +15 -0
- package/src/objects/MindmapObject.js +76 -0
- package/src/objects/ObjectFactory.js +3 -1
- package/src/services/BoardService.js +127 -31
- package/src/services/GridSnapResolver.js +60 -0
- package/src/services/MiroZoomLevels.js +39 -0
- package/src/services/SettingsApplier.js +0 -4
- package/src/services/ZoomPanController.js +51 -32
- package/src/tools/object-tools/PlacementTool.js +12 -3
- package/src/tools/object-tools/SelectTool.js +11 -1
- package/src/tools/object-tools/placement/GhostController.js +100 -1
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +2 -2
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +2 -2
- package/src/tools/object-tools/selection/InlineEditorController.js +15 -0
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +757 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +20 -1
- package/src/tools/object-tools/selection/SelectToolSetup.js +2 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +7 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +12 -16
- package/src/ui/ContextMenu.js +6 -6
- package/src/ui/DotGridDebugPanel.js +253 -0
- package/src/ui/HtmlTextLayer.js +1 -1
- package/src/ui/TextPropertiesPanel.js +2 -2
- package/src/ui/handles/GroupSelectionHandlesController.js +4 -1
- package/src/ui/handles/HandlesDomRenderer.js +1485 -14
- package/src/ui/handles/HandlesEventBridge.js +49 -5
- package/src/ui/handles/HandlesInteractionController.js +4 -4
- package/src/ui/mindmap/MindmapConnectionLayer.js +254 -0
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +285 -0
- package/src/ui/mindmap/MindmapLayoutConfig.js +29 -0
- package/src/ui/mindmap/MindmapTextOverlayAdapter.js +144 -0
- package/src/ui/styles/toolbar.css +1 -0
- package/src/ui/styles/workspace.css +100 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +35 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +6 -6
- package/src/ui/toolbar/ToolbarRenderer.js +1 -0
- package/src/ui/toolbar/ToolbarStateController.js +1 -0
- package/src/utils/iconLoader.js +10 -4
package/src/grid/LineGrid.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BaseGrid } from './BaseGrid.js';
|
|
2
|
+
import { getScreenAnchor } from './ScreenGridPhaseMachine.js';
|
|
3
|
+
import { resolveLineGridState } from './LineGridZoomPhases.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Линейная прямоугольная сетка
|
|
@@ -7,16 +9,30 @@ export class LineGrid extends BaseGrid {
|
|
|
7
9
|
constructor(options = {}) {
|
|
8
10
|
super(options);
|
|
9
11
|
this.type = 'line';
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
12
|
+
|
|
13
|
+
// Жесткий Miro-профиль line-grid: не зависим от сохраненных override.
|
|
14
|
+
this.showSubGrid = false;
|
|
15
|
+
this.subGridDivisions = 0;
|
|
16
|
+
this.subGridColor = 0xFEFEFE;
|
|
17
|
+
this.subGridOpacity = 1;
|
|
18
|
+
this.bigGridColor = 0xECECEC;
|
|
19
|
+
this.bigGridOpacity = 1;
|
|
20
|
+
this.lineWidth = 1;
|
|
21
|
+
this.color = 0xF4F4F4;
|
|
22
|
+
this.opacity = 1;
|
|
23
|
+
|
|
24
|
+
this._majorAnchorX = null;
|
|
25
|
+
this._majorAnchorY = null;
|
|
26
|
+
this._majorStepX = null;
|
|
27
|
+
this._majorStepY = null;
|
|
28
|
+
this._minorAnchorX = null;
|
|
29
|
+
this._minorAnchorY = null;
|
|
30
|
+
this._minorStepX = null;
|
|
31
|
+
this._minorStepY = null;
|
|
32
|
+
this._majorCursorOffsetX = 0;
|
|
33
|
+
this._majorCursorOffsetY = 0;
|
|
34
|
+
this._minorCursorOffsetX = 0;
|
|
35
|
+
this._minorCursorOffsetY = 0;
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
/**
|
|
@@ -27,20 +43,26 @@ export class LineGrid extends BaseGrid {
|
|
|
27
43
|
if (typeof this.opacity === 'number') {
|
|
28
44
|
this.graphics.alpha = this.opacity;
|
|
29
45
|
}
|
|
46
|
+
const state = this.getScreenGridState();
|
|
30
47
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
this.graphics.lineStyle({
|
|
49
|
+
width: Math.max(1, Math.round(this.lineWidth || 1)),
|
|
50
|
+
color: this.color,
|
|
51
|
+
alpha: 1,
|
|
52
|
+
alignment: 0
|
|
53
|
+
});
|
|
33
54
|
} catch (_) {
|
|
34
|
-
this.graphics.lineStyle(this.lineWidth, this.color, 1);
|
|
55
|
+
this.graphics.lineStyle(Math.max(1, Math.round(this.lineWidth || 1)), this.color, 1);
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
// Основные линии сетки
|
|
38
59
|
this.drawMainGrid();
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
if (
|
|
42
|
-
this.
|
|
60
|
+
|
|
61
|
+
// Вторая (крупная) сетка.
|
|
62
|
+
if ((state.secondGridStep || 0) > 0) {
|
|
63
|
+
this.drawSecondGrid();
|
|
43
64
|
}
|
|
65
|
+
|
|
44
66
|
}
|
|
45
67
|
|
|
46
68
|
/**
|
|
@@ -48,21 +70,32 @@ export class LineGrid extends BaseGrid {
|
|
|
48
70
|
*/
|
|
49
71
|
drawMainGrid() {
|
|
50
72
|
const b = this.getDrawBounds();
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
73
|
+
const { screenStep } = this.getScreenGridState();
|
|
74
|
+
const step = Math.max(1, screenStep);
|
|
75
|
+
const worldX = this.viewportTransform?.worldX || 0;
|
|
76
|
+
const worldY = this.viewportTransform?.worldY || 0;
|
|
77
|
+
const cursorX = this.viewportTransform?.zoomCursorX;
|
|
78
|
+
const cursorY = this.viewportTransform?.zoomCursorY;
|
|
79
|
+
const useCursorAnchor = this.viewportTransform?.useCursorAnchor === true;
|
|
80
|
+
const anchorX = this._resolveScreenAnchor('x', worldX, step, cursorX, useCursorAnchor, 'major');
|
|
81
|
+
const anchorY = this._resolveScreenAnchor('y', worldY, step, cursorY, useCursorAnchor, 'major');
|
|
82
|
+
const alignStart = (min, anchor) => {
|
|
83
|
+
const d = ((anchor - min) % step + step) % step;
|
|
84
|
+
return min + d;
|
|
85
|
+
};
|
|
86
|
+
const startX = alignStart(b.left, anchorX);
|
|
87
|
+
const endX = b.right + step;
|
|
88
|
+
const startY = alignStart(b.top, anchorY);
|
|
89
|
+
const endY = b.bottom + step;
|
|
57
90
|
// Вертикальные линии
|
|
58
91
|
for (let x = startX; x <= endX; x += step) {
|
|
59
|
-
const px = Math.round(x)
|
|
92
|
+
const px = Math.round(x);
|
|
60
93
|
this.graphics.moveTo(px, b.top);
|
|
61
94
|
this.graphics.lineTo(px, b.bottom);
|
|
62
95
|
}
|
|
63
96
|
// Горизонтальные линии
|
|
64
97
|
for (let y = startY; y <= endY; y += step) {
|
|
65
|
-
const py = Math.round(y)
|
|
98
|
+
const py = Math.round(y);
|
|
66
99
|
this.graphics.moveTo(b.left, py);
|
|
67
100
|
this.graphics.lineTo(b.right, py);
|
|
68
101
|
}
|
|
@@ -72,32 +105,136 @@ export class LineGrid extends BaseGrid {
|
|
|
72
105
|
* Рисует подсетку (более мелкие линии)
|
|
73
106
|
*/
|
|
74
107
|
drawSubGrid() {
|
|
75
|
-
const
|
|
108
|
+
const { screenStep, minorScreenStep } = this.getScreenGridState();
|
|
109
|
+
const subSize = Math.max(1, Math.round(minorScreenStep || (screenStep / this.subGridDivisions)));
|
|
76
110
|
try {
|
|
77
|
-
this.graphics.lineStyle({
|
|
111
|
+
this.graphics.lineStyle({
|
|
112
|
+
width: 1,
|
|
113
|
+
color: this.subGridColor,
|
|
114
|
+
alpha: this.subGridOpacity,
|
|
115
|
+
alignment: 0
|
|
116
|
+
});
|
|
78
117
|
} catch (_) {
|
|
79
|
-
this.graphics.lineStyle(
|
|
118
|
+
this.graphics.lineStyle(1, this.subGridColor, this.subGridOpacity);
|
|
80
119
|
}
|
|
81
120
|
const b = this.getDrawBounds();
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
121
|
+
const worldX = this.viewportTransform?.worldX || 0;
|
|
122
|
+
const worldY = this.viewportTransform?.worldY || 0;
|
|
123
|
+
const cursorX = this.viewportTransform?.zoomCursorX;
|
|
124
|
+
const cursorY = this.viewportTransform?.zoomCursorY;
|
|
125
|
+
const useCursorAnchor = this.viewportTransform?.useCursorAnchor === true;
|
|
126
|
+
const anchorX = this._resolveScreenAnchor('x', worldX, subSize, cursorX, useCursorAnchor, 'minor');
|
|
127
|
+
const anchorY = this._resolveScreenAnchor('y', worldY, subSize, cursorY, useCursorAnchor, 'minor');
|
|
128
|
+
const alignStart = (min, anchor) => {
|
|
129
|
+
const d = ((anchor - min) % subSize + subSize) % subSize;
|
|
130
|
+
return min + d;
|
|
131
|
+
};
|
|
132
|
+
const startX = alignStart(b.left, anchorX);
|
|
133
|
+
const endX = b.right + subSize;
|
|
134
|
+
const startY = alignStart(b.top, anchorY);
|
|
135
|
+
const endY = b.bottom + subSize;
|
|
136
|
+
const majorStep = Math.max(1, screenStep);
|
|
137
|
+
const majorAnchorX = this._resolveScreenAnchor('x', worldX, majorStep, cursorX, useCursorAnchor, 'major');
|
|
138
|
+
const majorAnchorY = this._resolveScreenAnchor('y', worldY, majorStep, cursorY, useCursorAnchor, 'major');
|
|
139
|
+
const isOnMajor = (value, major, anchor) => {
|
|
140
|
+
const rel = (value - anchor) / major;
|
|
141
|
+
return Math.abs(rel - Math.round(rel)) < 1e-4;
|
|
142
|
+
};
|
|
86
143
|
for (let x = startX; x <= endX; x += subSize) {
|
|
87
|
-
if (x
|
|
88
|
-
const px = Math.round(x)
|
|
144
|
+
if (!isOnMajor(x, majorStep, majorAnchorX)) {
|
|
145
|
+
const px = Math.round(x);
|
|
89
146
|
this.graphics.moveTo(px, b.top);
|
|
90
147
|
this.graphics.lineTo(px, b.bottom);
|
|
91
148
|
}
|
|
92
149
|
}
|
|
93
150
|
for (let y = startY; y <= endY; y += subSize) {
|
|
94
|
-
if (y
|
|
95
|
-
const py = Math.round(y)
|
|
151
|
+
if (!isOnMajor(y, majorStep, majorAnchorY)) {
|
|
152
|
+
const py = Math.round(y);
|
|
96
153
|
this.graphics.moveTo(b.left, py);
|
|
97
154
|
this.graphics.lineTo(b.right, py);
|
|
98
155
|
}
|
|
99
156
|
}
|
|
100
157
|
}
|
|
158
|
+
|
|
159
|
+
drawSecondGrid() {
|
|
160
|
+
const { secondGridStep } = this.getScreenGridState();
|
|
161
|
+
const step = Math.max(1, Math.round(secondGridStep || 0));
|
|
162
|
+
if (step <= 0) return;
|
|
163
|
+
try {
|
|
164
|
+
this.graphics.lineStyle({
|
|
165
|
+
width: 1,
|
|
166
|
+
color: this.bigGridColor,
|
|
167
|
+
alpha: this.bigGridOpacity,
|
|
168
|
+
alignment: 0
|
|
169
|
+
});
|
|
170
|
+
} catch (_) {
|
|
171
|
+
this.graphics.lineStyle(1, this.bigGridColor, this.bigGridOpacity);
|
|
172
|
+
}
|
|
173
|
+
const b = this.getDrawBounds();
|
|
174
|
+
const worldX = this.viewportTransform?.worldX || 0;
|
|
175
|
+
const worldY = this.viewportTransform?.worldY || 0;
|
|
176
|
+
const cursorX = this.viewportTransform?.zoomCursorX;
|
|
177
|
+
const cursorY = this.viewportTransform?.zoomCursorY;
|
|
178
|
+
const useCursorAnchor = this.viewportTransform?.useCursorAnchor === true;
|
|
179
|
+
const anchorX = this._resolveScreenAnchor('x', worldX, step, cursorX, useCursorAnchor, 'major');
|
|
180
|
+
const anchorY = this._resolveScreenAnchor('y', worldY, step, cursorY, useCursorAnchor, 'major');
|
|
181
|
+
const alignStart = (min, anchor) => {
|
|
182
|
+
const d = ((anchor - min) % step + step) % step;
|
|
183
|
+
return min + d;
|
|
184
|
+
};
|
|
185
|
+
const startX = alignStart(b.left, anchorX);
|
|
186
|
+
const endX = b.right + step;
|
|
187
|
+
const startY = alignStart(b.top, anchorY);
|
|
188
|
+
const endY = b.bottom + step;
|
|
189
|
+
for (let x = startX; x <= endX; x += step) {
|
|
190
|
+
const px = Math.round(x);
|
|
191
|
+
this.graphics.moveTo(px, b.top);
|
|
192
|
+
this.graphics.lineTo(px, b.bottom);
|
|
193
|
+
}
|
|
194
|
+
for (let y = startY; y <= endY; y += step) {
|
|
195
|
+
const py = Math.round(y);
|
|
196
|
+
this.graphics.moveTo(b.left, py);
|
|
197
|
+
this.graphics.lineTo(b.right, py);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_normalizeAnchor(anchor, stepPx) {
|
|
202
|
+
const step = Math.max(1, Math.round(stepPx));
|
|
203
|
+
return ((Math.round(anchor) % step) + step) % step;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_resolveScreenAnchor(axis, worldOffset, stepPx, cursorPx, useCursorAnchor, layer) {
|
|
207
|
+
const step = Math.max(1, Math.round(stepPx));
|
|
208
|
+
const anchorKey = `_${layer}Anchor${axis === 'x' ? 'X' : 'Y'}`;
|
|
209
|
+
const stepKey = `_${layer}Step${axis === 'x' ? 'X' : 'Y'}`;
|
|
210
|
+
const cursorOffsetKey = `_${layer}CursorOffset${axis === 'x' ? 'X' : 'Y'}`;
|
|
211
|
+
const raw = this._normalizeAnchor(getScreenAnchor(worldOffset, step), step);
|
|
212
|
+
if (useCursorAnchor && Number.isFinite(cursorPx)) {
|
|
213
|
+
const prevAnchor = Number(this[anchorKey]);
|
|
214
|
+
const prevStep = Math.max(1, Math.round(Number(this[stepKey]) || step));
|
|
215
|
+
if (Number.isFinite(prevAnchor)) {
|
|
216
|
+
this[cursorOffsetKey] = this._normalizeAnchor(Math.round(cursorPx) - prevAnchor, prevStep);
|
|
217
|
+
} else if (!Number.isFinite(this[cursorOffsetKey])) {
|
|
218
|
+
this[cursorOffsetKey] = 0;
|
|
219
|
+
}
|
|
220
|
+
const offset = this._normalizeAnchor(this[cursorOffsetKey], step);
|
|
221
|
+
const locked = this._normalizeAnchor(Math.round(cursorPx) - offset, step);
|
|
222
|
+
this[anchorKey] = locked;
|
|
223
|
+
this[stepKey] = step;
|
|
224
|
+
return locked;
|
|
225
|
+
}
|
|
226
|
+
this[anchorKey] = raw;
|
|
227
|
+
this[stepKey] = step;
|
|
228
|
+
return raw;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
getScreenGridState() {
|
|
232
|
+
return resolveLineGridState(this._zoom, {
|
|
233
|
+
minScreenSpacing: this.minScreenSpacing,
|
|
234
|
+
phases: this.screenPhases,
|
|
235
|
+
subGridDivisions: this.subGridDivisions,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
101
238
|
|
|
102
239
|
/**
|
|
103
240
|
* Вычисляет точку привязки для линейной сетки
|
|
@@ -134,7 +271,9 @@ export class LineGrid extends BaseGrid {
|
|
|
134
271
|
showSubGrid: this.showSubGrid,
|
|
135
272
|
subGridDivisions: this.subGridDivisions,
|
|
136
273
|
subGridColor: this.subGridColor,
|
|
137
|
-
subGridOpacity: this.subGridOpacity
|
|
274
|
+
subGridOpacity: this.subGridOpacity,
|
|
275
|
+
bigGridColor: this.bigGridColor,
|
|
276
|
+
bigGridOpacity: this.bigGridOpacity
|
|
138
277
|
};
|
|
139
278
|
}
|
|
140
279
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { resolveScreenGridState } from './ScreenGridPhaseMachine.js';
|
|
2
|
+
|
|
3
|
+
function clampZoom(zoom) {
|
|
4
|
+
return Math.max(0.01, Math.min(5, zoom || 1));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const MAJOR_SCREEN_POINTS = [
|
|
8
|
+
{ zoomPercent: 1.4, majorPx: 17 },
|
|
9
|
+
{ zoomPercent: 1.6, majorPx: 20 },
|
|
10
|
+
{ zoomPercent: 1.8, majorPx: 22 },
|
|
11
|
+
{ zoomPercent: 2.0, majorPx: 25 },
|
|
12
|
+
{ zoomPercent: 2.2, majorPx: 27 },
|
|
13
|
+
{ zoomPercent: 2.4, majorPx: 31 },
|
|
14
|
+
{ zoomPercent: 2.6, majorPx: 35 },
|
|
15
|
+
{ zoomPercent: 3.0, majorPx: 39 },
|
|
16
|
+
{ zoomPercent: 3.4, majorPx: 44 },
|
|
17
|
+
{ zoomPercent: 3.6, majorPx: 49 },
|
|
18
|
+
{ zoomPercent: 4.0, majorPx: 55 },
|
|
19
|
+
{ zoomPercent: 4.6, majorPx: 60 }, // label 5 (second pass), single grid
|
|
20
|
+
{ zoomPercent: 5.4, majorPx: 17 }, // label 5 (first pass), double grid
|
|
21
|
+
{ zoomPercent: 6.0, majorPx: 20 },
|
|
22
|
+
{ zoomPercent: 7.0, majorPx: 22 },
|
|
23
|
+
{ zoomPercent: 7.6, majorPx: 24 }, // label 8 (second pass)
|
|
24
|
+
{ zoomPercent: 8.4, majorPx: 27 }, // label 8 (first pass)
|
|
25
|
+
{ zoomPercent: 10.0, majorPx: 30 },
|
|
26
|
+
{ zoomPercent: 11.0, majorPx: 34 },
|
|
27
|
+
{ zoomPercent: 12.0, majorPx: 38 },
|
|
28
|
+
{ zoomPercent: 13.0, majorPx: 42 },
|
|
29
|
+
{ zoomPercent: 15.0, majorPx: 48 },
|
|
30
|
+
{ zoomPercent: 17.0, majorPx: 54 },
|
|
31
|
+
{ zoomPercent: 19.0, majorPx: 60 }, // single grid
|
|
32
|
+
{ zoomPercent: 21.0, majorPx: 17 },
|
|
33
|
+
{ zoomPercent: 24.0, majorPx: 19 },
|
|
34
|
+
{ zoomPercent: 26.0, majorPx: 21 },
|
|
35
|
+
{ zoomPercent: 30.0, majorPx: 24 },
|
|
36
|
+
{ zoomPercent: 33.0, majorPx: 26 },
|
|
37
|
+
{ zoomPercent: 37.0, majorPx: 30 },
|
|
38
|
+
{ zoomPercent: 41.0, majorPx: 33 },
|
|
39
|
+
{ zoomPercent: 46.0, majorPx: 37 },
|
|
40
|
+
{ zoomPercent: 52.0, majorPx: 42 },
|
|
41
|
+
{ zoomPercent: 58.0, majorPx: 46 },
|
|
42
|
+
{ zoomPercent: 65.0, majorPx: 52 },
|
|
43
|
+
{ zoomPercent: 73, majorPx: 60 },
|
|
44
|
+
{ zoomPercent: 82, majorPx: 16 },
|
|
45
|
+
{ zoomPercent: 92, majorPx: 18 },
|
|
46
|
+
{ zoomPercent: 103, majorPx: 20 },
|
|
47
|
+
{ zoomPercent: 115, majorPx: 23 },
|
|
48
|
+
{ zoomPercent: 129, majorPx: 25 },
|
|
49
|
+
{ zoomPercent: 144, majorPx: 30 },
|
|
50
|
+
{ zoomPercent: 162, majorPx: 35 },
|
|
51
|
+
{ zoomPercent: 181, majorPx: 35 },
|
|
52
|
+
{ zoomPercent: 203, majorPx: 40 },
|
|
53
|
+
{ zoomPercent: 227, majorPx: 45 },
|
|
54
|
+
{ zoomPercent: 254, majorPx: 50 },
|
|
55
|
+
{ zoomPercent: 285, majorPx: 60 },
|
|
56
|
+
{ zoomPercent: 319, majorPx: 64 },
|
|
57
|
+
{ zoomPercent: 357, majorPx: 70 },
|
|
58
|
+
{ zoomPercent: 400, majorPx: 80 },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function interpolateMajorStep(zoomPercent) {
|
|
62
|
+
if (zoomPercent <= MAJOR_SCREEN_POINTS[0].zoomPercent) {
|
|
63
|
+
const ratio = zoomPercent / MAJOR_SCREEN_POINTS[0].zoomPercent;
|
|
64
|
+
return Math.max(1, Math.round(MAJOR_SCREEN_POINTS[0].majorPx * ratio));
|
|
65
|
+
}
|
|
66
|
+
if (zoomPercent >= MAJOR_SCREEN_POINTS[MAJOR_SCREEN_POINTS.length - 1].zoomPercent) {
|
|
67
|
+
const ratio = zoomPercent / MAJOR_SCREEN_POINTS[MAJOR_SCREEN_POINTS.length - 1].zoomPercent;
|
|
68
|
+
return Math.max(1, Math.round(MAJOR_SCREEN_POINTS[MAJOR_SCREEN_POINTS.length - 1].majorPx * ratio));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < MAJOR_SCREEN_POINTS.length - 1; i += 1) {
|
|
72
|
+
const a = MAJOR_SCREEN_POINTS[i];
|
|
73
|
+
const b = MAJOR_SCREEN_POINTS[i + 1];
|
|
74
|
+
if (zoomPercent >= a.zoomPercent && zoomPercent <= b.zoomPercent) {
|
|
75
|
+
const t = (zoomPercent - a.zoomPercent) / (b.zoomPercent - a.zoomPercent);
|
|
76
|
+
const value = a.majorPx + (b.majorPx - a.majorPx) * t;
|
|
77
|
+
return Math.max(1, Math.round(value));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Math.max(1, Math.round(resolveScreenGridState(zoomPercent / 100).screenStep));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeZoomPercent(zoomPercent) {
|
|
84
|
+
return Math.round(zoomPercent * 10) / 10;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const EXACT_MAJOR_BY_PERCENT = new Map(
|
|
88
|
+
MAJOR_SCREEN_POINTS.map((row) => [normalizeZoomPercent(row.zoomPercent), row.majorPx])
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Профиль line-grid под наблюдаемые checkpoint'ы из MIRO_LINE_GRID.md.
|
|
93
|
+
* Возвращает major/minor screen-step и видимость подсетки.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveLineGridState(zoom, options = {}) {
|
|
96
|
+
const z = clampZoom(zoom);
|
|
97
|
+
const base = resolveScreenGridState(z, options);
|
|
98
|
+
const zoomPercent = z * 100;
|
|
99
|
+
|
|
100
|
+
const roundedPercent = Math.round(zoomPercent);
|
|
101
|
+
const normalizedPercent = normalizeZoomPercent(zoomPercent);
|
|
102
|
+
let majorScreenStep = EXACT_MAJOR_BY_PERCENT.get(normalizedPercent) ?? interpolateMajorStep(zoomPercent);
|
|
103
|
+
const minorScreenStep = null;
|
|
104
|
+
const showSubGridByZoom = false;
|
|
105
|
+
const hasSecondGridByRoundedPercent =
|
|
106
|
+
roundedPercent === 254 ||
|
|
107
|
+
roundedPercent === 227 ||
|
|
108
|
+
roundedPercent === 203 ||
|
|
109
|
+
roundedPercent === 181 ||
|
|
110
|
+
roundedPercent === 162 ||
|
|
111
|
+
roundedPercent === 144 ||
|
|
112
|
+
roundedPercent === 129 ||
|
|
113
|
+
roundedPercent === 115 ||
|
|
114
|
+
roundedPercent === 103 ||
|
|
115
|
+
roundedPercent === 92 ||
|
|
116
|
+
roundedPercent === 82 ||
|
|
117
|
+
roundedPercent === 65 ||
|
|
118
|
+
roundedPercent === 58 ||
|
|
119
|
+
roundedPercent === 52 ||
|
|
120
|
+
roundedPercent === 46 ||
|
|
121
|
+
roundedPercent === 41 ||
|
|
122
|
+
roundedPercent === 37;
|
|
123
|
+
const hasSecondGridByExactPercent =
|
|
124
|
+
normalizedPercent === 33.0 ||
|
|
125
|
+
normalizedPercent === 30.0 ||
|
|
126
|
+
normalizedPercent === 26.0 ||
|
|
127
|
+
normalizedPercent === 24.0 ||
|
|
128
|
+
normalizedPercent === 21.0 ||
|
|
129
|
+
normalizedPercent === 17.0 ||
|
|
130
|
+
normalizedPercent === 15.0 ||
|
|
131
|
+
normalizedPercent === 13.0 ||
|
|
132
|
+
normalizedPercent === 12.0 ||
|
|
133
|
+
normalizedPercent === 11.0 ||
|
|
134
|
+
normalizedPercent === 10.0 ||
|
|
135
|
+
normalizedPercent === 8.4 ||
|
|
136
|
+
normalizedPercent === 7.6 ||
|
|
137
|
+
normalizedPercent === 7.0 ||
|
|
138
|
+
normalizedPercent === 6.0 ||
|
|
139
|
+
normalizedPercent === 5.4 ||
|
|
140
|
+
normalizedPercent === 65.0 ||
|
|
141
|
+
normalizedPercent === 58.0 ||
|
|
142
|
+
normalizedPercent === 52.0 ||
|
|
143
|
+
normalizedPercent === 46.0 ||
|
|
144
|
+
normalizedPercent === 41.0 ||
|
|
145
|
+
normalizedPercent === 37.0;
|
|
146
|
+
const hasSecondGrid = hasSecondGridByRoundedPercent || hasSecondGridByExactPercent;
|
|
147
|
+
const hasSecondGridForUltraLowZoom = normalizedPercent < 5.0 && normalizedPercent !== 4.6;
|
|
148
|
+
const secondGridStep = (roundedPercent === 100 || hasSecondGrid)
|
|
149
|
+
|| hasSecondGridForUltraLowZoom
|
|
150
|
+
? Math.max(1, Math.round(majorScreenStep * 4))
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
...base,
|
|
155
|
+
screenStep: majorScreenStep,
|
|
156
|
+
majorScreenStep,
|
|
157
|
+
minorScreenStep,
|
|
158
|
+
secondGridStep,
|
|
159
|
+
showSubGridByZoom,
|
|
160
|
+
subDivisions: 0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Общая state machine для screen-grid.
|
|
3
|
+
* Возвращает world-step активной фазы и screen-step (px) для текущего zoom.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {{ zoomMin: number, zoomMax: number, worldStep: number }[]} */
|
|
7
|
+
const DEFAULT_PHASES = [
|
|
8
|
+
{ zoomMin: 0.02, zoomMax: 0.12, worldStep: 160 },
|
|
9
|
+
{ zoomMin: 0.12, zoomMax: 0.25, worldStep: 80 },
|
|
10
|
+
{ zoomMin: 0.25, zoomMax: 0.5, worldStep: 40 },
|
|
11
|
+
{ zoomMin: 0.5, zoomMax: 5, worldStep: 20 },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function clampZoom(zoom) {
|
|
15
|
+
return Math.max(0.02, Math.min(5, zoom || 1));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pickPhase(zoom, phases = DEFAULT_PHASES) {
|
|
19
|
+
const z = clampZoom(zoom);
|
|
20
|
+
for (let i = 0; i < phases.length; i++) {
|
|
21
|
+
const phase = phases[i];
|
|
22
|
+
const inRange = i === phases.length - 1
|
|
23
|
+
? (z >= phase.zoomMin && z <= phase.zoomMax)
|
|
24
|
+
: (z >= phase.zoomMin && z < phase.zoomMax);
|
|
25
|
+
if (inRange) return phase;
|
|
26
|
+
}
|
|
27
|
+
return phases[phases.length - 1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveScreenGridState(zoom, options = {}) {
|
|
31
|
+
const z = clampZoom(zoom);
|
|
32
|
+
const phases = options.phases || DEFAULT_PHASES;
|
|
33
|
+
const minScreenSpacing = options.minScreenSpacing ?? 8;
|
|
34
|
+
const phase = pickPhase(z, phases);
|
|
35
|
+
const worldStep = phase.worldStep;
|
|
36
|
+
const screenStep = Math.max(minScreenSpacing, worldStep * z);
|
|
37
|
+
return { zoom: z, phase, worldStep, screenStep };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getScreenAnchor(worldOffset, stepPx) {
|
|
41
|
+
const step = Math.max(1e-6, stepPx || 1);
|
|
42
|
+
const mod = worldOffset % step;
|
|
43
|
+
return mod < 0 ? mod + step : mod;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function snapScreenValue(valuePx, anchorPx, stepPx) {
|
|
47
|
+
const step = Math.max(1e-6, stepPx || 1);
|
|
48
|
+
return Math.round((valuePx - anchorPx) / step) * step + anchorPx;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { DEFAULT_PHASES };
|