@sequent-org/moodboard 1.3.4 → 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 +1486 -15
- 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
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { GridFactory } from '../grid/GridFactory.js';
|
|
2
2
|
import { Events } from '../core/events/Events.js';
|
|
3
|
+
import {
|
|
4
|
+
incrementGridDiagnosticCounter,
|
|
5
|
+
logGridDiagnostic,
|
|
6
|
+
} from '../grid/GridDiagnostics.js';
|
|
3
7
|
|
|
4
8
|
export class BoardService {
|
|
5
9
|
constructor(eventBus, pixi) {
|
|
@@ -7,9 +11,17 @@ export class BoardService {
|
|
|
7
11
|
this.pixi = pixi;
|
|
8
12
|
this.grid = null;
|
|
9
13
|
this._getCanvasSize = null;
|
|
14
|
+
this._eventsAttached = false;
|
|
15
|
+
this._handlers = null;
|
|
16
|
+
this._destroyed = false;
|
|
17
|
+
this._lastZoomCursor = null;
|
|
18
|
+
this._consumeZoomCursorAnchor = false;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
async init(getCanvasSize) {
|
|
22
|
+
if (this._destroyed) {
|
|
23
|
+
this._destroyed = false;
|
|
24
|
+
}
|
|
13
25
|
this._getCanvasSize = getCanvasSize;
|
|
14
26
|
// Не создаём сетку по умолчанию, чтобы избежать визуального переключения.
|
|
15
27
|
// Сетка будет установлена из сохранённых настроек через Events.UI.GridChange.
|
|
@@ -18,9 +30,9 @@ export class BoardService {
|
|
|
18
30
|
this._attachEvents();
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
_buildHandlers() {
|
|
34
|
+
const onGridChange = ({ type, options: overrideOptions }) => {
|
|
35
|
+
incrementGridDiagnosticCounter('boardService.gridChange.calls');
|
|
24
36
|
const size = this._getCanvasSize?.() || { width: 800, height: 600 };
|
|
25
37
|
if (type === 'off') {
|
|
26
38
|
this.grid?.setEnabled(false);
|
|
@@ -31,6 +43,7 @@ export class BoardService {
|
|
|
31
43
|
grid: { type: 'off', options: this.grid?.serialize ? this.grid.serialize() : {} }
|
|
32
44
|
});
|
|
33
45
|
} catch (_) {}
|
|
46
|
+
logGridDiagnostic('BoardService', 'grid switched off', { hasGrid: !!this.grid });
|
|
34
47
|
return;
|
|
35
48
|
}
|
|
36
49
|
this.grid?.destroy?.();
|
|
@@ -53,34 +66,75 @@ export class BoardService {
|
|
|
53
66
|
grid: { type, options: this.grid.serialize ? this.grid.serialize() : gridOptions }
|
|
54
67
|
});
|
|
55
68
|
} catch (_) {}
|
|
69
|
+
logGridDiagnostic('BoardService', 'grid switched on', {
|
|
70
|
+
type,
|
|
71
|
+
options: gridOptions,
|
|
72
|
+
});
|
|
56
73
|
} catch (e) {
|
|
57
74
|
console.warn('Unknown grid type:', type);
|
|
58
75
|
}
|
|
59
|
-
}
|
|
76
|
+
};
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
this.eventBus.on(Events.UI.MinimapGetData, (req) => {
|
|
78
|
+
const onMinimapGetData = (req) => {
|
|
63
79
|
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
64
80
|
const viewEl = this.pixi.app.view;
|
|
65
|
-
const objects = (this.pixi?.objects ? Array.from(this.pixi.objects.keys()) : []).map((id) => id);
|
|
66
81
|
req.world = { x: world.x, y: world.y, scale: world.scale?.x || 1 };
|
|
67
82
|
req.view = { width: viewEl.clientWidth, height: viewEl.clientHeight };
|
|
68
83
|
// Прокидываем только метаданные объектов через ядро (сам список формирует Core)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onViewportChanged = () => this.refreshGridViewport();
|
|
87
|
+
const onWheelZoom = ({ x, y } = {}) => {
|
|
88
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
89
|
+
this._lastZoomCursor = { x: Math.round(x), y: Math.round(y) };
|
|
90
|
+
this._consumeZoomCursorAnchor = true;
|
|
91
|
+
};
|
|
92
|
+
const onMinimapCenterOn = ({ worldX, worldY }) => {
|
|
72
93
|
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
73
94
|
const viewW = this.pixi.app.view.clientWidth;
|
|
74
95
|
const viewH = this.pixi.app.view.clientHeight;
|
|
75
96
|
const s = world.scale?.x || 1;
|
|
76
|
-
world.x = viewW / 2 - worldX * s;
|
|
77
|
-
world.y = viewH / 2 - worldY * s;
|
|
78
|
-
if (this.pixi.gridLayer) {
|
|
79
|
-
this.pixi.gridLayer.x = world.x;
|
|
80
|
-
this.pixi.gridLayer.y = world.y;
|
|
81
|
-
}
|
|
97
|
+
world.x = Math.round(viewW / 2 - worldX * s);
|
|
98
|
+
world.y = Math.round(viewH / 2 - worldY * s);
|
|
82
99
|
this.refreshGridViewport();
|
|
83
|
-
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
onGridChange,
|
|
104
|
+
onMinimapGetData,
|
|
105
|
+
onViewportChanged,
|
|
106
|
+
onWheelZoom,
|
|
107
|
+
onMinimapCenterOn,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_attachEvents() {
|
|
112
|
+
if (this._eventsAttached) return;
|
|
113
|
+
this._handlers = this._handlers || this._buildHandlers();
|
|
114
|
+
|
|
115
|
+
// Смена вида сетки из UI
|
|
116
|
+
this.eventBus.on(Events.UI.GridChange, this._handlers.onGridChange);
|
|
117
|
+
|
|
118
|
+
// Миникарта: данные и управление
|
|
119
|
+
this.eventBus.on(Events.UI.MinimapGetData, this._handlers.onMinimapGetData);
|
|
120
|
+
this.eventBus.on(Events.Viewport.Changed, this._handlers.onViewportChanged);
|
|
121
|
+
this.eventBus.on(Events.Tool.WheelZoom, this._handlers.onWheelZoom);
|
|
122
|
+
this.eventBus.on(Events.UI.MinimapCenterOn, this._handlers.onMinimapCenterOn);
|
|
123
|
+
this._eventsAttached = true;
|
|
124
|
+
incrementGridDiagnosticCounter('boardService.lifecycle.attach');
|
|
125
|
+
logGridDiagnostic('BoardService', 'events attached');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_detachEvents() {
|
|
129
|
+
if (!this._eventsAttached || !this._handlers) return;
|
|
130
|
+
this.eventBus.off(Events.UI.GridChange, this._handlers.onGridChange);
|
|
131
|
+
this.eventBus.off(Events.UI.MinimapGetData, this._handlers.onMinimapGetData);
|
|
132
|
+
this.eventBus.off(Events.Viewport.Changed, this._handlers.onViewportChanged);
|
|
133
|
+
this.eventBus.off(Events.Tool.WheelZoom, this._handlers.onWheelZoom);
|
|
134
|
+
this.eventBus.off(Events.UI.MinimapCenterOn, this._handlers.onMinimapCenterOn);
|
|
135
|
+
this._eventsAttached = false;
|
|
136
|
+
incrementGridDiagnosticCounter('boardService.lifecycle.detach');
|
|
137
|
+
logGridDiagnostic('BoardService', 'events detached');
|
|
84
138
|
}
|
|
85
139
|
|
|
86
140
|
resize() {
|
|
@@ -93,9 +147,10 @@ export class BoardService {
|
|
|
93
147
|
}
|
|
94
148
|
|
|
95
149
|
/**
|
|
96
|
-
*
|
|
150
|
+
* Обновляет screen-grid слой по viewport состоянию.
|
|
97
151
|
*/
|
|
98
152
|
refreshGridViewport() {
|
|
153
|
+
incrementGridDiagnosticCounter('boardService.refreshGridViewport.calls');
|
|
99
154
|
if (!this.grid?.enabled || !this.pixi?.gridLayer) return;
|
|
100
155
|
const view = this.pixi.app?.view;
|
|
101
156
|
if (!view) return;
|
|
@@ -103,26 +158,67 @@ export class BoardService {
|
|
|
103
158
|
const gl = this.pixi.gridLayer;
|
|
104
159
|
const scale = world.scale?.x ?? 1;
|
|
105
160
|
|
|
106
|
-
//
|
|
107
|
-
gl.x =
|
|
108
|
-
gl.y =
|
|
161
|
+
// Screen-grid всегда рендерится в координатах экрана.
|
|
162
|
+
gl.x = 0;
|
|
163
|
+
gl.y = 0;
|
|
109
164
|
if (gl.scale) {
|
|
110
|
-
gl.scale.set(
|
|
165
|
+
gl.scale.set(1);
|
|
166
|
+
}
|
|
167
|
+
if ((gl.x || 0) !== 0 || (gl.y || 0) !== 0 || (gl.scale?.x || 1) !== 1 || (gl.scale?.y || 1) !== 1) {
|
|
168
|
+
logGridDiagnostic('BoardService', 'gridLayer normalization mismatch', {
|
|
169
|
+
x: gl.x || 0,
|
|
170
|
+
y: gl.y || 0,
|
|
171
|
+
scaleX: gl.scale?.x || 1,
|
|
172
|
+
scaleY: gl.scale?.y || 1,
|
|
173
|
+
});
|
|
111
174
|
}
|
|
112
175
|
|
|
113
|
-
|
|
114
|
-
if (this.grid.type === 'dot' && typeof this.grid.setZoom === 'function') {
|
|
176
|
+
if (typeof this.grid.setZoom === 'function') {
|
|
115
177
|
this.grid.setZoom(scale);
|
|
116
178
|
}
|
|
179
|
+
if (typeof this.grid.setViewportTransform === 'function') {
|
|
180
|
+
const zoomCursor = this._lastZoomCursor;
|
|
181
|
+
const useCursorAnchor = this._consumeZoomCursorAnchor && !!zoomCursor;
|
|
182
|
+
this.grid.setViewportTransform({
|
|
183
|
+
worldX: world.x || 0,
|
|
184
|
+
worldY: world.y || 0,
|
|
185
|
+
scale,
|
|
186
|
+
viewWidth: view.clientWidth,
|
|
187
|
+
viewHeight: view.clientHeight,
|
|
188
|
+
zoomCursorX: zoomCursor?.x ?? null,
|
|
189
|
+
zoomCursorY: zoomCursor?.y ?? null,
|
|
190
|
+
useCursorAnchor,
|
|
191
|
+
});
|
|
192
|
+
this._consumeZoomCursorAnchor = false;
|
|
193
|
+
}
|
|
117
194
|
|
|
118
|
-
// Видимая область в
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const bottom = (view.clientHeight - gl.y + pad) / scale;
|
|
195
|
+
// Видимая область в screen-координатах.
|
|
196
|
+
const pad = 32;
|
|
197
|
+
const left = -pad;
|
|
198
|
+
const top = -pad;
|
|
199
|
+
const right = view.clientWidth + pad;
|
|
200
|
+
const bottom = view.clientHeight + pad;
|
|
125
201
|
this.grid.setVisibleBounds(left, top, right, bottom);
|
|
202
|
+
logGridDiagnostic('BoardService', 'viewport refreshed', {
|
|
203
|
+
gridType: this.grid?.type || 'unknown',
|
|
204
|
+
scale,
|
|
205
|
+
worldX: world.x || 0,
|
|
206
|
+
worldY: world.y || 0,
|
|
207
|
+
bounds: { left, top, right, bottom },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
destroy() {
|
|
212
|
+
if (this._destroyed) return;
|
|
213
|
+
this._destroyed = true;
|
|
214
|
+
this._detachEvents();
|
|
215
|
+
this.grid?.destroy?.();
|
|
216
|
+
this.grid = null;
|
|
217
|
+
if (this.pixi?.setGrid) {
|
|
218
|
+
this.pixi.setGrid(null);
|
|
219
|
+
}
|
|
220
|
+
incrementGridDiagnosticCounter('boardService.lifecycle.destroy');
|
|
221
|
+
logGridDiagnostic('BoardService', 'destroy');
|
|
126
222
|
}
|
|
127
223
|
}
|
|
128
224
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { incrementGridDiagnosticCounter, logGridDiagnostic } from '../grid/GridDiagnostics.js';
|
|
2
|
+
|
|
3
|
+
export class GridSnapResolver {
|
|
4
|
+
constructor(core) {
|
|
5
|
+
this.core = core;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
_getGrid() {
|
|
9
|
+
return this.core?.boardService?.grid || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_isEnabled() {
|
|
13
|
+
const grid = this._getGrid();
|
|
14
|
+
return !!(grid && grid.enabled && grid.snapEnabled !== false);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
snapWorldTopLeft(position, size = null) {
|
|
18
|
+
incrementGridDiagnosticCounter('gridSnapResolver.snapWorldTopLeft.calls');
|
|
19
|
+
if (!position || typeof position.x !== 'number' || typeof position.y !== 'number') {
|
|
20
|
+
return position;
|
|
21
|
+
}
|
|
22
|
+
if (!this._isEnabled()) {
|
|
23
|
+
logGridDiagnostic('GridSnapResolver', 'snap skipped: grid disabled', { position, size });
|
|
24
|
+
return position;
|
|
25
|
+
}
|
|
26
|
+
const grid = this._getGrid();
|
|
27
|
+
const halfW = size ? (size.width || 0) / 2 : 0;
|
|
28
|
+
const halfH = size ? (size.height || 0) / 2 : 0;
|
|
29
|
+
const center = {
|
|
30
|
+
x: position.x + halfW,
|
|
31
|
+
y: position.y + halfH,
|
|
32
|
+
};
|
|
33
|
+
if (typeof grid.snapWorldPoint === 'function') {
|
|
34
|
+
const snappedCenter = grid.snapWorldPoint(center.x, center.y);
|
|
35
|
+
const snapped = {
|
|
36
|
+
x: snappedCenter.x - halfW,
|
|
37
|
+
y: snappedCenter.y - halfH,
|
|
38
|
+
};
|
|
39
|
+
logGridDiagnostic('GridSnapResolver', 'snap via snapWorldPoint', {
|
|
40
|
+
type: grid.type || 'unknown',
|
|
41
|
+
position,
|
|
42
|
+
size,
|
|
43
|
+
snapped,
|
|
44
|
+
});
|
|
45
|
+
return snapped;
|
|
46
|
+
}
|
|
47
|
+
const fallback = grid.snapToGrid(center.x, center.y);
|
|
48
|
+
const snapped = {
|
|
49
|
+
x: fallback.x - halfW,
|
|
50
|
+
y: fallback.y - halfH,
|
|
51
|
+
};
|
|
52
|
+
logGridDiagnostic('GridSnapResolver', 'snap via snapToGrid fallback', {
|
|
53
|
+
type: grid.type || 'unknown',
|
|
54
|
+
position,
|
|
55
|
+
size,
|
|
56
|
+
snapped,
|
|
57
|
+
});
|
|
58
|
+
return snapped;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Наблюдаемые шаги Miro при zoom-out от 400%.
|
|
3
|
+
* Оригинальная последовательность содержит дубликаты на низком zoom.
|
|
4
|
+
*/
|
|
5
|
+
export const MIRO_ZOOM_DOWN_FROM_400 = [
|
|
6
|
+
400, 357, 319, 285, 254, 227, 203, 181, 162, 144, 129, 115, 103, 92, 82,
|
|
7
|
+
73, 65, 58, 52, 46, 41, 37,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Наблюдаемые шаги Miro при zoom-in от 100%.
|
|
12
|
+
*/
|
|
13
|
+
export const MIRO_ZOOM_UP_FROM_100 = [
|
|
14
|
+
100, 112, 125, 140, 157, 176, 197, 221, 248, 277, 311, 348, 390, 400,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Наблюдаемые шаги Miro при zoom-out от 100%.
|
|
19
|
+
*/
|
|
20
|
+
export const MIRO_ZOOM_DOWN_FROM_100 = [
|
|
21
|
+
// Дубли (8, 5) представлены разными zoom-value, но одинаковой label после округления.
|
|
22
|
+
100, 92, 82, 73, 65, 58, 52, 46, 41, 37, 33, 30, 26, 24, 21, 19, 17, 15, 13,
|
|
23
|
+
12, 11, 10, 8.4, 7.6, 7, 6, 5.4, 4.6, 4.0, 3.6, 3.4, 3.0, 2.6, 2.4, 2.2, 2.0,
|
|
24
|
+
1.8, 1.6, 1.4,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const uniqueSorted = new Set([
|
|
28
|
+
...MIRO_ZOOM_DOWN_FROM_400,
|
|
29
|
+
...MIRO_ZOOM_UP_FROM_100,
|
|
30
|
+
...MIRO_ZOOM_DOWN_FROM_100,
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Общий справочник checkpoint'ов для аналитики/тестов.
|
|
35
|
+
*/
|
|
36
|
+
export const MIRO_ZOOM_LEVELS = Array.from(uniqueSorted).sort((a, b) => a - b);
|
|
37
|
+
|
|
38
|
+
export const MIRO_ZOOM_UP_TO_100 = Array.from(new Set(MIRO_ZOOM_DOWN_FROM_100)).sort((a, b) => a - b);
|
|
39
|
+
|
|
@@ -85,10 +85,6 @@ export class SettingsApplier {
|
|
|
85
85
|
if (world) {
|
|
86
86
|
world.x = s.pan.x;
|
|
87
87
|
world.y = s.pan.y;
|
|
88
|
-
if (this.pixi?.gridLayer) {
|
|
89
|
-
this.pixi.gridLayer.x = s.pan.x;
|
|
90
|
-
this.pixi.gridLayer.y = s.pan.y;
|
|
91
|
-
}
|
|
92
88
|
try { this.eventBus.emit(Events.Viewport.Changed); } catch (_) {}
|
|
93
89
|
}
|
|
94
90
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { Events } from '../core/events/Events.js';
|
|
2
|
+
import {
|
|
3
|
+
MIRO_ZOOM_DOWN_FROM_100,
|
|
4
|
+
MIRO_ZOOM_DOWN_FROM_400,
|
|
5
|
+
MIRO_ZOOM_UP_FROM_100,
|
|
6
|
+
MIRO_ZOOM_UP_TO_100,
|
|
7
|
+
} from './MiroZoomLevels.js';
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Верхняя граница — 500%.
|
|
10
|
+
* Направленные zoom-последовательности, выровненные под наблюдения Miro.
|
|
6
11
|
*/
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
const ZOOM_UP_TO_100 = MIRO_ZOOM_UP_TO_100;
|
|
13
|
+
const ZOOM_UP_FROM_100 = MIRO_ZOOM_UP_FROM_100;
|
|
14
|
+
const ZOOM_DOWN_FROM_100 = MIRO_ZOOM_DOWN_FROM_100;
|
|
15
|
+
const ZOOM_DOWN_FROM_400 = MIRO_ZOOM_DOWN_FROM_400;
|
|
16
|
+
const EPSILON = 1e-6;
|
|
10
17
|
|
|
11
18
|
export class ZoomPanController {
|
|
12
19
|
constructor(eventBus, pixi) {
|
|
@@ -14,17 +21,34 @@ export class ZoomPanController {
|
|
|
14
21
|
this.pixi = pixi;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
_nextAscendingLevel(percent, levels) {
|
|
25
|
+
for (let i = 0; i < levels.length; i += 1) {
|
|
26
|
+
if (levels[i] > percent + EPSILON) return levels[i];
|
|
27
|
+
}
|
|
28
|
+
return levels[levels.length - 1];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_nextDescendingLevel(percent, levels) {
|
|
32
|
+
for (let i = 0; i < levels.length; i += 1) {
|
|
33
|
+
if (levels[i] < percent - EPSILON) return levels[i];
|
|
34
|
+
}
|
|
35
|
+
return levels[levels.length - 1];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_pickTargetPercent(oldPercent, delta) {
|
|
39
|
+
if (delta < 0) {
|
|
40
|
+
if (oldPercent >= 100) {
|
|
41
|
+
return this._nextAscendingLevel(oldPercent, ZOOM_UP_FROM_100);
|
|
25
42
|
}
|
|
43
|
+
return this._nextAscendingLevel(oldPercent, ZOOM_UP_TO_100);
|
|
26
44
|
}
|
|
27
|
-
|
|
45
|
+
if (delta > 0) {
|
|
46
|
+
if (oldPercent > 100) {
|
|
47
|
+
return this._nextDescendingLevel(oldPercent, ZOOM_DOWN_FROM_400);
|
|
48
|
+
}
|
|
49
|
+
return this._nextDescendingLevel(oldPercent, ZOOM_DOWN_FROM_100);
|
|
50
|
+
}
|
|
51
|
+
return oldPercent;
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
attach() {
|
|
@@ -32,25 +56,20 @@ export class ZoomPanController {
|
|
|
32
56
|
this.eventBus.on(Events.Tool.WheelZoom, ({ x, y, delta }) => {
|
|
33
57
|
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
34
58
|
const oldScale = world.scale.x || 1;
|
|
35
|
-
const oldPercent =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} else if (delta > 0) {
|
|
41
|
-
targetPercent = ZOOM_LEVELS[Math.max(0, idx - 1)];
|
|
42
|
-
} else {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const newScale = Math.max(0.02, Math.min(5, targetPercent / 100));
|
|
59
|
+
const oldPercent = oldScale * 100;
|
|
60
|
+
if (!(delta < 0 || delta > 0)) return;
|
|
61
|
+
|
|
62
|
+
const targetPercent = this._pickTargetPercent(oldPercent, delta);
|
|
63
|
+
const newScale = Math.max(0.01, Math.min(5, targetPercent / 100));
|
|
46
64
|
if (Math.abs(newScale - oldScale) < 0.0001) return;
|
|
65
|
+
|
|
47
66
|
// Вычисляем мировые координаты точки под курсором до изменения скейла
|
|
48
67
|
const worldX = (x - world.x) / oldScale;
|
|
49
68
|
const worldY = (y - world.y) / oldScale;
|
|
50
69
|
// Применяем новый скейл и пересчитываем позицию, чтобы точка под курсором осталась на месте
|
|
51
70
|
world.scale.set(newScale);
|
|
52
|
-
world.x = x - worldX * newScale;
|
|
53
|
-
world.y = y - worldY * newScale;
|
|
71
|
+
world.x = Math.round(x - worldX * newScale);
|
|
72
|
+
world.y = Math.round(y - worldY * newScale);
|
|
54
73
|
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
|
|
55
74
|
this.eventBus.emit(Events.Viewport.Changed);
|
|
56
75
|
});
|
|
@@ -76,8 +95,8 @@ export class ZoomPanController {
|
|
|
76
95
|
const worldX = (centerX - world.x) / oldScale;
|
|
77
96
|
const worldY = (centerY - world.y) / oldScale;
|
|
78
97
|
world.scale.set(1);
|
|
79
|
-
world.x = centerX - worldX
|
|
80
|
-
world.y = centerY - worldY
|
|
98
|
+
world.x = Math.round(centerX - worldX);
|
|
99
|
+
world.y = Math.round(centerY - worldY);
|
|
81
100
|
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: 100 });
|
|
82
101
|
this.eventBus.emit(Events.Viewport.Changed);
|
|
83
102
|
});
|
|
@@ -99,13 +118,13 @@ export class ZoomPanController {
|
|
|
99
118
|
const padding = 40;
|
|
100
119
|
const scaleX = (viewW - padding) / bboxW;
|
|
101
120
|
const scaleY = (viewH - padding) / bboxH;
|
|
102
|
-
const newScale = Math.max(0.
|
|
121
|
+
const newScale = Math.max(0.01, Math.min(5, Math.min(scaleX, scaleY)));
|
|
103
122
|
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
104
123
|
const worldCenterX = minX + bboxW / 2;
|
|
105
124
|
const worldCenterY = minY + bboxH / 2;
|
|
106
125
|
world.scale.set(newScale);
|
|
107
|
-
world.x = viewW / 2 - worldCenterX * newScale;
|
|
108
|
-
world.y = viewH / 2 - worldCenterY * newScale;
|
|
126
|
+
world.x = Math.round(viewW / 2 - worldCenterX * newScale);
|
|
127
|
+
world.y = Math.round(viewH / 2 - worldCenterY * newScale);
|
|
109
128
|
this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
|
|
110
129
|
this.eventBus.emit(Events.Viewport.Changed);
|
|
111
130
|
});
|
|
@@ -81,6 +81,8 @@ export class PlacementTool extends BaseTool {
|
|
|
81
81
|
this.showFrameGhost();
|
|
82
82
|
} else if (this.pending.type === 'shape') {
|
|
83
83
|
this.showShapeGhost();
|
|
84
|
+
} else if (this.pending.type === 'mindmap') {
|
|
85
|
+
this.showMindmapGhost();
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
}
|
|
@@ -292,6 +294,13 @@ export class PlacementTool extends BaseTool {
|
|
|
292
294
|
return this.ghostController.showShapeGhost();
|
|
293
295
|
}
|
|
294
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Показать "призрак" mindmap-объекта
|
|
299
|
+
*/
|
|
300
|
+
showMindmapGhost() {
|
|
301
|
+
return this.ghostController.showMindmapGhost();
|
|
302
|
+
}
|
|
303
|
+
|
|
295
304
|
/**
|
|
296
305
|
* Разместить выбранное изображение на холсте
|
|
297
306
|
*/
|
|
@@ -360,10 +369,10 @@ export class PlacementTool extends BaseTool {
|
|
|
360
369
|
|
|
361
370
|
// Возвращает подходящий курсор для текущего pending состояния
|
|
362
371
|
PlacementTool.prototype._getPendingCursor = function() {
|
|
363
|
-
if (!this.pending) return '
|
|
372
|
+
if (!this.pending) return 'default';
|
|
364
373
|
if (this.pending.type === 'text') return 'text';
|
|
365
|
-
if (this.pending.type === 'frame-draw') return '
|
|
366
|
-
return '
|
|
374
|
+
if (this.pending.type === 'frame-draw') return 'default';
|
|
375
|
+
return 'default';
|
|
367
376
|
};
|
|
368
377
|
|
|
369
378
|
|
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
} from './selection/CloneFlowController.js';
|
|
25
25
|
import {
|
|
26
26
|
openTextEditor as openTextEditorViaController,
|
|
27
|
+
openMindmapEditor as openMindmapEditorViaController,
|
|
27
28
|
openFileNameEditor as openFileNameEditorViaController,
|
|
28
29
|
closeFileNameEditor as closeFileNameEditorViaController,
|
|
29
|
-
closeTextEditor as closeTextEditorViaController
|
|
30
|
+
closeTextEditor as closeTextEditorViaController,
|
|
31
|
+
closeMindmapEditor as closeMindmapEditorViaController
|
|
30
32
|
} from './selection/InlineEditorController.js';
|
|
31
33
|
import {
|
|
32
34
|
hitTest as hitTestViaService,
|
|
@@ -325,6 +327,10 @@ export class SelectTool extends BaseTool {
|
|
|
325
327
|
return openTextEditorViaController.call(this, object, create);
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
_openMindmapEditor(object, create = false) {
|
|
331
|
+
return openMindmapEditorViaController.call(this, object, create);
|
|
332
|
+
}
|
|
333
|
+
|
|
328
334
|
_openFileNameEditor(object, create = false) {
|
|
329
335
|
return openFileNameEditorViaController.call(this, object, create);
|
|
330
336
|
}
|
|
@@ -336,6 +342,10 @@ export class SelectTool extends BaseTool {
|
|
|
336
342
|
return closeTextEditorViaController.call(this, commit);
|
|
337
343
|
}
|
|
338
344
|
|
|
345
|
+
_closeMindmapEditor(commit) {
|
|
346
|
+
return closeMindmapEditorViaController.call(this, commit);
|
|
347
|
+
}
|
|
348
|
+
|
|
339
349
|
destroy() {
|
|
340
350
|
return destroySelectTool.call(this, () => super.destroy());
|
|
341
351
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { MINDMAP_LAYOUT } from '../../../ui/mindmap/MindmapLayoutConfig.js';
|
|
2
3
|
|
|
3
4
|
export class GhostController {
|
|
4
5
|
constructor(host) {
|
|
@@ -233,7 +234,7 @@ export class GhostController {
|
|
|
233
234
|
}
|
|
234
235
|
} catch (_) {}
|
|
235
236
|
} else if (host.app && host.app.view) {
|
|
236
|
-
host.cursor = '
|
|
237
|
+
host.cursor = 'default';
|
|
237
238
|
host.app.view.style.cursor = host.cursor;
|
|
238
239
|
}
|
|
239
240
|
}
|
|
@@ -501,4 +502,102 @@ export class GhostController {
|
|
|
501
502
|
|
|
502
503
|
host.world.addChild(host.ghostContainer);
|
|
503
504
|
}
|
|
505
|
+
|
|
506
|
+
showMindmapGhost() {
|
|
507
|
+
const host = this.host;
|
|
508
|
+
if (!host.pending || host.pending.type !== 'mindmap' || !host.world) return;
|
|
509
|
+
|
|
510
|
+
this.hideGhost();
|
|
511
|
+
|
|
512
|
+
host.ghostContainer = new PIXI.Container();
|
|
513
|
+
host.ghostContainer.alpha = 0.75;
|
|
514
|
+
|
|
515
|
+
const width = Math.max(1, Math.round(host.pending.properties?.width || MINDMAP_LAYOUT.width));
|
|
516
|
+
const height = Math.max(1, Math.round(host.pending.properties?.height || MINDMAP_LAYOUT.height));
|
|
517
|
+
const strokeColor = (typeof host.pending.properties?.strokeColor === 'number')
|
|
518
|
+
? host.pending.properties.strokeColor
|
|
519
|
+
: 0x2563EB;
|
|
520
|
+
const fillColor = (typeof host.pending.properties?.fillColor === 'number')
|
|
521
|
+
? host.pending.properties.fillColor
|
|
522
|
+
: 0x3B82F6;
|
|
523
|
+
const fillAlpha = (typeof host.pending.properties?.fillAlpha === 'number')
|
|
524
|
+
? host.pending.properties.fillAlpha
|
|
525
|
+
: 0.25;
|
|
526
|
+
const strokeWidth = (typeof host.pending.properties?.strokeWidth === 'number')
|
|
527
|
+
? host.pending.properties.strokeWidth
|
|
528
|
+
: 1;
|
|
529
|
+
const fontSize = Math.max(1, Math.round(host.pending.properties?.fontSize || MINDMAP_LAYOUT.fontSize));
|
|
530
|
+
const fontFamily = host.pending.properties?.fontFamily || 'Roboto, Arial, sans-serif';
|
|
531
|
+
const textColor = host.pending.properties?.textColor || 0x212121;
|
|
532
|
+
const paddingX = Math.max(0, Math.round(host.pending.properties?.paddingX ?? MINDMAP_LAYOUT.paddingX));
|
|
533
|
+
const placeholderText = 'Напишите что-нибудь';
|
|
534
|
+
const dynamicRadius = Math.max(0, Math.floor(Math.min(width, height) / 2));
|
|
535
|
+
const baseHeight = Math.max(
|
|
536
|
+
1,
|
|
537
|
+
Math.round(host.pending.properties?.capsuleBaseHeight || host.pending.properties?.height || MINDMAP_LAYOUT.height)
|
|
538
|
+
);
|
|
539
|
+
const baseRadius = Math.max(0, Math.floor(baseHeight / 2));
|
|
540
|
+
const cornerRadius = Math.min(dynamicRadius, baseRadius);
|
|
541
|
+
|
|
542
|
+
const graphics = new PIXI.Graphics();
|
|
543
|
+
const drawGhostCapsule = (lineWidth, alpha = 1) => {
|
|
544
|
+
try {
|
|
545
|
+
graphics.lineStyle({
|
|
546
|
+
width: lineWidth,
|
|
547
|
+
color: strokeColor,
|
|
548
|
+
alpha,
|
|
549
|
+
alignment: 0,
|
|
550
|
+
cap: 'round',
|
|
551
|
+
join: 'round',
|
|
552
|
+
miterLimit: 2,
|
|
553
|
+
});
|
|
554
|
+
} catch (_) {
|
|
555
|
+
graphics.lineStyle(lineWidth, strokeColor, alpha, 0);
|
|
556
|
+
}
|
|
557
|
+
graphics.drawRoundedRect(0, 0, width, height, cornerRadius);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
graphics.beginFill(fillColor, fillAlpha);
|
|
562
|
+
graphics.drawRoundedRect(0, 0, width, height, cornerRadius);
|
|
563
|
+
graphics.endFill();
|
|
564
|
+
} catch (_) {
|
|
565
|
+
graphics.beginFill(fillColor, fillAlpha);
|
|
566
|
+
graphics.drawRoundedRect(0, 0, width, height, cornerRadius);
|
|
567
|
+
graphics.endFill();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
drawGhostCapsule(strokeWidth, 1);
|
|
571
|
+
|
|
572
|
+
host.ghostContainer.addChild(graphics);
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const lineHeight = Math.max(1, Math.round(fontSize * 1.24));
|
|
576
|
+
const rendererRes = Math.max(1, host.app?.renderer?.resolution || 1);
|
|
577
|
+
const textScale = 1 / rendererRes;
|
|
578
|
+
const placeholder = new PIXI.Text(placeholderText, {
|
|
579
|
+
fontFamily,
|
|
580
|
+
fontSize,
|
|
581
|
+
fontWeight: '400',
|
|
582
|
+
fill: textColor,
|
|
583
|
+
align: 'left',
|
|
584
|
+
lineHeight,
|
|
585
|
+
wordWrap: false,
|
|
586
|
+
breakWords: false,
|
|
587
|
+
});
|
|
588
|
+
placeholder.alpha = 0.45;
|
|
589
|
+
placeholder.scale.set(textScale);
|
|
590
|
+
placeholder.x = paddingX;
|
|
591
|
+
const localBounds = placeholder.getLocalBounds();
|
|
592
|
+
const measuredHeight = Math.max(1, Number.isFinite(localBounds?.height) ? localBounds.height : lineHeight);
|
|
593
|
+
const scaledMeasuredHeight = measuredHeight * textScale;
|
|
594
|
+
const targetY = (height - scaledMeasuredHeight) / 2;
|
|
595
|
+
placeholder.y = (Math.round(targetY * 2) / 2) - 2;
|
|
596
|
+
host.ghostContainer.addChild(placeholder);
|
|
597
|
+
} catch (_) {}
|
|
598
|
+
|
|
599
|
+
host.ghostContainer.pivot.x = width / 2;
|
|
600
|
+
host.ghostContainer.pivot.y = height / 2;
|
|
601
|
+
host.world.addChild(host.ghostContainer);
|
|
602
|
+
}
|
|
504
603
|
}
|
|
@@ -32,6 +32,8 @@ export class PlacementEventsBridge {
|
|
|
32
32
|
this.host.startFrameDrawMode();
|
|
33
33
|
} else if (this.host.pending.type === 'shape') {
|
|
34
34
|
this.host.showShapeGhost();
|
|
35
|
+
} else if (this.host.pending.type === 'mindmap') {
|
|
36
|
+
this.host.showMindmapGhost();
|
|
35
37
|
}
|
|
36
38
|
if (this.host.pending.placeOnMouseUp && this.host.app && this.host.app.view) {
|
|
37
39
|
const onUp = (ev) => {
|