@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.
Files changed (64) hide show
  1. package/package.json +6 -1
  2. package/src/assets/icons/mindmap.svg +3 -0
  3. package/src/core/SaveManager.js +44 -15
  4. package/src/core/commands/MindmapStatePatchCommand.js +85 -0
  5. package/src/core/commands/UpdateContentCommand.js +47 -4
  6. package/src/core/flows/LayerAndViewportFlow.js +87 -14
  7. package/src/core/flows/ObjectLifecycleFlow.js +7 -2
  8. package/src/core/flows/SaveFlow.js +10 -7
  9. package/src/core/flows/TransformFlow.js +2 -2
  10. package/src/core/index.js +81 -11
  11. package/src/core/rendering/ObjectRenderer.js +7 -2
  12. package/src/grid/BaseGrid.js +65 -0
  13. package/src/grid/CrossGrid.js +89 -24
  14. package/src/grid/CrossGridZoomPhases.js +167 -0
  15. package/src/grid/DotGrid.js +117 -34
  16. package/src/grid/DotGridZoomPhases.js +214 -16
  17. package/src/grid/GridDiagnostics.js +80 -0
  18. package/src/grid/GridFactory.js +13 -11
  19. package/src/grid/LineGrid.js +176 -37
  20. package/src/grid/LineGridZoomPhases.js +163 -0
  21. package/src/grid/ScreenGridPhaseMachine.js +51 -0
  22. package/src/mindmap/MindmapCompoundContract.js +235 -0
  23. package/src/moodboard/ActionHandler.js +1 -0
  24. package/src/moodboard/DataManager.js +57 -0
  25. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +21 -0
  26. package/src/moodboard/integration/MoodBoardEventBindings.js +26 -1
  27. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +15 -0
  28. package/src/objects/MindmapObject.js +76 -0
  29. package/src/objects/ObjectFactory.js +3 -1
  30. package/src/services/BoardService.js +127 -31
  31. package/src/services/GridSnapResolver.js +60 -0
  32. package/src/services/MiroZoomLevels.js +39 -0
  33. package/src/services/SettingsApplier.js +0 -4
  34. package/src/services/ZoomPanController.js +51 -32
  35. package/src/tools/object-tools/PlacementTool.js +12 -3
  36. package/src/tools/object-tools/SelectTool.js +11 -1
  37. package/src/tools/object-tools/placement/GhostController.js +100 -1
  38. package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -0
  39. package/src/tools/object-tools/placement/PlacementInputRouter.js +2 -2
  40. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +2 -2
  41. package/src/tools/object-tools/selection/InlineEditorController.js +15 -0
  42. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +716 -0
  43. package/src/tools/object-tools/selection/SelectInputRouter.js +6 -0
  44. package/src/tools/object-tools/selection/SelectToolSetup.js +2 -0
  45. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +12 -16
  46. package/src/ui/ContextMenu.js +6 -6
  47. package/src/ui/DotGridDebugPanel.js +253 -0
  48. package/src/ui/HtmlTextLayer.js +1 -1
  49. package/src/ui/TextPropertiesPanel.js +2 -2
  50. package/src/ui/handles/GroupSelectionHandlesController.js +4 -1
  51. package/src/ui/handles/HandlesDomRenderer.js +1486 -15
  52. package/src/ui/handles/HandlesEventBridge.js +49 -5
  53. package/src/ui/handles/HandlesInteractionController.js +4 -4
  54. package/src/ui/mindmap/MindmapConnectionLayer.js +239 -0
  55. package/src/ui/mindmap/MindmapHtmlTextLayer.js +285 -0
  56. package/src/ui/mindmap/MindmapLayoutConfig.js +29 -0
  57. package/src/ui/mindmap/MindmapTextOverlayAdapter.js +144 -0
  58. package/src/ui/styles/toolbar.css +1 -0
  59. package/src/ui/styles/workspace.css +100 -0
  60. package/src/ui/toolbar/ToolbarActionRouter.js +35 -0
  61. package/src/ui/toolbar/ToolbarPopupsController.js +6 -6
  62. package/src/ui/toolbar/ToolbarRenderer.js +1 -0
  63. package/src/ui/toolbar/ToolbarStateController.js +1 -0
  64. 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.4",
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>
@@ -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: 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
- const payload = {
444
- boardId,
445
- // отправляем «сырой» снимок; на серверной стороне допускается приём JSON
446
- boardData: data,
447
- settings: data.settings || undefined,
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.x = newCenter.x;
206
- pixiObject.y = newCenter.y;
207
- const obj = core.state.state.objects.find(o => o.id === id);
208
- if (obj) {
209
- const halfW = (pixiObject.width || 0) / 2;
210
- const halfH = (pixiObject.height || 0) / 2;
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
- try {
27
- const result = await core.cleanupUnusedImages();
28
- if (result.deletedCount > 0) {
29
- }
30
- } catch (error) {
31
- console.warn('⚠️ Не удалось выполнить автоматическую очистку изображений:', error.message);
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) => {