@sequent-org/moodboard 1.2.118 → 1.3.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 (122) hide show
  1. package/package.json +11 -1
  2. package/src/assets/icons/rotate-icon.svg +1 -1
  3. package/src/core/HistoryManager.js +16 -16
  4. package/src/core/KeyboardManager.js +48 -539
  5. package/src/core/PixiEngine.js +9 -9
  6. package/src/core/SaveManager.js +56 -31
  7. package/src/core/bootstrap/CoreInitializer.js +65 -0
  8. package/src/core/commands/DeleteObjectCommand.js +8 -0
  9. package/src/core/commands/GroupDeleteCommand.js +75 -0
  10. package/src/core/commands/GroupRotateCommand.js +6 -0
  11. package/src/core/commands/UpdateContentCommand.js +52 -0
  12. package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
  13. package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
  14. package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
  15. package/src/core/commands/UpdateTextStyleCommand.js +90 -0
  16. package/src/core/commands/index.js +6 -0
  17. package/src/core/events/Events.js +7 -0
  18. package/src/core/flows/ClipboardFlow.js +553 -0
  19. package/src/core/flows/LayerAndViewportFlow.js +283 -0
  20. package/src/core/flows/ObjectLifecycleFlow.js +336 -0
  21. package/src/core/flows/SaveFlow.js +34 -0
  22. package/src/core/flows/TransformFlow.js +277 -0
  23. package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
  24. package/src/core/index.js +41 -1765
  25. package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
  26. package/src/core/keyboard/KeyboardContextGuards.js +35 -0
  27. package/src/core/keyboard/KeyboardEventRouter.js +92 -0
  28. package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
  29. package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
  30. package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
  31. package/src/core/rendering/ObjectRenderer.js +3 -7
  32. package/src/grid/BaseGrid.js +26 -0
  33. package/src/grid/CrossGrid.js +7 -6
  34. package/src/grid/DotGrid.js +89 -33
  35. package/src/grid/DotGridZoomPhases.js +42 -0
  36. package/src/grid/LineGrid.js +22 -21
  37. package/src/moodboard/MoodBoard.js +31 -532
  38. package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
  39. package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
  40. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
  41. package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
  42. package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
  43. package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
  44. package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
  45. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
  46. package/src/objects/FileObject.js +17 -6
  47. package/src/objects/FrameObject.js +50 -10
  48. package/src/objects/NoteObject.js +5 -4
  49. package/src/services/BoardService.js +42 -2
  50. package/src/services/FrameService.js +83 -42
  51. package/src/services/ResizePolicyService.js +152 -0
  52. package/src/services/SettingsApplier.js +7 -2
  53. package/src/services/ZoomPanController.js +35 -9
  54. package/src/tools/ToolManager.js +30 -537
  55. package/src/tools/board-tools/PanTool.js +5 -11
  56. package/src/tools/manager/ToolActivationController.js +49 -0
  57. package/src/tools/manager/ToolEventRouter.js +396 -0
  58. package/src/tools/manager/ToolManagerGuards.js +33 -0
  59. package/src/tools/manager/ToolManagerLifecycle.js +110 -0
  60. package/src/tools/manager/ToolRegistry.js +33 -0
  61. package/src/tools/object-tools/DrawingTool.js +48 -14
  62. package/src/tools/object-tools/PlacementTool.js +50 -1049
  63. package/src/tools/object-tools/PlacementToolV2.js +88 -0
  64. package/src/tools/object-tools/SelectTool.js +174 -2681
  65. package/src/tools/object-tools/placement/GhostController.js +504 -0
  66. package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
  67. package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
  68. package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
  69. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
  70. package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
  71. package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
  72. package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
  73. package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
  74. package/src/tools/object-tools/selection/CursorController.js +78 -0
  75. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
  76. package/src/tools/object-tools/selection/HitTestService.js +102 -0
  77. package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
  78. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
  79. package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
  80. package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
  81. package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
  82. package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
  83. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
  84. package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
  85. package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
  86. package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
  87. package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
  88. package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
  89. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
  90. package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
  91. package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
  92. package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
  93. package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
  94. package/src/ui/FilePropertiesPanel.js +61 -32
  95. package/src/ui/FramePropertiesPanel.js +176 -101
  96. package/src/ui/HtmlHandlesLayer.js +121 -976
  97. package/src/ui/MapPanel.js +12 -7
  98. package/src/ui/NotePropertiesPanel.js +17 -2
  99. package/src/ui/TextPropertiesPanel.js +124 -738
  100. package/src/ui/Toolbar.js +71 -1180
  101. package/src/ui/Topbar.js +23 -25
  102. package/src/ui/ZoomPanel.js +16 -5
  103. package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
  104. package/src/ui/handles/HandlesDomRenderer.js +278 -0
  105. package/src/ui/handles/HandlesEventBridge.js +102 -0
  106. package/src/ui/handles/HandlesInteractionController.js +772 -0
  107. package/src/ui/handles/HandlesPositioningService.js +206 -0
  108. package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
  109. package/src/ui/styles/toolbar.css +2 -0
  110. package/src/ui/styles/workspace.css +13 -6
  111. package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
  112. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
  113. package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
  114. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
  115. package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
  116. package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
  117. package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
  118. package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
  119. package/src/ui/toolbar/ToolbarRenderer.js +97 -0
  120. package/src/ui/toolbar/ToolbarStateController.js +79 -0
  121. package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
  122. package/src/utils/emojiLoaderNoBundler.js +1 -1
