@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequent-org/moodboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Interactive moodboard",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -29,7 +29,12 @@
|
|
|
29
29
|
"test:coverage": "vitest run --coverage",
|
|
30
30
|
"test:watch": "vitest --watch",
|
|
31
31
|
"test:e2e": "playwright test",
|
|
32
|
+
"test:grid:stability": "vitest run tests/services/BoardService.grid-zoom.test.js tests/services/BoardService.grid-destroy.test.js tests/services/BoardService.lifecycle.screen-grid.test.js tests/services/BoardService.screen-grid.settings-reload.test.js tests/services/GridSnapResolver.screen-state.test.js tests/services/GridSnapResolver.screen-grid-types.test.js",
|
|
33
|
+
"test:grid:stress": "vitest run tests/services/BoardService.screen-grid.stress.test.js tests/services/GridSnapResolver.screen-grid.stress.test.js",
|
|
34
|
+
"test:grid:all": "npm run test:grid:stability && npm run test:grid:stress",
|
|
35
|
+
"test:screen:integer": "node scripts/check-screen-integer-contract.mjs",
|
|
32
36
|
"grid:our-metrics": "node scripts/export-our-dot-grid-metrics.mjs",
|
|
37
|
+
"grid:diagnose:dot": "node scripts/diagnose-dot-grid-behavior.mjs",
|
|
33
38
|
"grid:miro-metrics": "node scripts/analyze-miro-dot-screenshots.mjs",
|
|
34
39
|
"deploy:build": "npm run build && npm run start",
|
|
35
40
|
"deploy:prod": "NODE_ENV=production npm run build && NODE_ENV=production npm run start"
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19 2a3 3 0 1 1 0 6c-.463 0-.9-.109-1.291-.296l-1.19 1.19A3.992 3.992 0 0 1 18 12a3.991 3.991 0 0 1-1.48 3.105l1.189 1.19A2.984 2.984 0 0 1 19 16a3 3 0 1 1-3 3c0-.463.108-.9.295-1.291l-1.748-1.748A4.106 4.106 0 0 1 14 16h-4c-.186 0-.37-.014-.549-.04l-1.747 1.75c.187.392.296.828.296 1.291a3 3 0 1 1-3-3c.462 0 .899.108 1.29.295l1.19-1.19A3.992 3.992 0 0 1 6 12a3.99 3.99 0 0 1 1.48-3.106l-1.19-1.19A2.982 2.982 0 0 1 5 8a3 3 0 1 1 3-3c0 .463-.109.899-.296 1.29l1.748 1.748C9.632 8.014 9.814 8 10 8h4c.185 0 .368.013.547.037l1.748-1.747A2.983 2.983 0 0 1 16 5a3 3 0 0 1 3-3ZM5 18a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm14 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-9-8a2 2 0 1 0 0 4h4a2 2 0 1 0 0-4h-4ZM5 4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm14 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/>
|
|
3
|
+
</svg>
|
package/src/core/SaveManager.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Менеджер автоматического сохранения данных
|
|
3
3
|
*/
|
|
4
4
|
import { Events } from './events/Events.js';
|
|
5
|
+
import { logMindmapCompoundDebug } from '../mindmap/MindmapCompoundContract.js';
|
|
5
6
|
export class SaveManager {
|
|
6
7
|
constructor(eventBus, options = {}) {
|
|
7
8
|
this.eventBus = eventBus;
|
|
@@ -190,6 +191,23 @@ export class SaveManager {
|
|
|
190
191
|
this.updateSaveStatus('idle');
|
|
191
192
|
return;
|
|
192
193
|
}
|
|
194
|
+
const objects = Array.isArray(saveData?.boardData?.objects)
|
|
195
|
+
? saveData.boardData.objects
|
|
196
|
+
: Array.isArray(saveData?.objects) ? saveData.objects : [];
|
|
197
|
+
const mindmapNodes = objects
|
|
198
|
+
.filter((obj) => obj?.type === 'mindmap')
|
|
199
|
+
.map((obj) => ({
|
|
200
|
+
id: obj.id || null,
|
|
201
|
+
compoundId: obj.properties?.mindmap?.compoundId || null,
|
|
202
|
+
role: obj.properties?.mindmap?.role || null,
|
|
203
|
+
parentId: obj.properties?.mindmap?.parentId || null,
|
|
204
|
+
}));
|
|
205
|
+
if (mindmapNodes.length > 0) {
|
|
206
|
+
logMindmapCompoundDebug('save:roundtrip:before-send', {
|
|
207
|
+
totalMindmapNodes: mindmapNodes.length,
|
|
208
|
+
sample: mindmapNodes.slice(0, 5),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
193
211
|
|
|
194
212
|
// Проверяем, изменились ли данные с последнего сохранения
|
|
195
213
|
if (this.lastSavedData && JSON.stringify(saveData) === JSON.stringify(this.lastSavedData)) {
|
|
@@ -206,6 +224,12 @@ export class SaveManager {
|
|
|
206
224
|
const isSuccess = response.success === true || (response.data !== undefined);
|
|
207
225
|
|
|
208
226
|
if (isSuccess) {
|
|
227
|
+
if (mindmapNodes.length > 0) {
|
|
228
|
+
logMindmapCompoundDebug('save:roundtrip:success', {
|
|
229
|
+
totalMindmapNodes: mindmapNodes.length,
|
|
230
|
+
sample: mindmapNodes.slice(0, 5),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
209
233
|
this.lastSavedData = JSON.parse(JSON.stringify(saveData));
|
|
210
234
|
this.hasUnsavedChanges = false;
|
|
211
235
|
this.retryCount = 0;
|
|
@@ -260,7 +284,7 @@ export class SaveManager {
|
|
|
260
284
|
}
|
|
261
285
|
|
|
262
286
|
const requestBody = {
|
|
263
|
-
boardId
|
|
287
|
+
boardId,
|
|
264
288
|
boardData: data
|
|
265
289
|
};
|
|
266
290
|
|
|
@@ -299,6 +323,19 @@ export class SaveManager {
|
|
|
299
323
|
|
|
300
324
|
return await response.json();
|
|
301
325
|
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Строит единый payload сохранения для всех каналов отправки
|
|
329
|
+
* (обычный save, beacon, sync XHR fallback).
|
|
330
|
+
*/
|
|
331
|
+
_buildSavePayload(boardId, data, csrfToken = undefined) {
|
|
332
|
+
return {
|
|
333
|
+
boardId,
|
|
334
|
+
boardData: data,
|
|
335
|
+
settings: data?.settings || undefined,
|
|
336
|
+
_token: csrfToken || undefined
|
|
337
|
+
};
|
|
338
|
+
}
|
|
302
339
|
|
|
303
340
|
/**
|
|
304
341
|
* Обработка ошибок сохранения
|
|
@@ -440,14 +477,11 @@ export class SaveManager {
|
|
|
440
477
|
if (!data) return;
|
|
441
478
|
|
|
442
479
|
const boardId = data.id || 'default';
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
// CSRF токен добавим в тело (для серверов, которые принимают _token из JSON)
|
|
449
|
-
_token: (typeof document !== 'undefined') ? (document.querySelector('meta[name="csrf-token"]')?.value || document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')) : undefined
|
|
450
|
-
};
|
|
480
|
+
// CSRF токен добавим в тело (для серверов, которые принимают _token из JSON)
|
|
481
|
+
const csrfToken = (typeof document !== 'undefined')
|
|
482
|
+
? (document.querySelector('meta[name="csrf-token"]')?.value || document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'))
|
|
483
|
+
: undefined;
|
|
484
|
+
const payload = this._buildSavePayload(boardId, data, csrfToken);
|
|
451
485
|
|
|
452
486
|
const body = JSON.stringify(payload);
|
|
453
487
|
|
|
@@ -485,12 +519,7 @@ export class SaveManager {
|
|
|
485
519
|
xhr.setRequestHeader('Accept', 'application/json');
|
|
486
520
|
if (csrfToken) xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
|
|
487
521
|
|
|
488
|
-
const payload =
|
|
489
|
-
b: boardId,
|
|
490
|
-
boardData: data,
|
|
491
|
-
settings: data.settings || undefined,
|
|
492
|
-
_token: csrfToken || undefined
|
|
493
|
-
};
|
|
522
|
+
const payload = this._buildSavePayload(boardId, data, csrfToken || undefined);
|
|
494
523
|
try { xhr.send(JSON.stringify(payload)); } catch (_) { /* игнорируем */ }
|
|
495
524
|
} catch (_) { /* игнорируем */ }
|
|
496
525
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BaseCommand } from './BaseCommand.js';
|
|
2
|
+
import { Events } from '../events/Events.js';
|
|
3
|
+
|
|
4
|
+
function deepClone(value) {
|
|
5
|
+
return JSON.parse(JSON.stringify(value));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function asEntryMap(entries) {
|
|
9
|
+
const map = new Map();
|
|
10
|
+
const src = entries && typeof entries === 'object' ? entries : {};
|
|
11
|
+
Object.keys(src).forEach((id) => {
|
|
12
|
+
const entry = src[id];
|
|
13
|
+
if (!entry || typeof entry !== 'object') return;
|
|
14
|
+
map.set(id, entry);
|
|
15
|
+
});
|
|
16
|
+
return map;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class MindmapStatePatchCommand extends BaseCommand {
|
|
20
|
+
constructor(core, beforeEntries, afterEntries, description = 'Снимок состояния mindmap') {
|
|
21
|
+
super('mindmap-state-patch', description);
|
|
22
|
+
this.core = core;
|
|
23
|
+
this.beforeEntries = deepClone(beforeEntries || {});
|
|
24
|
+
this.afterEntries = deepClone(afterEntries || {});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
execute() {
|
|
28
|
+
this._applyEntries(this.afterEntries);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
undo() {
|
|
32
|
+
this._applyEntries(this.beforeEntries);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_applyEntries(entries) {
|
|
36
|
+
if (!this.core) return;
|
|
37
|
+
const objects = this.core?.state?.state?.objects || [];
|
|
38
|
+
const byId = new Map((Array.isArray(objects) ? objects : []).map((obj) => [obj?.id, obj]));
|
|
39
|
+
const entryMap = asEntryMap(entries);
|
|
40
|
+
entryMap.forEach((entry, id) => {
|
|
41
|
+
const node = byId.get(id);
|
|
42
|
+
if (!node || node.type !== 'mindmap') return;
|
|
43
|
+
|
|
44
|
+
const position = entry?.position || {};
|
|
45
|
+
const size = entry?.size || {};
|
|
46
|
+
const properties = entry?.properties || {};
|
|
47
|
+
|
|
48
|
+
const nextPos = {
|
|
49
|
+
x: Math.round(Number(position?.x || 0)),
|
|
50
|
+
y: Math.round(Number(position?.y || 0)),
|
|
51
|
+
};
|
|
52
|
+
const nextSize = {
|
|
53
|
+
width: Math.max(1, Math.round(Number(size?.width || node.width || node?.properties?.width || 1))),
|
|
54
|
+
height: Math.max(1, Math.round(Number(size?.height || node.height || node?.properties?.height || 1))),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.core.updateObjectSizeAndPositionDirect(id, nextSize, nextPos, 'mindmap', { snap: false });
|
|
58
|
+
|
|
59
|
+
node.properties = deepClone(properties);
|
|
60
|
+
node.width = nextSize.width;
|
|
61
|
+
node.height = nextSize.height;
|
|
62
|
+
|
|
63
|
+
const pixiObject = this.core?.pixi?.objects?.get?.(id);
|
|
64
|
+
const instance = pixiObject?._mb?.instance;
|
|
65
|
+
if (pixiObject?._mb) pixiObject._mb.properties = node.properties;
|
|
66
|
+
if (instance) {
|
|
67
|
+
const props = node.properties || {};
|
|
68
|
+
if (Number.isFinite(props.strokeColor)) instance.strokeColor = Math.floor(Number(props.strokeColor));
|
|
69
|
+
if (Number.isFinite(props.fillColor)) instance.fillColor = Math.floor(Number(props.fillColor));
|
|
70
|
+
if (Number.isFinite(props.fillAlpha)) instance.fillAlpha = Number(props.fillAlpha);
|
|
71
|
+
if (Number.isFinite(props.strokeWidth)) instance.strokeWidth = Math.max(1, Math.round(Number(props.strokeWidth)));
|
|
72
|
+
if (typeof instance._redrawPreserveTransform === 'function') {
|
|
73
|
+
instance._redrawPreserveTransform();
|
|
74
|
+
} else if (typeof instance._draw === 'function') {
|
|
75
|
+
instance._draw();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.emit(Events.Object.TransformUpdated, { objectId: id });
|
|
80
|
+
this.emit(Events.Object.Updated, { objectId: id });
|
|
81
|
+
});
|
|
82
|
+
this.core?.state?.markDirty?.();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -5,20 +5,24 @@ import { BaseCommand } from './BaseCommand.js';
|
|
|
5
5
|
import { Events } from '../events/Events.js';
|
|
6
6
|
|
|
7
7
|
export class UpdateContentCommand extends BaseCommand {
|
|
8
|
-
constructor(coreMoodboard, objectId, oldContent, newContent) {
|
|
8
|
+
constructor(coreMoodboard, objectId, oldContent, newContent, options = {}) {
|
|
9
9
|
super('update_content', `Изменить текст`);
|
|
10
10
|
this.coreMoodboard = coreMoodboard;
|
|
11
11
|
this.objectId = objectId;
|
|
12
12
|
this.oldContent = oldContent;
|
|
13
13
|
this.newContent = newContent;
|
|
14
|
+
this.oldSize = options?.oldSize ? { ...options.oldSize } : null;
|
|
15
|
+
this.newSize = options?.newSize ? { ...options.newSize } : null;
|
|
16
|
+
this.oldPosition = options?.oldPosition ? { ...options.oldPosition } : null;
|
|
17
|
+
this.newPosition = options?.newPosition ? { ...options.newPosition } : null;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
execute() {
|
|
17
|
-
this._applyContent(this.newContent);
|
|
21
|
+
this._applyContent(this.newContent, this.newSize, this.newPosition);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
undo() {
|
|
21
|
-
this._applyContent(this.oldContent);
|
|
25
|
+
this._applyContent(this.oldContent, this.oldSize, this.oldPosition);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
canMergeWith(otherCommand) {
|
|
@@ -31,10 +35,12 @@ export class UpdateContentCommand extends BaseCommand {
|
|
|
31
35
|
throw new Error('Cannot merge commands');
|
|
32
36
|
}
|
|
33
37
|
this.newContent = otherCommand.newContent;
|
|
38
|
+
this.newSize = otherCommand.newSize ? { ...otherCommand.newSize } : this.newSize;
|
|
39
|
+
this.newPosition = otherCommand.newPosition ? { ...otherCommand.newPosition } : this.newPosition;
|
|
34
40
|
this.timestamp = otherCommand.timestamp;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
_applyContent(content) {
|
|
43
|
+
_applyContent(content, sizeSnapshot = null, positionSnapshot = null) {
|
|
38
44
|
const objects = this.coreMoodboard.state.getObjects();
|
|
39
45
|
const object = objects.find((obj) => obj.id === this.objectId);
|
|
40
46
|
if (object) {
|
|
@@ -42,6 +48,43 @@ export class UpdateContentCommand extends BaseCommand {
|
|
|
42
48
|
object.properties = {};
|
|
43
49
|
}
|
|
44
50
|
object.properties.content = content;
|
|
51
|
+
|
|
52
|
+
const isMindmap = object.type === 'mindmap';
|
|
53
|
+
const hasSizeSnapshot = sizeSnapshot
|
|
54
|
+
&& Number.isFinite(sizeSnapshot.width)
|
|
55
|
+
&& Number.isFinite(sizeSnapshot.height);
|
|
56
|
+
if (isMindmap && hasSizeSnapshot) {
|
|
57
|
+
const nextSize = {
|
|
58
|
+
width: Math.max(1, Math.round(sizeSnapshot.width)),
|
|
59
|
+
height: Math.max(1, Math.round(sizeSnapshot.height)),
|
|
60
|
+
};
|
|
61
|
+
const nextPosition = (positionSnapshot
|
|
62
|
+
&& Number.isFinite(positionSnapshot.x)
|
|
63
|
+
&& Number.isFinite(positionSnapshot.y))
|
|
64
|
+
? { x: Math.round(positionSnapshot.x), y: Math.round(positionSnapshot.y) }
|
|
65
|
+
: { x: Math.round(object.position?.x || 0), y: Math.round(object.position?.y || 0) };
|
|
66
|
+
this.coreMoodboard.updateObjectSizeAndPositionDirect(
|
|
67
|
+
this.objectId,
|
|
68
|
+
nextSize,
|
|
69
|
+
nextPosition,
|
|
70
|
+
'mindmap',
|
|
71
|
+
{ snap: false }
|
|
72
|
+
);
|
|
73
|
+
this.emit(Events.Tool.ResizeUpdate, {
|
|
74
|
+
object: this.objectId,
|
|
75
|
+
size: nextSize,
|
|
76
|
+
position: nextPosition,
|
|
77
|
+
});
|
|
78
|
+
this.emit(Events.Tool.ResizeEnd, {
|
|
79
|
+
object: this.objectId,
|
|
80
|
+
oldSize: nextSize,
|
|
81
|
+
newSize: nextSize,
|
|
82
|
+
oldPosition: nextPosition,
|
|
83
|
+
newPosition: nextPosition,
|
|
84
|
+
});
|
|
85
|
+
this.emit(Events.Object.TransformUpdated, { objectId: this.objectId });
|
|
86
|
+
this.emit(Events.Object.Updated, { objectId: this.objectId });
|
|
87
|
+
}
|
|
45
88
|
this.coreMoodboard.state.markDirty();
|
|
46
89
|
}
|
|
47
90
|
this.emit(Events.Tool.UpdateObjectContent, {
|
|
@@ -6,6 +6,35 @@ import {
|
|
|
6
6
|
MoveObjectCommand
|
|
7
7
|
} from '../commands/index.js';
|
|
8
8
|
|
|
9
|
+
function collectMindmapChildrenByParent(objects) {
|
|
10
|
+
const childrenByParent = new Map();
|
|
11
|
+
(Array.isArray(objects) ? objects : []).forEach((obj) => {
|
|
12
|
+
if (!obj || obj.type !== 'mindmap') return;
|
|
13
|
+
const meta = obj?.properties?.mindmap || {};
|
|
14
|
+
if (meta?.role !== 'child' || !meta?.parentId) return;
|
|
15
|
+
if (!childrenByParent.has(meta.parentId)) childrenByParent.set(meta.parentId, []);
|
|
16
|
+
childrenByParent.get(meta.parentId).push(obj.id);
|
|
17
|
+
});
|
|
18
|
+
return childrenByParent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectMindmapDescendants(childrenByParent, rootId) {
|
|
22
|
+
const result = [];
|
|
23
|
+
const queue = [...(childrenByParent.get(rootId) || [])];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
while (queue.length > 0) {
|
|
26
|
+
const nextId = queue.shift();
|
|
27
|
+
if (!nextId || seen.has(nextId)) continue;
|
|
28
|
+
seen.add(nextId);
|
|
29
|
+
result.push(nextId);
|
|
30
|
+
const nested = childrenByParent.get(nextId) || [];
|
|
31
|
+
nested.forEach((id) => {
|
|
32
|
+
if (!seen.has(id)) queue.push(id);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
9
38
|
export function setupLayerAndViewportFlow(core) {
|
|
10
39
|
const applyZOrderFromState = () => {
|
|
11
40
|
const arr = core.state.state.objects || [];
|
|
@@ -95,6 +124,43 @@ export function setupLayerAndViewportFlow(core) {
|
|
|
95
124
|
});
|
|
96
125
|
|
|
97
126
|
core.eventBus.on(Events.Tool.DragStart, (data) => {
|
|
127
|
+
const objects = core?.state?.state?.objects || [];
|
|
128
|
+
const byId = new Map((Array.isArray(objects) ? objects : []).map((obj) => [obj?.id, obj]));
|
|
129
|
+
const dragged = byId.get(data.object);
|
|
130
|
+
const draggedMeta = dragged?.properties?.mindmap || {};
|
|
131
|
+
if (dragged?.type === 'mindmap') {
|
|
132
|
+
const compoundId = (typeof draggedMeta?.compoundId === 'string' && draggedMeta.compoundId.length > 0)
|
|
133
|
+
? draggedMeta.compoundId
|
|
134
|
+
: dragged?.id;
|
|
135
|
+
const scopeIds = (() => {
|
|
136
|
+
if (draggedMeta?.role === 'root' || !draggedMeta?.parentId) {
|
|
137
|
+
return (Array.isArray(objects) ? objects : [])
|
|
138
|
+
.filter((obj) => obj?.type === 'mindmap')
|
|
139
|
+
.filter((obj) => (obj?.properties?.mindmap?.compoundId || obj?.id) === compoundId)
|
|
140
|
+
.map((obj) => obj.id);
|
|
141
|
+
}
|
|
142
|
+
const childrenByParent = collectMindmapChildrenByParent(objects);
|
|
143
|
+
return [dragged.id, ...collectMindmapDescendants(childrenByParent, dragged.id)];
|
|
144
|
+
})();
|
|
145
|
+
const startById = new Map();
|
|
146
|
+
scopeIds.forEach((id) => {
|
|
147
|
+
const obj = byId.get(id);
|
|
148
|
+
if (!obj) return;
|
|
149
|
+
startById.set(id, {
|
|
150
|
+
x: Math.round(obj?.position?.x || 0),
|
|
151
|
+
y: Math.round(obj?.position?.y || 0),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
const rootStart = startById.get(dragged.id);
|
|
155
|
+
core._mindmapLiveDrag = {
|
|
156
|
+
draggedId: dragged.id,
|
|
157
|
+
scopeIds,
|
|
158
|
+
startById,
|
|
159
|
+
rootStart,
|
|
160
|
+
};
|
|
161
|
+
} else {
|
|
162
|
+
core._mindmapLiveDrag = null;
|
|
163
|
+
}
|
|
98
164
|
const pixiObject = core.pixi.objects.get(data.object);
|
|
99
165
|
if (pixiObject) {
|
|
100
166
|
const halfW = (pixiObject.width || 0) / 2;
|
|
@@ -108,10 +174,6 @@ export function setupLayerAndViewportFlow(core) {
|
|
|
108
174
|
core.pixi.worldLayer.x += delta.x;
|
|
109
175
|
core.pixi.worldLayer.y += delta.y;
|
|
110
176
|
}
|
|
111
|
-
if (core.pixi.gridLayer) {
|
|
112
|
-
core.pixi.gridLayer.x += delta.x;
|
|
113
|
-
core.pixi.gridLayer.y += delta.y;
|
|
114
|
-
}
|
|
115
177
|
if (!core.pixi.worldLayer) {
|
|
116
178
|
const stage = core.pixi.app.stage;
|
|
117
179
|
stage.x += delta.x;
|
|
@@ -202,15 +264,12 @@ export function setupLayerAndViewportFlow(core) {
|
|
|
202
264
|
if (!pixiObject) continue;
|
|
203
265
|
const startCenter = core._groupDragStart.get(id) || { x: pixiObject.x, y: pixiObject.y };
|
|
204
266
|
const newCenter = { x: startCenter.x + dx, y: startCenter.y + dy };
|
|
205
|
-
pixiObject.
|
|
206
|
-
pixiObject.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
obj.position.x = newCenter.x - halfW;
|
|
212
|
-
obj.position.y = newCenter.y - halfH;
|
|
213
|
-
}
|
|
267
|
+
const halfW = (pixiObject.width || 0) / 2;
|
|
268
|
+
const halfH = (pixiObject.height || 0) / 2;
|
|
269
|
+
core.updateObjectPositionDirect(id, {
|
|
270
|
+
x: newCenter.x - halfW,
|
|
271
|
+
y: newCenter.y - halfH,
|
|
272
|
+
}, { snap: false });
|
|
214
273
|
}
|
|
215
274
|
core.state.markDirty();
|
|
216
275
|
});
|
|
@@ -235,7 +294,20 @@ export function setupLayerAndViewportFlow(core) {
|
|
|
235
294
|
});
|
|
236
295
|
|
|
237
296
|
core.eventBus.on(Events.Tool.DragUpdate, (data) => {
|
|
238
|
-
core.updateObjectPositionDirect(data.object, data.position);
|
|
297
|
+
core.updateObjectPositionDirect(data.object, data.position, { snap: false });
|
|
298
|
+
const ctx = core._mindmapLiveDrag;
|
|
299
|
+
if (ctx && ctx.draggedId === data.object && ctx.rootStart && ctx.startById instanceof Map) {
|
|
300
|
+
const dx = Math.round((data?.position?.x || 0) - ctx.rootStart.x);
|
|
301
|
+
const dy = Math.round((data?.position?.y || 0) - ctx.rootStart.y);
|
|
302
|
+
(Array.isArray(ctx.scopeIds) ? ctx.scopeIds : []).forEach((id) => {
|
|
303
|
+
if (id === data.object) return;
|
|
304
|
+
const start = ctx.startById.get(id);
|
|
305
|
+
if (!start) return;
|
|
306
|
+
const nextPos = { x: Math.round(start.x + dx), y: Math.round(start.y + dy) };
|
|
307
|
+
core.updateObjectPositionDirect(id, nextPos, { snap: false });
|
|
308
|
+
core.eventBus.emit(Events.Object.TransformUpdated, { objectId: id });
|
|
309
|
+
});
|
|
310
|
+
}
|
|
239
311
|
});
|
|
240
312
|
|
|
241
313
|
core.eventBus.on(Events.Tool.DragEnd, (data) => {
|
|
@@ -279,5 +351,6 @@ export function setupLayerAndViewportFlow(core) {
|
|
|
279
351
|
}
|
|
280
352
|
core.dragStartPosition = null;
|
|
281
353
|
}
|
|
354
|
+
core._mindmapLiveDrag = null;
|
|
282
355
|
});
|
|
283
356
|
}
|
|
@@ -296,9 +296,14 @@ export function setupObjectLifecycleFlow(core) {
|
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
core.eventBus.on(Events.Object.ContentChange, (data) => {
|
|
299
|
-
const { objectId, oldContent, newContent } = data;
|
|
299
|
+
const { objectId, oldContent, newContent, oldSize, newSize, oldPosition, newPosition } = data;
|
|
300
300
|
if (objectId && oldContent !== undefined && newContent !== undefined && oldContent !== newContent) {
|
|
301
|
-
const command = new UpdateContentCommand(core, objectId, oldContent, newContent
|
|
301
|
+
const command = new UpdateContentCommand(core, objectId, oldContent, newContent, {
|
|
302
|
+
oldSize,
|
|
303
|
+
newSize,
|
|
304
|
+
oldPosition,
|
|
305
|
+
newPosition,
|
|
306
|
+
});
|
|
302
307
|
command.setEventBus(core.eventBus);
|
|
303
308
|
core.history.executeCommand(command);
|
|
304
309
|
}
|
|
@@ -23,12 +23,15 @@ export function setupSaveFlow(core) {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
core.eventBus.on(Events.Save.Success, async () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
26
|
+
// ВРЕМЕННО ОТКЛЮЧЕНО:
|
|
27
|
+
// cleanup-фича требует доработки контракта и серверной поддержки.
|
|
28
|
+
// Автоматический вызов удален, чтобы не запускать cleanup после сохранения.
|
|
29
|
+
// try {
|
|
30
|
+
// const result = await core.cleanupUnusedImages();
|
|
31
|
+
// if (result.deletedCount > 0) {
|
|
32
|
+
// }
|
|
33
|
+
// } catch (error) {
|
|
34
|
+
// console.warn('⚠️ Не удалось выполнить автоматическую очистку изображений:', error.message);
|
|
35
|
+
// }
|
|
33
36
|
});
|
|
34
37
|
}
|
|
@@ -95,7 +95,7 @@ export function setupTransformFlow(core) {
|
|
|
95
95
|
height: Math.max(10, snap.size.height * sy)
|
|
96
96
|
};
|
|
97
97
|
const newPos = { x: newCenter.x - newSize.width / 2, y: newCenter.y - newSize.height / 2 };
|
|
98
|
-
core.updateObjectSizeAndPositionDirect(id, newSize, newPos, snap.type || null);
|
|
98
|
+
core.updateObjectSizeAndPositionDirect(id, newSize, newPos, snap.type || null, { snap: false });
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
@@ -134,7 +134,7 @@ export function setupTransformFlow(core) {
|
|
|
134
134
|
position = resolveResizePositionFallback(core, data.object, data.size);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
core.updateObjectSizeAndPositionDirect(data.object, data.size, position, objectType);
|
|
137
|
+
core.updateObjectSizeAndPositionDirect(data.object, data.size, position, objectType, { snap: false });
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
core.eventBus.on(Events.Tool.ResizeEnd, (data) => {
|