@sequent-org/moodboard 1.0.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 (123) hide show
  1. package/package.json +44 -0
  2. package/src/assets/icons/README.md +105 -0
  3. package/src/assets/icons/attachments.svg +3 -0
  4. package/src/assets/icons/clear.svg +5 -0
  5. package/src/assets/icons/comments.svg +3 -0
  6. package/src/assets/icons/emoji.svg +6 -0
  7. package/src/assets/icons/frame.svg +3 -0
  8. package/src/assets/icons/image.svg +3 -0
  9. package/src/assets/icons/note.svg +3 -0
  10. package/src/assets/icons/pan.svg +3 -0
  11. package/src/assets/icons/pencil.svg +3 -0
  12. package/src/assets/icons/redo.svg +3 -0
  13. package/src/assets/icons/select.svg +9 -0
  14. package/src/assets/icons/shapes.svg +3 -0
  15. package/src/assets/icons/text-add.svg +3 -0
  16. package/src/assets/icons/topbar/README.md +39 -0
  17. package/src/assets/icons/topbar/grid-cross.svg +6 -0
  18. package/src/assets/icons/topbar/grid-dot.svg +3 -0
  19. package/src/assets/icons/topbar/grid-line.svg +3 -0
  20. package/src/assets/icons/topbar/grid-off.svg +3 -0
  21. package/src/assets/icons/topbar/paint.svg +3 -0
  22. package/src/assets/icons/undo.svg +3 -0
  23. package/src/core/ApiClient.js +309 -0
  24. package/src/core/EventBus.js +42 -0
  25. package/src/core/HistoryManager.js +261 -0
  26. package/src/core/KeyboardManager.js +710 -0
  27. package/src/core/PixiEngine.js +439 -0
  28. package/src/core/SaveManager.js +381 -0
  29. package/src/core/StateManager.js +64 -0
  30. package/src/core/commands/BaseCommand.js +68 -0
  31. package/src/core/commands/CopyObjectCommand.js +44 -0
  32. package/src/core/commands/CreateObjectCommand.js +46 -0
  33. package/src/core/commands/DeleteObjectCommand.js +146 -0
  34. package/src/core/commands/EditFileNameCommand.js +107 -0
  35. package/src/core/commands/GroupMoveCommand.js +47 -0
  36. package/src/core/commands/GroupReorderZCommand.js +74 -0
  37. package/src/core/commands/GroupResizeCommand.js +37 -0
  38. package/src/core/commands/GroupRotateCommand.js +41 -0
  39. package/src/core/commands/MoveObjectCommand.js +89 -0
  40. package/src/core/commands/PasteObjectCommand.js +103 -0
  41. package/src/core/commands/ReorderZCommand.js +45 -0
  42. package/src/core/commands/ResizeObjectCommand.js +135 -0
  43. package/src/core/commands/RotateObjectCommand.js +70 -0
  44. package/src/core/commands/index.js +14 -0
  45. package/src/core/events/Events.js +147 -0
  46. package/src/core/index.js +1632 -0
  47. package/src/core/rendering/GeometryUtils.js +89 -0
  48. package/src/core/rendering/HitTestManager.js +186 -0
  49. package/src/core/rendering/LayerManager.js +137 -0
  50. package/src/core/rendering/ObjectRenderer.js +363 -0
  51. package/src/core/rendering/PixiRenderer.js +140 -0
  52. package/src/core/rendering/index.js +9 -0
  53. package/src/grid/BaseGrid.js +164 -0
  54. package/src/grid/CrossGrid.js +75 -0
  55. package/src/grid/DotGrid.js +148 -0
  56. package/src/grid/GridFactory.js +173 -0
  57. package/src/grid/LineGrid.js +115 -0
  58. package/src/index.js +2 -0
  59. package/src/moodboard/ActionHandler.js +114 -0
  60. package/src/moodboard/DataManager.js +114 -0
  61. package/src/moodboard/MoodBoard.js +359 -0
  62. package/src/moodboard/WorkspaceManager.js +103 -0
  63. package/src/objects/BaseObject.js +1 -0
  64. package/src/objects/CommentObject.js +115 -0
  65. package/src/objects/DrawingObject.js +114 -0
  66. package/src/objects/EmojiObject.js +98 -0
  67. package/src/objects/FileObject.js +318 -0
  68. package/src/objects/FrameObject.js +127 -0
  69. package/src/objects/ImageObject.js +72 -0
  70. package/src/objects/NoteObject.js +227 -0
  71. package/src/objects/ObjectFactory.js +61 -0
  72. package/src/objects/ShapeObject.js +134 -0
  73. package/src/objects/StampObject.js +0 -0
  74. package/src/objects/StickerObject.js +0 -0
  75. package/src/objects/TextObject.js +123 -0
  76. package/src/services/BoardService.js +85 -0
  77. package/src/services/FileUploadService.js +398 -0
  78. package/src/services/FrameService.js +138 -0
  79. package/src/services/ImageUploadService.js +246 -0
  80. package/src/services/ZOrderManager.js +50 -0
  81. package/src/services/ZoomPanController.js +78 -0
  82. package/src/src.7z +0 -0
  83. package/src/src.zip +0 -0
  84. package/src/src2.zip +0 -0
  85. package/src/tools/AlignmentGuides.js +326 -0
  86. package/src/tools/BaseTool.js +257 -0
  87. package/src/tools/ResizeHandles.js +381 -0
  88. package/src/tools/ToolManager.js +580 -0
  89. package/src/tools/board-tools/PanTool.js +43 -0
  90. package/src/tools/board-tools/ZoomTool.js +393 -0
  91. package/src/tools/object-tools/DrawingTool.js +404 -0
  92. package/src/tools/object-tools/PlacementTool.js +1005 -0
  93. package/src/tools/object-tools/SelectTool.js +2183 -0
  94. package/src/tools/object-tools/TextTool.js +416 -0
  95. package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
  96. package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
  97. package/src/tools/object-tools/selection/GroupDragController.js +61 -0
  98. package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
  99. package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
  100. package/src/tools/object-tools/selection/HandlesSync.js +96 -0
  101. package/src/tools/object-tools/selection/ResizeController.js +68 -0
  102. package/src/tools/object-tools/selection/RotateController.js +58 -0
  103. package/src/tools/object-tools/selection/SelectionModel.js +42 -0
  104. package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
  105. package/src/ui/CommentPopover.js +187 -0
  106. package/src/ui/ContextMenu.js +340 -0
  107. package/src/ui/FilePropertiesPanel.js +298 -0
  108. package/src/ui/FramePropertiesPanel.js +462 -0
  109. package/src/ui/HtmlHandlesLayer.js +778 -0
  110. package/src/ui/HtmlTextLayer.js +279 -0
  111. package/src/ui/MapPanel.js +290 -0
  112. package/src/ui/NotePropertiesPanel.js +502 -0
  113. package/src/ui/SaveStatus.js +250 -0
  114. package/src/ui/TextPropertiesPanel.js +911 -0
  115. package/src/ui/Toolbar.js +1118 -0
  116. package/src/ui/Topbar.js +220 -0
  117. package/src/ui/ZoomPanel.js +116 -0
  118. package/src/ui/styles/workspace.css +854 -0
  119. package/src/utils/colors.js +0 -0
  120. package/src/utils/geometry.js +0 -0
  121. package/src/utils/iconLoader.js +270 -0
  122. package/src/utils/objectIdGenerator.js +17 -0
  123. package/src/utils/topbarIconLoader.js +114 -0
