@sequent-org/moodboard 1.3.5 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +26 -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 +716 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +6 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +2 -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 +239 -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/DotGrid.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { BaseGrid } from './BaseGrid.js';
|
|
2
|
-
import { getActivePhases, getEffectiveSize } from './DotGridZoomPhases.js';
|
|
2
|
+
import { getActivePhases, getDotCheckpointForZoom, getDotColor, getEffectiveSize, getRawScreenDotRadius, getScreenDotRadius, getScreenSpacing } from './DotGridZoomPhases.js';
|
|
3
|
+
import { getScreenAnchor } from './ScreenGridPhaseMachine.js';
|
|
4
|
+
|
|
5
|
+
/** Checkpoint zoomPercent: 1px dot вместо круга */
|
|
6
|
+
const DOT_1PX_CHECKPOINTS = new Set([7, 7.6, 8.4, 15, 17, 19, 20, 21, 24, 26, 30, 37, 41, 46]);
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Точечная сетка с фазовым переключением при зуме (как в Miro).
|
|
@@ -14,8 +18,7 @@ export class DotGrid extends BaseGrid {
|
|
|
14
18
|
this.highlightIntersections = options.highlightIntersections ?? true;
|
|
15
19
|
this.dotSize = options.dotSize ?? 1; // для serialize/setDotSize; фазы переопределяют при отрисовке
|
|
16
20
|
this.snapSize = options.snapSize ?? 20; // фиксированный шаг привязки в world-координатах
|
|
17
|
-
|
|
18
|
-
this.minScreenDotRadius = options.minScreenDotRadius ?? 0.45;
|
|
21
|
+
this.minScreenDotRadius = options.minScreenDotRadius ?? 0;
|
|
19
22
|
this.minScreenSpacing = options.minScreenSpacing ?? 8;
|
|
20
23
|
this.maxDotsPerPhase = options.maxDotsPerPhase ?? 25000;
|
|
21
24
|
this.intersectionSize = options.intersectionSize ?? this.dotSize;
|
|
@@ -23,6 +26,21 @@ export class DotGrid extends BaseGrid {
|
|
|
23
26
|
|
|
24
27
|
/** Текущий zoom (scale) для выбора фазы. 1 = 100%. */
|
|
25
28
|
this._zoom = 1;
|
|
29
|
+
|
|
30
|
+
// DotGrid всегда непрозрачный.
|
|
31
|
+
this.opacity = 1;
|
|
32
|
+
this.graphics.alpha = 1;
|
|
33
|
+
|
|
34
|
+
// Cursor-centric anchor-контракт: при зуме точка под курсором фиксируется.
|
|
35
|
+
this._anchorX = null;
|
|
36
|
+
this._anchorY = null;
|
|
37
|
+
this._lastStepPxX = null;
|
|
38
|
+
this._lastStepPxY = null;
|
|
39
|
+
this._cursorOffsetX = 0;
|
|
40
|
+
this._cursorOffsetY = 0;
|
|
41
|
+
|
|
42
|
+
// Временный debug-режим: разрешает дробный радиус для тонкой подстройки.
|
|
43
|
+
this._allowFloatDotRadius = false;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/**
|
|
@@ -49,7 +67,7 @@ export class DotGrid extends BaseGrid {
|
|
|
49
67
|
createVisual() {
|
|
50
68
|
this.size = this._getEffectiveSize();
|
|
51
69
|
const phases = this._getActivePhases();
|
|
52
|
-
const baseOpacity =
|
|
70
|
+
const baseOpacity = 1;
|
|
53
71
|
for (const { phase, alpha } of phases) {
|
|
54
72
|
this._drawPhaseDots(phase, baseOpacity * alpha);
|
|
55
73
|
}
|
|
@@ -60,41 +78,79 @@ export class DotGrid extends BaseGrid {
|
|
|
60
78
|
*/
|
|
61
79
|
_drawPhaseDots(phase, alpha) {
|
|
62
80
|
const b = this.getDrawBounds();
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const ny = Math.floor(heightWorld / size) + 3;
|
|
73
|
-
return nx * ny;
|
|
74
|
-
};
|
|
75
|
-
const dots = estimateDots();
|
|
76
|
-
if (dots > this.maxDotsPerPhase) {
|
|
77
|
-
const densityFactor = Math.sqrt(dots / this.maxDotsPerPhase);
|
|
78
|
-
size *= Math.max(1, densityFactor);
|
|
79
|
-
}
|
|
81
|
+
const stepPx = Math.max(1, Math.round(getScreenSpacing(this._zoom)));
|
|
82
|
+
|
|
83
|
+
const worldX = this.viewportTransform?.worldX || 0;
|
|
84
|
+
const worldY = this.viewportTransform?.worldY || 0;
|
|
85
|
+
const cursorX = this.viewportTransform?.zoomCursorX;
|
|
86
|
+
const cursorY = this.viewportTransform?.zoomCursorY;
|
|
87
|
+
const useCursorAnchor = this.viewportTransform?.useCursorAnchor === true;
|
|
88
|
+
const anchorX = this._resolveScreenAnchor('x', worldX, stepPx, cursorX, useCursorAnchor);
|
|
89
|
+
const anchorY = this._resolveScreenAnchor('y', worldY, stepPx, cursorY, useCursorAnchor);
|
|
80
90
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
const alignStart = (min, anchor, step) => {
|
|
92
|
+
const minInt = Math.round(min);
|
|
93
|
+
const d = ((anchor - minInt) % step + step) % step;
|
|
94
|
+
return minInt + d;
|
|
95
|
+
};
|
|
96
|
+
const startX = alignStart(b.left, anchorX, stepPx);
|
|
97
|
+
const startY = alignStart(b.top, anchorY, stepPx);
|
|
98
|
+
const endX = Math.round(b.right) + stepPx;
|
|
99
|
+
const endY = Math.round(b.bottom) + stepPx;
|
|
85
100
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
const rawDotSize = this._allowFloatDotRadius
|
|
102
|
+
? getRawScreenDotRadius(this._zoom, this.minScreenDotRadius)
|
|
103
|
+
: getScreenDotRadius(this._zoom, this.minScreenDotRadius);
|
|
104
|
+
const checkpoint = getDotCheckpointForZoom(this._zoom);
|
|
105
|
+
const zoomPct = checkpoint?.zoomPercent ?? 100;
|
|
106
|
+
const use1px = DOT_1PX_CHECKPOINTS.has(zoomPct);
|
|
107
|
+
const dotSize = use1px ? 0.4 : rawDotSize;
|
|
108
|
+
const dotColor = getDotColor(this._zoom, this.color);
|
|
89
109
|
|
|
90
|
-
this.graphics.beginFill(
|
|
91
|
-
for (let x = startX; x <= endX; x +=
|
|
92
|
-
for (let y = startY; y <= endY; y +=
|
|
110
|
+
this.graphics.beginFill(dotColor, alpha);
|
|
111
|
+
for (let x = startX; x <= endX; x += stepPx) {
|
|
112
|
+
for (let y = startY; y <= endY; y += stepPx) {
|
|
93
113
|
this.drawDot(x, y, dotSize);
|
|
94
114
|
}
|
|
95
115
|
}
|
|
96
116
|
this.graphics.endFill();
|
|
97
117
|
}
|
|
118
|
+
|
|
119
|
+
_normalizeAnchor(anchor, stepPx) {
|
|
120
|
+
const step = Math.max(1, Math.round(stepPx));
|
|
121
|
+
const normalized = ((Math.round(anchor) % step) + step) % step;
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_resolveScreenAnchor(axis, worldOffset, stepPx, cursorPx, useCursorAnchor) {
|
|
126
|
+
const anchorKey = axis === 'x' ? '_anchorX' : '_anchorY';
|
|
127
|
+
const lastStepKey = axis === 'x' ? '_lastStepPxX' : '_lastStepPxY';
|
|
128
|
+
const cursorOffsetKey = axis === 'x' ? '_cursorOffsetX' : '_cursorOffsetY';
|
|
129
|
+
const step = Math.max(1, Math.round(stepPx));
|
|
130
|
+
const rawAnchor = this._normalizeAnchor(getScreenAnchor(worldOffset, step), step);
|
|
131
|
+
|
|
132
|
+
// Во время cursor-centric zoom привязываем сетку к экранной позиции курсора.
|
|
133
|
+
// Сохраняем относительное смещение курсор↔узел, чтобы не было "прилипания"
|
|
134
|
+
// узла к курсору, если курсор стоял в пустом месте.
|
|
135
|
+
if (useCursorAnchor && Number.isFinite(cursorPx)) {
|
|
136
|
+
const prevAnchor = Number(this[anchorKey]);
|
|
137
|
+
const prevStep = Math.max(1, Math.round(Number(this[lastStepKey]) || step));
|
|
138
|
+
if (Number.isFinite(prevAnchor)) {
|
|
139
|
+
this[cursorOffsetKey] = this._normalizeAnchor(Math.round(cursorPx) - prevAnchor, prevStep);
|
|
140
|
+
} else if (!Number.isFinite(this[cursorOffsetKey])) {
|
|
141
|
+
this[cursorOffsetKey] = 0;
|
|
142
|
+
}
|
|
143
|
+
const offset = this._normalizeAnchor(this[cursorOffsetKey], step);
|
|
144
|
+
const cursorAnchor = this._normalizeAnchor(Math.round(cursorPx) - offset, step);
|
|
145
|
+
this[anchorKey] = cursorAnchor;
|
|
146
|
+
this[lastStepKey] = step;
|
|
147
|
+
return cursorAnchor;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this[anchorKey] = rawAnchor;
|
|
151
|
+
this[lastStepKey] = step;
|
|
152
|
+
return rawAnchor;
|
|
153
|
+
}
|
|
98
154
|
|
|
99
155
|
/**
|
|
100
156
|
* Рисует выделенные пересечения (каждые 5 точек)
|
|
@@ -119,11 +175,20 @@ export class DotGrid extends BaseGrid {
|
|
|
119
175
|
* Рисует одну точку
|
|
120
176
|
*/
|
|
121
177
|
drawDot(x, y, size) {
|
|
178
|
+
const px = Math.round(x);
|
|
179
|
+
const py = Math.round(y);
|
|
180
|
+
const r = this._allowFloatDotRadius
|
|
181
|
+
? Math.max(0, Number(size) || 0)
|
|
182
|
+
: Math.max(0, Math.round(size));
|
|
183
|
+
if (r < 1) {
|
|
184
|
+
this.graphics.drawRect(px, py, 1, 1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
122
187
|
if (this.dotStyle === 'circle') {
|
|
123
|
-
this.graphics.drawCircle(
|
|
188
|
+
this.graphics.drawCircle(px, py, r);
|
|
124
189
|
} else if (this.dotStyle === 'square') {
|
|
125
|
-
const half =
|
|
126
|
-
this.graphics.drawRect(
|
|
190
|
+
const half = r;
|
|
191
|
+
this.graphics.drawRect(px - half, py - half, half * 2, half * 2);
|
|
127
192
|
}
|
|
128
193
|
}
|
|
129
194
|
|
|
@@ -177,6 +242,24 @@ export class DotGrid extends BaseGrid {
|
|
|
177
242
|
this.updateVisual();
|
|
178
243
|
}
|
|
179
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Временная debug-настройка дробного радиуса.
|
|
247
|
+
* @param {boolean} enabled
|
|
248
|
+
*/
|
|
249
|
+
setAllowFloatDotRadius(enabled) {
|
|
250
|
+
this._allowFloatDotRadius = enabled === true;
|
|
251
|
+
this.updateVisual();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* DotGrid всегда непрозрачный: внешний вызов setOpacity игнорируем.
|
|
256
|
+
*/
|
|
257
|
+
setOpacity() {
|
|
258
|
+
this.opacity = 1;
|
|
259
|
+
this.graphics.alpha = 1;
|
|
260
|
+
this.updateVisual();
|
|
261
|
+
}
|
|
262
|
+
|
|
180
263
|
// Делаем цвет пересечений таким же, как основной цвет
|
|
181
264
|
setColor(color) {
|
|
182
265
|
super.setColor(color);
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Логика фаз точечной сетки при зуме (чистые функции, тестируемые без PIXI).
|
|
3
|
-
* Фазы подобраны по зафиксированным checkpoint'ам Miro:
|
|
4
|
-
* - high zoom: базовый шаг 20 world units;
|
|
5
|
-
* - low zoom: шаг укрупняется, чтобы избежать перегруза при отрисовке.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
5
|
/** @type {{ zoomMin: number, zoomMax: number, size: number, dotSize: number }[]} */
|
|
@@ -10,26 +7,130 @@ export const PHASES = [
|
|
|
10
7
|
{ zoomMin: 0.02, zoomMax: 0.12, size: 160, dotSize: 0.7 },
|
|
11
8
|
{ zoomMin: 0.12, zoomMax: 0.25, size: 80, dotSize: 0.8 },
|
|
12
9
|
{ zoomMin: 0.25, zoomMax: 0.5, size: 40, dotSize: 0.9 },
|
|
13
|
-
{ zoomMin: 0.5, zoomMax: 5, size: 20, dotSize: 1 }
|
|
10
|
+
{ zoomMin: 0.5, zoomMax: 5, size: 20, dotSize: 1 },
|
|
14
11
|
];
|
|
15
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Дискретный профиль dot-grid по zoom checkpoint'ам:
|
|
15
|
+
* на каждом шаге фиксированы spacing / dotRadius / color.
|
|
16
|
+
*/
|
|
17
|
+
export const DOT_CHECKPOINTS = [
|
|
18
|
+
{ zoomPercent: 10, spacing: 16, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
19
|
+
{ zoomPercent: 11, spacing: 16, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
20
|
+
{ zoomPercent: 12, spacing: 18, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
21
|
+
{ zoomPercent: 13, spacing: 10, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
22
|
+
{ zoomPercent: 15, spacing: 12, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
23
|
+
{ zoomPercent: 17, spacing: 14, dotRadius: 1, color: 0xA3A3A3 },
|
|
24
|
+
{ zoomPercent: 19, spacing: 16, dotRadius: 1, color: 0xA3A3A3 },
|
|
25
|
+
{ zoomPercent: 20, spacing: 16, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
26
|
+
{ zoomPercent: 21, spacing: 18, dotRadius: 1, color: 0xA3A3A3 },
|
|
27
|
+
{ zoomPercent: 24, spacing: 20, dotRadius: 1, color: 0xA3A3A3 },
|
|
28
|
+
{ zoomPercent: 26, spacing: 16, dotRadius: 1, color: 0xA3A3A3 },
|
|
29
|
+
{ zoomPercent: 30, spacing: 12, dotRadius: 1, color: 0xA3A3A3 },
|
|
30
|
+
{ zoomPercent: 33, spacing: 13, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
31
|
+
{ zoomPercent: 37, spacing: 16, dotRadius: 1, color: 0xA3A3A3 },
|
|
32
|
+
{ zoomPercent: 41, spacing: 18, dotRadius: 1, color: 0xA3A3A3 },
|
|
33
|
+
{ zoomPercent: 46, spacing: 20, dotRadius: 1, color: 0xA3A3A3 },
|
|
34
|
+
{ zoomPercent: 50, spacing: 10, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
35
|
+
{ zoomPercent: 52, spacing: 11, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
36
|
+
{ zoomPercent: 58, spacing: 12, dotRadius: 1, color: 0xE8E8E8 },
|
|
37
|
+
{ zoomPercent: 1.4, spacing: 16, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
38
|
+
{ zoomPercent: 1.6, spacing: 18, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
39
|
+
{ zoomPercent: 1.8, spacing: 8, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
40
|
+
{ zoomPercent: 2.0, spacing: 10, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
41
|
+
{ zoomPercent: 2.2, spacing: 12, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
42
|
+
{ zoomPercent: 2.4, spacing: 14, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
43
|
+
{ zoomPercent: 2.6, spacing: 16, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
44
|
+
{ zoomPercent: 3.0, spacing: 18, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
45
|
+
{ zoomPercent: 3.4, spacing: 8, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
46
|
+
{ zoomPercent: 3.6, spacing: 10, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
47
|
+
{ zoomPercent: 4.0, spacing: 12, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
48
|
+
{ zoomPercent: 4.6, spacing: 14, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
49
|
+
{ zoomPercent: 5.4, spacing: 16, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
50
|
+
{ zoomPercent: 6.0, spacing: 18, dotRadius: 0.5, color: 0xA3A3A3 },
|
|
51
|
+
{ zoomPercent: 7.0, spacing: 8, dotRadius: 1, color: 0xA3A3A3 },
|
|
52
|
+
{ zoomPercent: 7.6, spacing: 10, dotRadius: 1, color: 0xA3A3A3 },
|
|
53
|
+
{ zoomPercent: 8.4, spacing: 12, dotRadius: 1, color: 0xA3A3A3 },
|
|
54
|
+
{ zoomPercent: 73, spacing: 15, dotRadius: 1, color: 0xE8E8E8 },
|
|
55
|
+
{ zoomPercent: 75, spacing: 15, dotRadius: 1, color: 0xE8E8E8 },
|
|
56
|
+
{ zoomPercent: 82, spacing: 16, dotRadius: 1, color: 0xE8E8E8 },
|
|
57
|
+
{ zoomPercent: 92, spacing: 18, dotRadius: 1.2, color: 0xE8E8E8 },
|
|
58
|
+
{ zoomPercent: 100, spacing: 20, dotRadius: 1, color: 0xE8E8E8 },
|
|
59
|
+
{ zoomPercent: 103, spacing: 21, dotRadius: 1.3, color: 0xE8E8E8 },
|
|
60
|
+
{ zoomPercent: 115, spacing: 23, dotRadius: 1.4, color: 0xE8E8E8 },
|
|
61
|
+
{ zoomPercent: 125, spacing: 25, dotRadius: 1, color: 0xE8E8E8 },
|
|
62
|
+
{ zoomPercent: 129, spacing: 26, dotRadius: 1.5, color: 0xE3E3E3 },
|
|
63
|
+
{ zoomPercent: 144, spacing: 29, dotRadius: 1.7, color: 0xE7E7E7 },
|
|
64
|
+
{ zoomPercent: 150, spacing: 30, dotRadius: 2, color: 0xE8E8E8 },
|
|
65
|
+
{ zoomPercent: 162, spacing: 32, dotRadius: 1.5, color: 0xE5E5E5 },
|
|
66
|
+
{ zoomPercent: 181, spacing: 36, dotRadius: 2, color: 0xE2E2E2 },
|
|
67
|
+
{ zoomPercent: 200, spacing: 40, dotRadius: 2, color: 0xE8E8E8 },
|
|
68
|
+
{ zoomPercent: 250, spacing: 50, dotRadius: 2, color: 0xE8E8E8 },
|
|
69
|
+
{ zoomPercent: 300, spacing: 60, dotRadius: 3, color: 0xE8E8E8 },
|
|
70
|
+
{ zoomPercent: 400, spacing: 80, dotRadius: 4, color: 0xE8E8E8 },
|
|
71
|
+
{ zoomPercent: 500, spacing: 100, dotRadius: 5, color: 0xE8E8E8 },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function clampZoom(zoom) {
|
|
75
|
+
return Math.max(0.02, Math.min(5, zoom || 1));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Для DotGrid границы фаз выбираются по "нижней" фазе (включительно),
|
|
80
|
+
* чтобы на 50% происходил ожидаемый визуальный скачок.
|
|
81
|
+
*/
|
|
82
|
+
function resolveDotPhase(zoom) {
|
|
83
|
+
const z = clampZoom(zoom);
|
|
84
|
+
for (const phase of PHASES) {
|
|
85
|
+
if (z >= phase.zoomMin && z <= phase.zoomMax) {
|
|
86
|
+
return phase;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return PHASES[PHASES.length - 1];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveDotCheckpoint(zoom) {
|
|
93
|
+
const z = clampZoom(zoom);
|
|
94
|
+
const p = z * 100;
|
|
95
|
+
let best = DOT_CHECKPOINTS[0];
|
|
96
|
+
let bestDist = Math.abs(best.zoomPercent - p);
|
|
97
|
+
for (let i = 1; i < DOT_CHECKPOINTS.length; i += 1) {
|
|
98
|
+
const row = DOT_CHECKPOINTS[i];
|
|
99
|
+
const dist = Math.abs(row.zoomPercent - p);
|
|
100
|
+
if (dist < bestDist) {
|
|
101
|
+
best = row;
|
|
102
|
+
bestDist = dist;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return best;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sanitizeColor(color, fallback = 0xE8E8E8) {
|
|
109
|
+
const n = Number(color);
|
|
110
|
+
if (!Number.isFinite(n)) return fallback;
|
|
111
|
+
return Math.max(0, Math.min(0xFFFFFF, Math.round(n)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sanitizeSpacing(spacing, fallback = 20) {
|
|
115
|
+
const n = Number(spacing);
|
|
116
|
+
if (!Number.isFinite(n)) return fallback;
|
|
117
|
+
return Math.max(1, Math.round(n));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sanitizeDotRadius(radius, fallback = 1) {
|
|
121
|
+
const n = Number(radius);
|
|
122
|
+
if (!Number.isFinite(n)) return fallback;
|
|
123
|
+
return Math.max(0, n);
|
|
124
|
+
}
|
|
125
|
+
|
|
16
126
|
/**
|
|
17
127
|
* Возвращает активные фазы и их alpha для crossfade.
|
|
18
128
|
* @param {number} zoom - world.scale.x (1 = 100%)
|
|
19
129
|
* @returns {Array<{ phase: object, alpha: number }>}
|
|
20
130
|
*/
|
|
21
131
|
export function getActivePhases(zoom) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const phase = PHASES[i];
|
|
25
|
-
const inRange = i === PHASES.length - 1
|
|
26
|
-
? (z >= phase.zoomMin && z <= phase.zoomMax)
|
|
27
|
-
: (z >= phase.zoomMin && z < phase.zoomMax);
|
|
28
|
-
if (inRange) {
|
|
29
|
-
return [{ phase, alpha: 1 }];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return [{ phase: PHASES[PHASES.length - 1], alpha: 1 }];
|
|
132
|
+
const phase = resolveDotPhase(zoom);
|
|
133
|
+
return [{ phase, alpha: 1 }];
|
|
33
134
|
}
|
|
34
135
|
|
|
35
136
|
/**
|
|
@@ -38,5 +139,102 @@ export function getActivePhases(zoom) {
|
|
|
38
139
|
* @returns {number}
|
|
39
140
|
*/
|
|
40
141
|
export function getEffectiveSize(zoom) {
|
|
41
|
-
return
|
|
142
|
+
return resolveDotPhase(zoom).size;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Возвращает текущий checkpoint (копию) для zoom.
|
|
147
|
+
* @param {number} zoom
|
|
148
|
+
* @returns {{ zoomPercent:number, spacing:number, dotRadius:number, color:number }}
|
|
149
|
+
*/
|
|
150
|
+
export function getDotCheckpointForZoom(zoom) {
|
|
151
|
+
const row = resolveDotCheckpoint(zoom);
|
|
152
|
+
return {
|
|
153
|
+
zoomPercent: row.zoomPercent,
|
|
154
|
+
spacing: row.spacing,
|
|
155
|
+
dotRadius: row.dotRadius,
|
|
156
|
+
color: row.color,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Обновляет checkpoint для заданного zoomPercent.
|
|
162
|
+
* Используется debug-панелью для live-настройки.
|
|
163
|
+
* @param {number} zoomPercent
|
|
164
|
+
* @param {{ spacing?: number, dotRadius?: number, color?: number }} patch
|
|
165
|
+
* @returns {{ zoomPercent:number, spacing:number, dotRadius:number, color:number } | null}
|
|
166
|
+
*/
|
|
167
|
+
export function updateDotCheckpoint(zoomPercent, patch = {}) {
|
|
168
|
+
const target = Number(zoomPercent);
|
|
169
|
+
if (!Number.isFinite(target)) return null;
|
|
170
|
+
const row = DOT_CHECKPOINTS.find((r) => Math.abs(r.zoomPercent - target) < 1e-6);
|
|
171
|
+
if (!row) return null;
|
|
172
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'spacing')) {
|
|
173
|
+
row.spacing = sanitizeSpacing(patch.spacing, row.spacing);
|
|
174
|
+
}
|
|
175
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'dotRadius')) {
|
|
176
|
+
row.dotRadius = sanitizeDotRadius(patch.dotRadius, row.dotRadius);
|
|
177
|
+
}
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'color')) {
|
|
179
|
+
row.color = sanitizeColor(patch.color, row.color);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
zoomPercent: row.zoomPercent,
|
|
183
|
+
spacing: row.spacing,
|
|
184
|
+
dotRadius: row.dotRadius,
|
|
185
|
+
color: row.color,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Возвращает screen-spacing в px для текущего zoom.
|
|
191
|
+
* @param {number} zoom
|
|
192
|
+
* @param {number} minScreenSpacing
|
|
193
|
+
* @returns {number}
|
|
194
|
+
*/
|
|
195
|
+
export function getScreenSpacing(zoom) {
|
|
196
|
+
return resolveDotCheckpoint(zoom).spacing;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Возвращает integer-радиус точки в screen-space для текущего zoom.
|
|
201
|
+
* @param {number} zoom
|
|
202
|
+
* @param {number} minScreenDotRadius
|
|
203
|
+
* @returns {number}
|
|
204
|
+
*/
|
|
205
|
+
export function getScreenDotRadius(zoom, minScreenDotRadius = 1) {
|
|
206
|
+
const stepRadius = resolveDotCheckpoint(zoom).dotRadius;
|
|
207
|
+
return Math.max(0, Math.round(stepRadius));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Возвращает raw-радиус без округления (для debug-просмотра).
|
|
212
|
+
* @param {number} zoom
|
|
213
|
+
* @param {number} minScreenDotRadius
|
|
214
|
+
* @returns {number}
|
|
215
|
+
*/
|
|
216
|
+
export function getRawScreenDotRadius(zoom, minScreenDotRadius = 0.1) {
|
|
217
|
+
const stepRadius = resolveDotCheckpoint(zoom).dotRadius;
|
|
218
|
+
return Math.max(0, Number(stepRadius) || 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Возвращает цвет точки для текущего zoom-checkpoint.
|
|
223
|
+
* @param {number} zoom
|
|
224
|
+
* @param {number} fallbackColor
|
|
225
|
+
* @returns {number}
|
|
226
|
+
*/
|
|
227
|
+
export function getDotColor(zoom, fallbackColor = 0xE8E8E8) {
|
|
228
|
+
const color = resolveDotCheckpoint(zoom).color;
|
|
229
|
+
return Number.isFinite(color) ? color : fallbackColor;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Точечная сетка всегда рендерится без прозрачности.
|
|
234
|
+
* @param {number} zoom
|
|
235
|
+
* @returns {number}
|
|
236
|
+
*/
|
|
237
|
+
export function getDotOpacity(zoom) {
|
|
238
|
+
void zoom;
|
|
239
|
+
return 1;
|
|
42
240
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const GLOBAL_DIAG_KEY = '__MOODBOARD_GRID_DIAGNOSTICS__';
|
|
2
|
+
const MAX_HISTORY = 500;
|
|
3
|
+
|
|
4
|
+
function getGlobalScope() {
|
|
5
|
+
try {
|
|
6
|
+
return globalThis;
|
|
7
|
+
} catch (_) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeEnabled(raw) {
|
|
13
|
+
if (raw === true || raw === '1' || raw === 1) return true;
|
|
14
|
+
if (raw === false || raw === '0' || raw === 0) return false;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureSink() {
|
|
19
|
+
const scope = getGlobalScope();
|
|
20
|
+
if (!scope) return null;
|
|
21
|
+
const existing = scope[GLOBAL_DIAG_KEY];
|
|
22
|
+
if (existing && typeof existing === 'object') {
|
|
23
|
+
if (typeof existing.enabled !== 'boolean') existing.enabled = false;
|
|
24
|
+
if (!existing.counters || typeof existing.counters !== 'object') existing.counters = {};
|
|
25
|
+
if (!Array.isArray(existing.history)) existing.history = [];
|
|
26
|
+
return existing;
|
|
27
|
+
}
|
|
28
|
+
const created = {
|
|
29
|
+
enabled: false,
|
|
30
|
+
counters: {},
|
|
31
|
+
history: [],
|
|
32
|
+
};
|
|
33
|
+
scope[GLOBAL_DIAG_KEY] = created;
|
|
34
|
+
return created;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isGridDiagnosticsEnabled() {
|
|
38
|
+
const envEnabled = normalizeEnabled(
|
|
39
|
+
(typeof process !== 'undefined' && process?.env?.MOODBOARD_GRID_DIAGNOSTICS) || false
|
|
40
|
+
);
|
|
41
|
+
if (envEnabled) return true;
|
|
42
|
+
const sink = ensureSink();
|
|
43
|
+
return !!sink?.enabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function incrementGridDiagnosticCounter(counterName) {
|
|
47
|
+
if (!counterName || !isGridDiagnosticsEnabled()) return;
|
|
48
|
+
const sink = ensureSink();
|
|
49
|
+
if (!sink) return;
|
|
50
|
+
sink.counters[counterName] = (sink.counters[counterName] || 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function logGridDiagnostic(scope, message, data = undefined) {
|
|
54
|
+
if (!isGridDiagnosticsEnabled()) return;
|
|
55
|
+
const sink = ensureSink();
|
|
56
|
+
if (!sink) return;
|
|
57
|
+
|
|
58
|
+
const entry = {
|
|
59
|
+
ts: Date.now(),
|
|
60
|
+
scope,
|
|
61
|
+
message,
|
|
62
|
+
data,
|
|
63
|
+
};
|
|
64
|
+
sink.history.push(entry);
|
|
65
|
+
if (sink.history.length > MAX_HISTORY) {
|
|
66
|
+
sink.history.splice(0, sink.history.length - MAX_HISTORY);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getGridDiagnosticsSnapshot() {
|
|
71
|
+
const sink = ensureSink();
|
|
72
|
+
if (!sink) {
|
|
73
|
+
return { enabled: false, counters: {}, history: [] };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
enabled: !!sink.enabled,
|
|
77
|
+
counters: { ...sink.counters },
|
|
78
|
+
history: [...sink.history],
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/grid/GridFactory.js
CHANGED
|
@@ -67,18 +67,20 @@ export class GridFactory {
|
|
|
67
67
|
const defaults = {
|
|
68
68
|
line: {
|
|
69
69
|
enabled: true,
|
|
70
|
-
size:
|
|
71
|
-
color:
|
|
72
|
-
opacity:
|
|
70
|
+
size: 20,
|
|
71
|
+
color: 0xF4F4F4,
|
|
72
|
+
opacity: 1,
|
|
73
73
|
lineWidth: 1,
|
|
74
|
-
showSubGrid:
|
|
75
|
-
subGridDivisions:
|
|
74
|
+
showSubGrid: true,
|
|
75
|
+
subGridDivisions: 5,
|
|
76
|
+
subGridColor: 0xFEFEFE,
|
|
77
|
+
subGridOpacity: 1
|
|
76
78
|
},
|
|
77
79
|
dot: {
|
|
78
80
|
enabled: true,
|
|
79
81
|
size: 30,
|
|
80
|
-
color:
|
|
81
|
-
opacity:
|
|
82
|
+
color: 0xE8E8E8,
|
|
83
|
+
opacity: 1,
|
|
82
84
|
dotSize: 1,
|
|
83
85
|
dotStyle: 'circle',
|
|
84
86
|
highlightIntersections: true
|
|
@@ -86,7 +88,7 @@ export class GridFactory {
|
|
|
86
88
|
cross: {
|
|
87
89
|
enabled: true,
|
|
88
90
|
size: 65, // шаг между крестиками
|
|
89
|
-
color:
|
|
91
|
+
color: 0xE8E8E8, // как у dot-сетки
|
|
90
92
|
opacity: 1, // непрозрачно
|
|
91
93
|
crossHalfSize: 5, // длина луча (половина креста) в px
|
|
92
94
|
crossLineWidth: 1
|
|
@@ -130,21 +132,21 @@ export class GridFactory {
|
|
|
130
132
|
type: 'dot',
|
|
131
133
|
size: 10,
|
|
132
134
|
color: 0xC0C0C0,
|
|
133
|
-
opacity:
|
|
135
|
+
opacity: 1,
|
|
134
136
|
dotSize: 1
|
|
135
137
|
},
|
|
136
138
|
'standard-dots': {
|
|
137
139
|
type: 'dot',
|
|
138
140
|
size: 20,
|
|
139
141
|
color: 0xB0B0B0,
|
|
140
|
-
opacity:
|
|
142
|
+
opacity: 1,
|
|
141
143
|
dotSize: 2
|
|
142
144
|
},
|
|
143
145
|
'bold-dots': {
|
|
144
146
|
type: 'dot',
|
|
145
147
|
size: 30,
|
|
146
148
|
color: 0xA0A0A0,
|
|
147
|
-
opacity:
|
|
149
|
+
opacity: 1,
|
|
148
150
|
dotSize: 3,
|
|
149
151
|
dotStyle: 'square'
|
|
150
152
|
},
|