@@ -211,12 +211,14 @@ export class PixiEngine {
211
211
  if (pixiObject instanceof PIXI.Sprite) {
212
212
  console.log('🗑️ PixiEngine: очищаем ресурсы изображения/эмоджи');
213
213
 
214
- // Очищаем текстуру (особенно важно для data URL)
214
+ // Очищаем текстуру (data URL, blob URL — освобождаем память)
215
215
  if (pixiObject.texture && pixiObject.texture !== PIXI.Texture.WHITE) {
216
- // Не уничтожаем базовые текстуры PIXI
217
216
  const textureSource = pixiObject.texture.baseTexture?.resource?.src;
218
- if (textureSource && (textureSource.startsWith('data:') || textureSource.includes('emodji'))) {
219
- pixiObject.texture.destroy(false); // Уничтожаем только созданную текстуру
217
+ if (textureSource && (textureSource.startsWith('data:') || textureSource.startsWith('blob:') || textureSource.includes('emodji'))) {
218
+ if (textureSource.startsWith('blob:')) {
219
+ try { URL.revokeObjectURL(textureSource); } catch (_) {}
220
+ }
221
+ pixiObject.texture.destroy(false);
220
222
  }
221
223
  }
222
224
 
@@ -381,12 +383,10 @@ export class PixiEngine {
381
383
  */
382
384
  setFrameFill(objectId, width, height, fillColor = 0xFFFFFF) {
383
385
  const pixiObject = this.objects.get(objectId);
384
- if (!pixiObject || !(pixiObject instanceof PIXI.Graphics)) return;
386
+ if (!pixiObject) return;
385
387
  const meta = pixiObject._mb || {};
386
- if (meta.type !== 'frame') return;
387
- if (meta.instance) {
388
- meta.instance.setFill(fillColor);
389
- }
388
+ if (meta.type !== 'frame' || !meta.instance) return;
389
+ meta.instance.setFill(fillColor);
390
390
  }
391
391
 
392
392
  /**
@@ -30,6 +30,10 @@ export class SaveManager {
30
30
  this.retryCount = 0;
31
31
  this.lastSavedData = null;
32
32
  this.hasUnsavedChanges = false;
33
+ this.periodicSaveTimer = null;
34
+ this._listenersAttached = false;
35
+ this._handlers = {};
36
+ this._domHandlers = {};
33
37
 
34
38
  // Состояния сохранения
35
39
  this.saveStatus = 'idle'; // idle, saving, saved, error
@@ -48,40 +52,45 @@ export class SaveManager {
48
52
  * Настройка обработчиков событий
49
53
  */
50
54
  setupEventListeners() {
51
- if (!this.options.autoSave) return;
55
+ if (!this.options.autoSave || this._listenersAttached) return;
56
+ this._listenersAttached = true;
57
+
58
+ this._handlers.onGridBoardDataChanged = () => {
59
+ this.markAsChanged();
60
+ };
61
+ this._handlers.onObjectCreated = () => {
62
+ this.markAsChanged();
63
+ };
64
+ this._handlers.onObjectUpdated = () => {
65
+ this.markAsChanged();
66
+ };
67
+ this._handlers.onObjectDeleted = () => {
68
+ this.markAsChanged();
69
+ };
70
+ this._handlers.onObjectStateChanged = () => {
71
+ this.markAsChanged();
72
+ };
52
73
 
53
74
  // Отслеживаем изменения сетки: не передаём частичные данные в сохранение,
54
75
  // чтобы собрать полный snapshot через getBoardData()
55
- this.eventBus.on(Events.Grid.BoardDataChanged, () => {
56
- this.markAsChanged();
57
- });
76
+ this.eventBus.on(Events.Grid.BoardDataChanged, this._handlers.onGridBoardDataChanged);
58
77
 
59
78
  // Отслеживаем создание объектов
60
- this.eventBus.on(Events.Object.Created, () => {
61
- this.markAsChanged();
62
- });
79
+ this.eventBus.on(Events.Object.Created, this._handlers.onObjectCreated);
63
80
 
64
81
  // Отслеживаем изменения объектов
65
- this.eventBus.on(Events.Object.Updated, (data) => {
66
-
67
- this.markAsChanged();
68
- });
82
+ this.eventBus.on(Events.Object.Updated, this._handlers.onObjectUpdated);
69
83
 
70
84
  // Отслеживаем удаление объектов
71
- this.eventBus.on(Events.Object.Deleted, () => {
72
- this.markAsChanged();
73
- });
85
+ this.eventBus.on(Events.Object.Deleted, this._handlers.onObjectDeleted);
74
86
 
75
87
  // Отслеживаем прямые изменения состояния (для Undo/Redo)
76
- this.eventBus.on(Events.Object.StateChanged, (data) => {
77
-
78
- this.markAsChanged();
79
- });
88
+ this.eventBus.on(Events.Object.StateChanged, this._handlers.onObjectStateChanged);
80
89
 
81
90
  // Отслеживание перемещений теперь происходит через команды и state:changed
82
91
 
83
92
  // Сохранение при закрытии страницы (в том числе при резком закрытии окна)
84
- window.addEventListener('beforeunload', (e) => {
93
+ this._domHandlers.beforeUnload = (e) => {
85
94
  if (!this.hasUnsavedChanges) return;
86
95
  try {
87
96
  if (this.options.useBeaconOnUnload) {
@@ -97,10 +106,11 @@ export class SaveManager {
97
106
  e.returnedValue = '';
98
107
  e.returnValue = '';
99
108
  return '';
100
- }, { capture: true });
109
+ };
110
+ window.addEventListener('beforeunload', this._domHandlers.beforeUnload, { capture: true });
101
111
 
102
112
  // Дополнительно: обработка быстрого ухода со страницы (pagehide надёжнее в части браузеров)
103
- window.addEventListener('pagehide', () => {
113
+ this._domHandlers.pageHide = () => {
104
114
  if (!this.hasUnsavedChanges) return;
105
115
  try {
106
116
  if (!this.options || this.options.useBeaconOnUnload) {
@@ -109,10 +119,11 @@ export class SaveManager {
109
119
  this._flushSyncFallback();
110
120
  }
111
121
  } catch (_) { /* игнорируем */ }
112
- }, { capture: true });
122
+ };
123
+ window.addEventListener('pagehide', this._domHandlers.pageHide, { capture: true });
113
124
 
114
125
  // Подстраховка на случай, когда вкладка просто уходит в фон без beforeunload/pagehide
115
- document.addEventListener('visibilitychange', () => {
126
+ this._domHandlers.visibilityChange = () => {
116
127
  if (document.visibilityState !== 'hidden') return;
117
128
  if (!this.hasUnsavedChanges) return;
118
129
  try {
@@ -122,11 +133,12 @@ export class SaveManager {
122
133
  this._flushSyncFallback();
123
134
  }
124
135
  } catch (_) { /* игнорируем */ }
125
- });
136
+ };
137
+ document.addEventListener('visibilitychange', this._domHandlers.visibilityChange);
126
138
 
127
139
  // Периодическое автосохранение
128
140
  if (this.options.autoSave) {
129
- setInterval(() => {
141
+ this.periodicSaveTimer = setInterval(() => {
130
142
  if (this.hasUnsavedChanges && !this.isRequestInProgress) {
131
143
  this.saveImmediately();
132
144
  }
@@ -489,6 +501,11 @@ export class SaveManager {
489
501
  destroy() {
490
502
  if (this.saveTimer) {
491
503
  clearTimeout(this.saveTimer);
504
+ this.saveTimer = null;
505
+ }
506
+ if (this.periodicSaveTimer) {
507
+ clearInterval(this.periodicSaveTimer);
508
+ this.periodicSaveTimer = null;
492
509
  }
493
510
 
494
511
  // Финальное сохранение перед уничтожением
@@ -496,11 +513,19 @@ export class SaveManager {
496
513
  this.saveImmediately();
497
514
  }
498
515
 
499
- // Удаляем обработчики событий (константы)
500
- this.eventBus.off(Events.Grid.BoardDataChanged);
501
- this.eventBus.off(Events.Object.Created);
502
- this.eventBus.off(Events.Object.Updated);
503
- this.eventBus.off(Events.Object.Deleted);
504
- this.eventBus.off(Events.Tool.DragEnd);
516
+ // Удаляем обработчики событий, передавая исходные callback-ссылки.
517
+ if (this._handlers.onGridBoardDataChanged) this.eventBus.off(Events.Grid.BoardDataChanged, this._handlers.onGridBoardDataChanged);
518
+ if (this._handlers.onObjectCreated) this.eventBus.off(Events.Object.Created, this._handlers.onObjectCreated);
519
+ if (this._handlers.onObjectUpdated) this.eventBus.off(Events.Object.Updated, this._handlers.onObjectUpdated);
520
+ if (this._handlers.onObjectDeleted) this.eventBus.off(Events.Object.Deleted, this._handlers.onObjectDeleted);
521
+ if (this._handlers.onObjectStateChanged) this.eventBus.off(Events.Object.StateChanged, this._handlers.onObjectStateChanged);
522
+
523
+ if (this._domHandlers.beforeUnload) window.removeEventListener('beforeunload', this._domHandlers.beforeUnload, { capture: true });
524
+ if (this._domHandlers.pageHide) window.removeEventListener('pagehide', this._domHandlers.pageHide, { capture: true });
525
+ if (this._domHandlers.visibilityChange) document.removeEventListener('visibilitychange', this._domHandlers.visibilityChange);
526
+
527
+ this._listenersAttached = false;
528
+ this._handlers = {};
529
+ this._domHandlers = {};
505
530
  }
506
531
  }
@@ -0,0 +1,65 @@
1
+ import { ToolManager } from '../../tools/ToolManager.js';
2
+ import { SelectTool } from '../../tools/object-tools/SelectTool.js';
3
+ import { BoardService } from '../../services/BoardService.js';
4
+ import { ZoomPanController } from '../../services/ZoomPanController.js';
5
+ import { ZOrderManager } from '../../services/ZOrderManager.js';
6
+ import { FrameService } from '../../services/FrameService.js';
7
+
8
+ export async function initializeCore(core) {
9
+ try {
10
+ await core.pixi.init();
11
+ core.keyboard.startListening();
12
+ await initializeCoreTools(core);
13
+
14
+ core.boardService = new BoardService(core.eventBus, core.pixi);
15
+ await core.boardService.init(() => (core.workspaceSize?.() || { width: core.options.width, height: core.options.height }));
16
+ core.zoomPan = new ZoomPanController(core.eventBus, core.pixi);
17
+ core.zoomPan.attach();
18
+ core.zOrder = new ZOrderManager(core.eventBus, core.pixi, core.state);
19
+ core.zOrder.attach();
20
+ core.frameService = new FrameService(core.eventBus, core.pixi, core.state);
21
+ core.frameService.attach();
22
+
23
+ core.state.loadBoard({
24
+ id: core.options.boardId || 'demo',
25
+ name: 'Demo Board',
26
+ objects: [],
27
+ viewport: { x: 0, y: 0, zoom: 1 }
28
+ });
29
+ } catch (error) {
30
+ console.error('MoodBoard init failed:', error);
31
+ }
32
+ }
33
+
34
+ export async function initializeCoreTools(core) {
35
+ const canvasElement = core.pixi.app.view;
36
+ core.workspaceSize = () => ({ width: canvasElement.clientWidth, height: canvasElement.clientHeight });
37
+ core.toolManager = new ToolManager(core.eventBus, canvasElement, core.pixi.app, core);
38
+
39
+ const selectTool = new SelectTool(core.eventBus);
40
+ core.toolManager.registerTool(selectTool);
41
+
42
+ const panToolModule = await import('../../tools/board-tools/PanTool.js');
43
+ const panTool = new panToolModule.PanTool(core.eventBus);
44
+ core.toolManager.registerTool(panTool);
45
+
46
+ const drawingToolModule = await import('../../tools/object-tools/DrawingTool.js');
47
+ const drawingTool = new drawingToolModule.DrawingTool(core.eventBus);
48
+ core.toolManager.registerTool(drawingTool);
49
+
50
+ const placementToolModule = await import('../../tools/object-tools/PlacementTool.js');
51
+ const placementTool = new placementToolModule.PlacementTool(core.eventBus, core);
52
+ core.toolManager.registerTool(placementTool);
53
+
54
+ const textToolModule = await import('../../tools/object-tools/TextTool.js');
55
+ const textTool = new textToolModule.TextTool(core.eventBus);
56
+ core.toolManager.registerTool(textTool);
57
+
58
+ core.selectTool = selectTool;
59
+ core.toolManager.activateTool('select');
60
+
61
+ core.setupToolEvents();
62
+ core.setupKeyboardEvents();
63
+ core.setupSaveEvents();
64
+ core.setupHistoryEvents();
65
+ }
@@ -61,6 +61,14 @@ export class DeleteObjectCommand extends BaseCommand {
61
61
  this.coreMoodboard.pixi.removeObject(this.objectId);
62
62
 
63
63
  console.log('🗑️ DeleteObjectCommand: объект удален из state и PIXI');
64
+
65
+ // Освобождаем blob URL у изображений (утечка памяти при fallback без upload)
66
+ const blobSrc = this.objectData?.properties?.src || this.objectData?.src;
67
+ if (typeof blobSrc === 'string' && blobSrc.startsWith('blob:')) {
68
+ try {
69
+ URL.revokeObjectURL(blobSrc);
70
+ } catch (_) {}
71
+ }
64
72
 
65
73
  // Если это файловый объект с fileId, удаляем файл с сервера
66
74
  if (this.fileIdToDelete && this.coreMoodboard.fileUploadService) {
@@ -0,0 +1,75 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ import { Events } from '../events/Events.js';
3
+
4
+ /**
5
+ * Команда группового удаления объектов.
6
+ * Один Undo восстанавливает всю группу.
7
+ */
8
+ export class GroupDeleteCommand extends BaseCommand {
9
+ constructor(coreMoodboard, objectIds) {
10
+ super('group_delete', `Удалить группу (${objectIds.length} объектов)`);
11
+ this.coreMoodboard = coreMoodboard;
12
+ this.objectIds = Array.isArray(objectIds) ? [...objectIds] : [];
13
+
14
+ const objects = this.coreMoodboard.state.getObjects();
15
+ this.objectsData = [];
16
+ for (const id of this.objectIds) {
17
+ const obj = objects.find((o) => o.id === id);
18
+ if (obj) {
19
+ const data = JSON.parse(JSON.stringify(obj));
20
+ if (data.type === 'image') {
21
+ if (data.imageId) {
22
+ const imageUrl = `/api/images/${data.imageId}/file`;
23
+ data.src = imageUrl;
24
+ if (!data.properties) data.properties = {};
25
+ data.properties.src = imageUrl;
26
+ }
27
+ }
28
+ this.objectsData.push({ id, data });
29
+ }
30
+ }
31
+ }
32
+
33
+ async execute() {
34
+ for (const { id, data } of this.objectsData) {
35
+ this.coreMoodboard.state.removeObject(id);
36
+ this.coreMoodboard.pixi.removeObject(id);
37
+
38
+ const blobSrc = data?.properties?.src || data?.src;
39
+ if (typeof blobSrc === 'string' && blobSrc.startsWith('blob:')) {
40
+ try {
41
+ URL.revokeObjectURL(blobSrc);
42
+ } catch (_) {}
43
+ }
44
+
45
+ if (data.type === 'file' && data.fileId && this.coreMoodboard.fileUploadService) {
46
+ try {
47
+ await this.coreMoodboard.fileUploadService.deleteFile(data.fileId);
48
+ } catch (_) {}
49
+ }
50
+
51
+ this.coreMoodboard.eventBus.emit(Events.Object.Deleted, { objectId: id });
52
+ }
53
+ }
54
+
55
+ undo() {
56
+ for (const { id, data } of this.objectsData) {
57
+ if (data.type === 'file' && data.fileId) {
58
+ const restored = { ...data };
59
+ if (restored.properties) {
60
+ restored.properties = {
61
+ ...restored.properties,
62
+ fileName: `[УДАЛЕН] ${restored.properties.fileName || 'файл'}`,
63
+ isDeleted: true,
64
+ };
65
+ }
66
+ this.coreMoodboard.state.addObject(restored);
67
+ this.coreMoodboard.pixi.createObject(restored);
68
+ } else {
69
+ this.coreMoodboard.state.addObject(data);
70
+ this.coreMoodboard.pixi.createObject(data);
71
+ }
72
+ this.coreMoodboard.eventBus.emit(Events.Object.Created, { objectId: id, objectData: data });
73
+ }
74
+ }
75
+ }
@@ -17,6 +17,9 @@ export class GroupRotateCommand extends BaseCommand {
17
17
 
18
18
  execute() {
19
19
  for (const c of this.changes) {
20
+ if (this.core.pixi?.updateObjectRotation) {
21
+ this.core.pixi.updateObjectRotation(c.id, c.toAngle);
22
+ }
20
23
  this.core.updateObjectRotationDirect(c.id, c.toAngle);
21
24
  this.core.updateObjectPositionDirect(c.id, c.toPos);
22
25
  this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'rotation', angle: c.toAngle });
@@ -26,6 +29,9 @@ export class GroupRotateCommand extends BaseCommand {
26
29
 
27
30
  undo() {
28
31
  for (const c of this.changes) {
32
+ if (this.core.pixi?.updateObjectRotation) {
33
+ this.core.pixi.updateObjectRotation(c.id, c.fromAngle);
34
+ }
29
35
  this.core.updateObjectRotationDirect(c.id, c.fromAngle);
30
36
  this.core.updateObjectPositionDirect(c.id, c.fromPos);
31
37
  this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'rotation', angle: c.fromAngle });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Команда изменения содержимого текста/записки для системы Undo/Redo
3
+ */
4
+ import { BaseCommand } from './BaseCommand.js';
5
+ import { Events } from '../events/Events.js';
6
+
7
+ export class UpdateContentCommand extends BaseCommand {
8
+ constructor(coreMoodboard, objectId, oldContent, newContent) {
9
+ super('update_content', `Изменить текст`);
10
+ this.coreMoodboard = coreMoodboard;
11
+ this.objectId = objectId;
12
+ this.oldContent = oldContent;
13
+ this.newContent = newContent;
14
+ }
15
+
16
+ execute() {
17
+ this._applyContent(this.newContent);
18
+ }
19
+
20
+ undo() {
21
+ this._applyContent(this.oldContent);
22
+ }
23
+
24
+ canMergeWith(otherCommand) {
25
+ return otherCommand instanceof UpdateContentCommand &&
26
+ otherCommand.objectId === this.objectId;
27
+ }
28
+
29
+ mergeWith(otherCommand) {
30
+ if (!this.canMergeWith(otherCommand)) {
31
+ throw new Error('Cannot merge commands');
32
+ }
33
+ this.newContent = otherCommand.newContent;
34
+ this.timestamp = otherCommand.timestamp;
35
+ }
36
+
37
+ _applyContent(content) {
38
+ const objects = this.coreMoodboard.state.getObjects();
39
+ const object = objects.find((obj) => obj.id === this.objectId);
40
+ if (object) {
41
+ if (!object.properties) {
42
+ object.properties = {};
43
+ }
44
+ object.properties.content = content;
45
+ this.coreMoodboard.state.markDirty();
46
+ }
47
+ this.emit(Events.Tool.UpdateObjectContent, {
48
+ objectId: this.objectId,
49
+ content,
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Команда изменения свойств фрейма (название, фон, тип, lockedAspect) для системы Undo/Redo.
3
+ * Поддерживает: title, backgroundColor, type, lockedAspect.
4
+ */
5
+ import { BaseCommand } from './BaseCommand.js';
6
+ import { Events } from '../events/Events.js';
7
+
8
+ const FRAME_PROP_LABELS = {
9
+ title: 'название',
10
+ backgroundColor: 'фон',
11
+ type: 'тип',
12
+ lockedAspect: 'фиксация пропорций',
13
+ };
14
+
15
+ export class UpdateFramePropertiesCommand extends BaseCommand {
16
+ /**
17
+ * @param {Object} coreMoodboard — ядро доски
18
+ * @param {string} objectId — id объекта фрейма
19
+ * @param {string} property — имя свойства (title | backgroundColor | type | lockedAspect)
20
+ * @param {*} oldValue — прежнее значение
21
+ * @param {*} newValue — новое значение
22
+ */
23
+ constructor(coreMoodboard, objectId, property, oldValue, newValue) {
24
+ super('update_frame_properties', `Изменить ${FRAME_PROP_LABELS[property] || property}`);
25
+ this.coreMoodboard = coreMoodboard;
26
+ this.objectId = objectId;
27
+ this.property = property;
28
+ this.oldValue = oldValue;
29
+ this.newValue = newValue;
30
+ }
31
+
32
+ execute() {
33
+ this._apply(this.newValue);
34
+ }
35
+
36
+ undo() {
37
+ this._apply(this.oldValue);
38
+ }
39
+
40
+ canMergeWith(otherCommand) {
41
+ return otherCommand instanceof UpdateFramePropertiesCommand &&
42
+ otherCommand.objectId === this.objectId &&
43
+ otherCommand.property === this.property;
44
+ }
45
+
46
+ mergeWith(otherCommand) {
47
+ if (!this.canMergeWith(otherCommand)) {
48
+ throw new Error('Cannot merge commands');
49
+ }
50
+ this.newValue = otherCommand.newValue;
51
+ this.timestamp = otherCommand.timestamp;
52
+ }
53
+
54
+ _apply(value) {
55
+ const { coreMoodboard, objectId, property } = this;
56
+ const objects = coreMoodboard.state.getObjects();
57
+ const object = objects.find((obj) => obj.id === objectId);
58
+ if (!object) return;
59
+
60
+ if (property === 'backgroundColor') {
61
+ object.backgroundColor = value;
62
+ } else {
63
+ if (!object.properties) object.properties = {};
64
+ object.properties[property] = value;
65
+ if (property === 'type') {
66
+ object.properties.lockedAspect = (value !== 'custom');
67
+ }
68
+ }
69
+
70
+ coreMoodboard.state.markDirty();
71
+
72
+ const pixiObject = coreMoodboard.pixi?.objects?.get(objectId);
73
+ if (pixiObject?._mb?.instance) {
74
+ const instance = pixiObject._mb.instance;
75
+ if (property === 'title' && instance.setTitle) {
76
+ instance.setTitle(value);
77
+ }
78
+ if (property === 'backgroundColor' && instance.setBackgroundColor) {
79
+ instance.setBackgroundColor(value);
80
+ }
81
+ }
82
+
83
+ let updates;
84
+ if (property === 'backgroundColor') {
85
+ updates = { backgroundColor: value };
86
+ } else {
87
+ updates = { properties: { [property]: value } };
88
+ if (property === 'type') {
89
+ updates.properties.lockedAspect = (value !== 'custom');
90
+ }
91
+ }
92
+
93
+ coreMoodboard.eventBus.emit(Events.Object.StateChanged, {
94
+ objectId,
95
+ updates,
96
+ });
97
+ }
98
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Команда смены типа фрейма (type + lockedAspect + размер + позиция) — одно действие в истории.
3
+ */
4
+ import { BaseCommand } from './BaseCommand.js';
5
+ import { Events } from '../events/Events.js';
6
+
7
+ export class UpdateFrameTypeCommand extends BaseCommand {
8
+ /**
9
+ * @param {Object} coreMoodboard
10
+ * @param {string} objectId
11
+ * @param {string} oldType
12
+ * @param {string} newType
13
+ * @param {{ width: number, height: number }} oldSize
14
+ * @param {{ width: number, height: number }} newSize
15
+ * @param {{ x: number, y: number }} oldPosition
16
+ * @param {{ x: number, y: number }} newPosition
17
+ */
18
+ constructor(coreMoodboard, objectId, oldType, newType, oldSize, newSize, oldPosition, newPosition) {
19
+ super('update_frame_type', `Изменить тип фрейма: ${oldType} → ${newType}`);
20
+ this.coreMoodboard = coreMoodboard;
21
+ this.objectId = objectId;
22
+ this.oldType = oldType;
23
+ this.newType = newType;
24
+ this.oldSize = oldSize ? { ...oldSize } : null;
25
+ this.newSize = newSize ? { ...newSize } : null;
26
+ this.oldPosition = oldPosition ? { ...oldPosition } : null;
27
+ this.newPosition = newPosition ? { ...newPosition } : null;
28
+ }
29
+
30
+ execute() {
31
+ this._apply(this.newType, this.newSize, this.newPosition);
32
+ }
33
+
34
+ undo() {
35
+ this._apply(this.oldType, this.oldSize, this.oldPosition);
36
+ }
37
+
38
+ _apply(typeValue, size, position) {
39
+ const { coreMoodboard, objectId } = this;
40
+ const objects = coreMoodboard.state.getObjects();
41
+ const object = objects.find((obj) => obj.id === objectId);
42
+ if (!object) return;
43
+
44
+ if (!object.properties) object.properties = {};
45
+ object.properties.type = typeValue;
46
+ object.properties.lockedAspect = (typeValue !== 'custom');
47
+
48
+ if (size) {
49
+ object.width = size.width;
50
+ object.height = size.height;
51
+ coreMoodboard.pixi.updateObjectSize(objectId, size, 'frame');
52
+ }
53
+
54
+ if (position) {
55
+ const pixiObject = coreMoodboard.pixi?.objects?.get(objectId);
56
+ if (pixiObject) {
57
+ const halfW = (object.width || 0) / 2;
58
+ const halfH = (object.height || 0) / 2;
59
+ pixiObject.x = position.x + halfW;
60
+ pixiObject.y = position.y + halfH;
61
+ }
62
+ object.position = object.position || { x: 0, y: 0 };
63
+ object.position.x = position.x;
64
+ object.position.y = position.y;
65
+ }
66
+
67
+ coreMoodboard.state.markDirty();
68
+
69
+ coreMoodboard.eventBus.emit(Events.Object.StateChanged, {
70
+ objectId,
71
+ updates: {
72
+ properties: { type: typeValue, lockedAspect: (typeValue !== 'custom') },
73
+ },
74
+ });
75
+
76
+ if (coreMoodboard.eventBus) {
77
+ coreMoodboard.eventBus.emit(Events.Object.TransformUpdated, {
78
+ objectId,
79
+ type: 'resize',
80
+ size: object.width && object.height ? { width: object.width, height: object.height } : null,
81
+ position: object.position,
82
+ });
83
+ }
84
+ }
85
+ }