@@ -0,0 +1,309 @@
1
+ // src/core/ApiClient.js
2
+ export class ApiClient {
3
+ constructor(baseUrl, authToken = null) {
4
+ this.baseUrl = baseUrl;
5
+ this.authToken = authToken;
6
+ }
7
+
8
+ async getBoard(boardId) {
9
+ try {
10
+ const response = await fetch(`/api/moodboard/${boardId}`, {
11
+ method: 'GET',
12
+ headers: {
13
+ 'Accept': 'application/json',
14
+ 'X-Requested-With': 'XMLHttpRequest'
15
+ },
16
+ credentials: 'same-origin'
17
+ });
18
+
19
+ if (!response.ok) {
20
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
21
+ }
22
+
23
+ const result = await response.json();
24
+
25
+ if (result.success) {
26
+ return { data: result.data };
27
+ } else {
28
+ throw new Error(result.message || 'Ошибка загрузки доски');
29
+ }
30
+ } catch (error) {
31
+ console.warn('API: Ошибка загрузки доски, используем заглушку:', error);
32
+ // Fallback к заглушке
33
+ return {
34
+ data: {
35
+ id: boardId,
36
+ name: 'Demo Board',
37
+ objects: []
38
+ }
39
+ };
40
+ }
41
+ }
42
+
43
+ async saveBoard(boardId, boardData) {
44
+ try {
45
+ // Фильтруем объекты изображений и файлов - убираем избыточные данные
46
+ const cleanedData = this._cleanObjectData(boardData);
47
+
48
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
49
+
50
+ const response = await fetch('/api/moodboard/save', {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'Accept': 'application/json',
55
+ 'X-CSRF-TOKEN': csrfToken,
56
+ 'X-Requested-With': 'XMLHttpRequest'
57
+ },
58
+ credentials: 'same-origin',
59
+ body: JSON.stringify({
60
+ boardId: boardId,
61
+ boardData: cleanedData
62
+ })
63
+ });
64
+
65
+ if (!response.ok) {
66
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
67
+ }
68
+
69
+ const result = await response.json();
70
+
71
+ if (result.success) {
72
+ return { success: true, data: result };
73
+ } else {
74
+ throw new Error(result.message || 'Ошибка сохранения доски');
75
+ }
76
+ } catch (error) {
77
+ console.error('ApiClient: Ошибка сохранения доски:', error);
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Очищает данные объектов от избыточной информации
84
+ * @private
85
+ */
86
+ _cleanObjectData(boardData) {
87
+ if (!boardData || !boardData.objects) {
88
+ return boardData;
89
+ }
90
+
91
+ const cleanedObjects = boardData.objects.map(obj => {
92
+ if (obj.type === 'image') {
93
+ console.log('🧹 DEBUG _cleanImageData: обрабатываем изображение:', {
94
+ id: obj.id,
95
+ imageId: obj.imageId,
96
+ hasSrc: !!obj.src,
97
+ hasPropertiesSrc: !!obj.properties?.src,
98
+ srcIsBase64: !!(obj.src && obj.src.startsWith('data:')),
99
+ propertiesSrcIsBase64: !!(obj.properties?.src && obj.properties.src.startsWith('data:'))
100
+ });
101
+
102
+ const cleanedObj = { ...obj };
103
+
104
+ // Если есть imageId, убираем src для экономии места
105
+ if (obj.imageId && typeof obj.imageId === 'string' && obj.imageId.trim().length > 0) {
106
+ console.log('🧹 DEBUG _cleanImageData: у изображения есть imageId, убираем src');
107
+
108
+ // Убираем src с верхнего уровня
109
+ if (cleanedObj.src) {
110
+ delete cleanedObj.src;
111
+ console.log('🧹 DEBUG: удален src с верхнего уровня');
112
+ }
113
+
114
+ // Убираем src из properties
115
+ if (cleanedObj.properties?.src) {
116
+ cleanedObj.properties = { ...cleanedObj.properties };
117
+ delete cleanedObj.properties.src;
118
+ console.log('🧹 DEBUG: удален src из properties');
119
+ }
120
+ }
121
+ // Если нет imageId, предупреждаем о base64
122
+ else {
123
+ console.log('🧹 DEBUG _cleanImageData: у изображения НЕТ imageId, оставляем src как есть');
124
+ if (cleanedObj.properties?.src && cleanedObj.properties.src.startsWith('data:')) {
125
+ console.warn('❌ Изображение сохраняется с base64 в properties, так как нет imageId:', cleanedObj.id);
126
+ }
127
+ if (cleanedObj.src && cleanedObj.src.startsWith('data:')) {
128
+ console.warn('❌ Изображение сохраняется с base64 в src, так как нет imageId:', cleanedObj.id);
129
+ }
130
+ if (!obj.imageId) {
131
+ console.warn('❌ У изображения отсутствует imageId:', cleanedObj.id);
132
+ }
133
+ }
134
+
135
+ return cleanedObj;
136
+ }
137
+
138
+ if (obj.type === 'file') {
139
+ console.log('🧹 DEBUG _cleanObjectData: обрабатываем файл:', {
140
+ id: obj.id,
141
+ fileId: obj.fileId,
142
+ hasContent: !!obj.content,
143
+ hasPropertiesContent: !!obj.properties?.content
144
+ });
145
+
146
+ const cleanedObj = { ...obj };
147
+
148
+ // Если есть fileId, убираем content для экономии места
149
+ if (obj.fileId && typeof obj.fileId === 'string' && obj.fileId.trim().length > 0) {
150
+ console.log('🧹 DEBUG _cleanObjectData: у файла есть fileId, убираем content');
151
+
152
+ // Убираем content с верхнего уровня
153
+ if (cleanedObj.content) {
154
+ delete cleanedObj.content;
155
+ console.log('🧹 DEBUG: удален content с верхнего уровня');
156
+ }
157
+
158
+ // Убираем content из properties
159
+ if (cleanedObj.properties?.content) {
160
+ cleanedObj.properties = { ...cleanedObj.properties };
161
+ delete cleanedObj.properties.content;
162
+ console.log('🧹 DEBUG: удален content из properties');
163
+ }
164
+ }
165
+ // Если нет fileId, предупреждаем о наличии content
166
+ else {
167
+ console.log('🧹 DEBUG _cleanObjectData: у файла НЕТ fileId, оставляем content как есть');
168
+ if (cleanedObj.properties?.content) {
169
+ console.warn('❌ Файл сохраняется с content в properties, так как нет fileId:', cleanedObj.id);
170
+ }
171
+ if (cleanedObj.content) {
172
+ console.warn('❌ Файл сохраняется с content, так как нет fileId:', cleanedObj.id);
173
+ }
174
+ if (!obj.fileId) {
175
+ console.warn('❌ У файла отсутствует fileId:', cleanedObj.id);
176
+ }
177
+ }
178
+
179
+ return cleanedObj;
180
+ }
181
+
182
+ return obj;
183
+ });
184
+
185
+ return {
186
+ ...boardData,
187
+ objects: cleanedObjects
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Восстанавливает URL изображений и файлов при загрузке
193
+ */
194
+ async restoreObjectUrls(boardData) {
195
+ if (!boardData || !boardData.objects) {
196
+ return boardData;
197
+ }
198
+
199
+ const restoredObjects = await Promise.all(
200
+ boardData.objects.map(async (obj) => {
201
+ if (obj.type === 'image') {
202
+ console.log('🔗 DEBUG restoreImageUrls: обрабатываем изображение:', {
203
+ id: obj.id,
204
+ imageId: obj.imageId,
205
+ hasSrc: !!obj.src,
206
+ hasPropertiesSrc: !!obj.properties?.src
207
+ });
208
+
209
+ if (obj.imageId && (!obj.src && !obj.properties?.src)) {
210
+ console.log('🔗 DEBUG: восстанавливаем URL для изображения');
211
+ try {
212
+ // Формируем URL изображения
213
+ const imageUrl = `/api/images/${obj.imageId}/file`;
214
+
215
+ return {
216
+ ...obj,
217
+ src: imageUrl,
218
+ properties: {
219
+ ...obj.properties,
220
+ src: imageUrl
221
+ }
222
+ };
223
+ } catch (error) {
224
+ console.warn(`Не удалось восстановить URL для изображения ${obj.imageId}:`, error);
225
+ return obj;
226
+ }
227
+ } else {
228
+ console.log('🔗 DEBUG: изображение уже имеет URL или нет imageId, оставляем как есть');
229
+ return obj;
230
+ }
231
+ }
232
+
233
+ if (obj.type === 'file') {
234
+ console.log('🔗 DEBUG restoreObjectUrls: обрабатываем файл:', {
235
+ id: obj.id,
236
+ fileId: obj.fileId,
237
+ hasUrl: !!obj.url,
238
+ hasPropertiesUrl: !!obj.properties?.url
239
+ });
240
+
241
+ if (obj.fileId) {
242
+ console.log('🔗 DEBUG: восстанавливаем данные для файла');
243
+ try {
244
+ // Формируем URL файла для скачивания
245
+ const fileUrl = `/api/files/${obj.fileId}/download`;
246
+
247
+ // Создаем обновленный объект с восстановленными данными
248
+ const restoredObj = {
249
+ ...obj,
250
+ url: fileUrl,
251
+ properties: {
252
+ ...obj.properties,
253
+ url: fileUrl
254
+ }
255
+ };
256
+
257
+ // Пытаемся восстановить актуальные метаданные файла с сервера
258
+ // (Это будет выполнено асинхронно, чтобы не блокировать загрузку)
259
+ setTimeout(async () => {
260
+ try {
261
+ const response = await fetch(`/api/files/${obj.fileId}`, {
262
+ headers: {
263
+ 'Accept': 'application/json',
264
+ 'X-Requested-With': 'XMLHttpRequest'
265
+ },
266
+ credentials: 'same-origin'
267
+ });
268
+
269
+ if (response.ok) {
270
+ const result = await response.json();
271
+ if (result.success && result.data) {
272
+ console.log('🔄 Обновляем метаданные файла с сервера:', result.data);
273
+ // Эмитим событие для обновления метаданных файла в состоянии
274
+ // (это будет обработано в core, если EventBus доступен)
275
+ if (typeof window !== 'undefined' && window.moodboardEventBus) {
276
+ window.moodboardEventBus.emit('file:metadata:updated', {
277
+ objectId: obj.id,
278
+ fileId: obj.fileId,
279
+ metadata: result.data
280
+ });
281
+ }
282
+ }
283
+ }
284
+ } catch (error) {
285
+ console.warn(`Не удалось обновить метаданные файла ${obj.fileId}:`, error);
286
+ }
287
+ }, 100);
288
+
289
+ return restoredObj;
290
+ } catch (error) {
291
+ console.warn(`Не удалось восстановить данные для файла ${obj.fileId}:`, error);
292
+ return obj;
293
+ }
294
+ } else {
295
+ console.log('🔗 DEBUG: файл не имеет fileId, оставляем как есть');
296
+ return obj;
297
+ }
298
+ }
299
+
300
+ return obj;
301
+ })
302
+ );
303
+
304
+ return {
305
+ ...boardData,
306
+ objects: restoredObjects
307
+ };
308
+ }
309
+ }
@@ -0,0 +1,42 @@
1
+ export class EventBus {
2
+ constructor() {
3
+ this.events = new Map();
4
+ }
5
+
6
+ on(event, callback) {
7
+ if (!this.events.has(event)) {
8
+ this.events.set(event, new Set());
9
+ }
10
+ this.events.get(event).add(callback);
11
+ }
12
+
13
+ off(event, callback) {
14
+ const callbacks = this.events.get(event);
15
+ if (callbacks) {
16
+ callbacks.delete(callback);
17
+
18
+ // Если callback'ов больше нет, удаляем событие из Map
19
+ if (callbacks.size === 0) {
20
+ this.events.delete(event);
21
+ }
22
+ }
23
+ }
24
+
25
+ emit(event, data) {
26
+ const callbacks = this.events.get(event);
27
+ if (callbacks) {
28
+ callbacks.forEach(callback => {
29
+ try {
30
+ callback(data);
31
+ } catch (error) {
32
+ // Логируем ошибку, но продолжаем выполнение
33
+ console.error(`Error in event callback for '${event}':`, error);
34
+ }
35
+ });
36
+ }
37
+ }
38
+
39
+ removeAllListeners() {
40
+ this.events.clear();
41
+ }
42
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Управляет историей команд для функции Undo/Redo
3
+ */
4
+ import { Events } from './events/Events.js';
5
+ export class HistoryManager {
6
+ constructor(eventBus, options = {}) {
7
+ this.eventBus = eventBus;
8
+ this.options = {
9
+ maxHistorySize: 50, // Максимальное количество команд в истории
10
+ mergeTimeout: 1000, // Время в мс для объединения похожих команд
11
+ ...options
12
+ };
13
+
14
+ // История выполненных команд
15
+ this.history = [];
16
+ // Текущая позиция в истории
17
+ this.currentIndex = -1;
18
+ // Флаг для предотвращения зацикливания при undo/redo
19
+ this.isExecutingCommand = false;
20
+
21
+ this.initEventListeners();
22
+ }
23
+
24
+ initEventListeners() {
25
+ // Слушаем события клавиатуры
26
+ this.eventBus.on(Events.Keyboard.Undo, () => {
27
+ this.undo();
28
+ });
29
+
30
+ this.eventBus.on(Events.Keyboard.Redo, () => {
31
+ this.redo();
32
+ });
33
+
34
+ // Для отладки
35
+ this.eventBus.on(Events.History.Debug, () => {
36
+ this.debugHistory();
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Выполнить команду и добавить в историю
42
+ */
43
+ executeCommand(command) {
44
+ if (this.isExecutingCommand) {
45
+ // Если мы в процессе undo/redo, не добавляем в историю
46
+ this._executeCommandSafely(command);
47
+ return;
48
+ }
49
+
50
+
51
+
52
+ // Проверяем, можно ли объединить с последней командой
53
+ const lastCommand = this.getLastCommand();
54
+ if (lastCommand &&
55
+ lastCommand.canMergeWith(command) &&
56
+ (command.timestamp - lastCommand.timestamp) < this.options.mergeTimeout) {
57
+
58
+
59
+ lastCommand.mergeWith(command);
60
+ this.eventBus.emit('history:changed', {
61
+ canUndo: this.canUndo(),
62
+ canRedo: this.canRedo(),
63
+ historySize: this.history.length
64
+ });
65
+ return;
66
+ }
67
+
68
+ // Выполняем команду
69
+ this._executeCommandSafely(command);
70
+
71
+ // Удаляем все команды после текущей позиции (если есть)
72
+ if (this.currentIndex < this.history.length - 1) {
73
+ this.history = this.history.slice(0, this.currentIndex + 1);
74
+ }
75
+
76
+ // Добавляем новую команду
77
+ this.history.push(command);
78
+ this.currentIndex++;
79
+
80
+ // Ограничиваем размер истории
81
+ if (this.history.length > this.options.maxHistorySize) {
82
+ this.history.shift();
83
+ this.currentIndex--;
84
+ }
85
+
86
+ // Уведомляем об изменении истории
87
+ this.eventBus.emit(Events.History.Changed, {
88
+ canUndo: this.canUndo(),
89
+ canRedo: this.canRedo(),
90
+ historySize: this.history.length,
91
+ currentCommand: command.toString()
92
+ });
93
+
94
+
95
+ }
96
+
97
+ /**
98
+ * Безопасно выполняет команду (поддерживает синхронные и асинхронные команды)
99
+ * @private
100
+ */
101
+ _executeCommandSafely(command) {
102
+ try {
103
+ const result = command.execute();
104
+ // Если команда возвращает Promise, обрабатываем асинхронно
105
+ if (result && typeof result.then === 'function') {
106
+ result.catch(error => {
107
+ console.error('Ошибка выполнения асинхронной команды:', error);
108
+ });
109
+ }
110
+ } catch (error) {
111
+ console.error('Ошибка выполнения команды:', error);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Отменить последнюю команду
117
+ */
118
+ undo() {
119
+ if (!this.canUndo()) {
120
+
121
+ return false;
122
+ }
123
+
124
+ const command = this.history[this.currentIndex];
125
+
126
+
127
+ this.isExecutingCommand = true;
128
+ try {
129
+ command.undo();
130
+ this.currentIndex--;
131
+
132
+ this.eventBus.emit(Events.History.Changed, {
133
+ canUndo: this.canUndo(),
134
+ canRedo: this.canRedo(),
135
+ historySize: this.history.length,
136
+ lastUndone: command.toString()
137
+ });
138
+
139
+
140
+ return true;
141
+ } catch (error) {
142
+ console.error('❌ Ошибка при отмене команды:', error);
143
+ return false;
144
+ } finally {
145
+ this.isExecutingCommand = false;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Повторить отмененную команду
151
+ */
152
+ redo() {
153
+ if (!this.canRedo()) {
154
+
155
+ return false;
156
+ }
157
+
158
+ this.currentIndex++;
159
+ const command = this.history[this.currentIndex];
160
+
161
+
162
+ this.isExecutingCommand = true;
163
+ try {
164
+ this._executeCommandSafely(command);
165
+
166
+ this.eventBus.emit(Events.History.Changed, {
167
+ canUndo: this.canUndo(),
168
+ canRedo: this.canRedo(),
169
+ historySize: this.history.length,
170
+ lastRedone: command.toString()
171
+ });
172
+
173
+
174
+ return true;
175
+ } catch (error) {
176
+ console.error('❌ Ошибка при повторе команды:', error);
177
+ this.currentIndex--; // Откатываем индекс при ошибке
178
+ return false;
179
+ } finally {
180
+ this.isExecutingCommand = false;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Можно ли отменить команду
186
+ */
187
+ canUndo() {
188
+ return this.currentIndex >= 0;
189
+ }
190
+
191
+ /**
192
+ * Можно ли повторить команду
193
+ */
194
+ canRedo() {
195
+ return this.currentIndex < this.history.length - 1;
196
+ }
197
+
198
+ /**
199
+ * Получить последнюю команду
200
+ */
201
+ getLastCommand() {
202
+ if (this.history.length === 0) return null;
203
+ return this.history[this.history.length - 1];
204
+ }
205
+
206
+ /**
207
+ * Очистить историю
208
+ */
209
+ clear() {
210
+ this.history = [];
211
+ this.currentIndex = -1;
212
+
213
+ this.eventBus.emit(Events.History.Changed, {
214
+ canUndo: false,
215
+ canRedo: false,
216
+ historySize: 0
217
+ });
218
+
219
+
220
+ }
221
+
222
+ /**
223
+ * Получить информацию об истории (для отладки)
224
+ */
225
+ getHistoryInfo() {
226
+ return {
227
+ totalCommands: this.history.length,
228
+ currentIndex: this.currentIndex,
229
+ canUndo: this.canUndo(),
230
+ canRedo: this.canRedo(),
231
+ commands: this.history.map((cmd, index) => ({
232
+ index,
233
+ isCurrent: index === this.currentIndex,
234
+ command: cmd.toString()
235
+ }))
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Вывести историю в консоль (для отладки)
241
+ */
242
+ debugHistory() {
243
+ // Отладочная информация об истории команд
244
+ const info = this.getHistoryInfo();
245
+ console.group('📚 История команд');
246
+ console.table(info.commands);
247
+ console.log(`Позиция: ${this.currentIndex + 1}/${this.history.length}`);
248
+ console.log(`Undo: ${this.canUndo()}, Redo: ${this.canRedo()}`);
249
+ console.groupEnd();
250
+ }
251
+
252
+ /**
253
+ * Уничтожить менеджер истории
254
+ */
255
+ destroy() {
256
+ this.clear();
257
+ this.eventBus.removeAllListeners(Events.Keyboard.Undo);
258
+ this.eventBus.removeAllListeners(Events.Keyboard.Redo);
259
+ this.eventBus.removeAllListeners(Events.History.Debug);
260
+ }
261
+ }