@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,381 @@
1
+ /**
2
+ * Менеджер автоматического сохранения данных
3
+ */
4
+ import { Events } from './events/Events.js';
5
+ export class SaveManager {
6
+ constructor(eventBus, options = {}) {
7
+ this.eventBus = eventBus;
8
+ this.apiClient = null; // Будет установлен позже через setApiClient
9
+ this.options = {
10
+ // Фиксированные настройки автосохранения (не настраиваются клиентом)
11
+ autoSave: true,
12
+ saveDelay: 1500, // Оптимальная задержка 1.5 секунды
13
+ maxRetries: 3,
14
+ retryDelay: 1000,
15
+ periodicSaveInterval: 30000, // Периодическое сохранение каждые 30 сек
16
+
17
+ // Настраиваемые эндпоинты (берем из options)
18
+ saveEndpoint: options.saveEndpoint || '/api/moodboard/save',
19
+ loadEndpoint: options.loadEndpoint || '/api/moodboard/load'
20
+ };
21
+
22
+ this.saveTimer = null;
23
+ this.isRequestInProgress = false;
24
+ this.retryCount = 0;
25
+ this.lastSavedData = null;
26
+ this.hasUnsavedChanges = false;
27
+
28
+ // Состояния сохранения
29
+ this.saveStatus = 'idle'; // idle, saving, saved, error
30
+
31
+ this.setupEventListeners();
32
+ }
33
+
34
+ /**
35
+ * Устанавливает ApiClient для использования в сохранении
36
+ */
37
+ setApiClient(apiClient) {
38
+ this.apiClient = apiClient;
39
+ }
40
+
41
+ /**
42
+ * Настройка обработчиков событий
43
+ */
44
+ setupEventListeners() {
45
+ if (!this.options.autoSave) return;
46
+
47
+ // Отслеживаем изменения в данных
48
+ this.eventBus.on(Events.Grid.BoardDataChanged, (data) => {
49
+ this.scheduleAutoSave(data);
50
+ });
51
+
52
+ // Отслеживаем создание объектов
53
+ this.eventBus.on(Events.Object.Created, () => {
54
+ this.markAsChanged();
55
+ });
56
+
57
+ // Отслеживаем изменения объектов
58
+ this.eventBus.on(Events.Object.Updated, (data) => {
59
+
60
+ this.markAsChanged();
61
+ });
62
+
63
+ // Отслеживаем удаление объектов
64
+ this.eventBus.on(Events.Object.Deleted, () => {
65
+ this.markAsChanged();
66
+ });
67
+
68
+ // Отслеживаем прямые изменения состояния (для Undo/Redo)
69
+ this.eventBus.on(Events.Object.StateChanged, (data) => {
70
+
71
+ this.markAsChanged();
72
+ });
73
+
74
+ // Отслеживание перемещений теперь происходит через команды и state:changed
75
+
76
+ // Сохранение при закрытии страницы
77
+ window.addEventListener('beforeunload', (e) => {
78
+ if (this.hasUnsavedChanges) {
79
+ this.saveImmediately();
80
+ // Предупреждаем о несохраненных изменениях
81
+ e.preventDefault();
82
+ e.returnValue = 'У вас есть несохраненные изменения. Вы уверены, что хотите покинуть страницу?';
83
+ return e.returnValue;
84
+ }
85
+ });
86
+
87
+ // Периодическое автосохранение
88
+ if (this.options.autoSave) {
89
+ setInterval(() => {
90
+ if (this.hasUnsavedChanges && !this.isRequestInProgress) {
91
+ this.saveImmediately();
92
+ }
93
+ }, this.options.periodicSaveInterval);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Отметить данные как измененные
99
+ */
100
+ markAsChanged() {
101
+ this.hasUnsavedChanges = true;
102
+ this.scheduleAutoSave();
103
+ }
104
+
105
+ /**
106
+ * Запланировать автоматическое сохранение с задержкой
107
+ */
108
+ scheduleAutoSave(data = null) {
109
+ if (!this.options.autoSave) return;
110
+
111
+ // Отменяем предыдущий таймер
112
+ if (this.saveTimer) {
113
+ clearTimeout(this.saveTimer);
114
+ }
115
+
116
+ // Устанавливаем новый таймер
117
+ this.saveTimer = setTimeout(() => {
118
+ this.saveImmediately(data);
119
+ }, this.options.saveDelay);
120
+
121
+ // Обновляем статус
122
+ this.updateSaveStatus('pending');
123
+ }
124
+
125
+ /**
126
+ * Немедленное сохранение
127
+ */
128
+ async saveImmediately(data = null) {
129
+ if (this.isRequestInProgress) return;
130
+
131
+ try {
132
+ // Получаем данные для сохранения
133
+ const saveData = data || await this.getBoardData();
134
+
135
+ // Проверяем, изменились ли данные с последнего сохранения
136
+ if (this.lastSavedData && JSON.stringify(saveData) === JSON.stringify(this.lastSavedData)) {
137
+ return; // Данные не изменились
138
+ }
139
+
140
+ this.isRequestInProgress = true;
141
+ this.updateSaveStatus('saving');
142
+
143
+ // Отправляем данные на сервер
144
+ const response = await this.sendSaveRequest(saveData);
145
+
146
+ // Проверяем успешность сохранения (разные форматы от ApiClient и прямого запроса)
147
+ const isSuccess = response.success === true || (response.data !== undefined);
148
+
149
+ if (isSuccess) {
150
+ this.lastSavedData = JSON.parse(JSON.stringify(saveData));
151
+ this.hasUnsavedChanges = false;
152
+ this.retryCount = 0;
153
+ this.updateSaveStatus('saved');
154
+
155
+ // Эмитируем событие успешного сохранения
156
+ this.eventBus.emit(Events.Save.Success, {
157
+ data: saveData,
158
+ timestamp: new Date().toISOString()
159
+ });
160
+ } else {
161
+ throw new Error(response.message || 'Ошибка сохранения');
162
+ }
163
+
164
+ } catch (error) {
165
+ console.error('Ошибка автосохранения:', error);
166
+ this.handleSaveError(error, data);
167
+ } finally {
168
+ this.isRequestInProgress = false;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Получение данных доски для сохранения
174
+ */
175
+ async getBoardData() {
176
+ return new Promise((resolve) => {
177
+ const requestData = { data: null };
178
+ this.eventBus.emit(Events.Save.GetBoardData, requestData);
179
+ resolve(requestData.data);
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Отправка запроса на сохранение
185
+ */
186
+ async sendSaveRequest(data) {
187
+ const boardId = data.id || 'default';
188
+
189
+ // Если есть ApiClient, используем его (он автоматически очистит данные изображений)
190
+ if (this.apiClient) {
191
+ return await this.apiClient.saveBoard(boardId, data);
192
+ }
193
+
194
+ // Fallback к прямому запросу (без очистки изображений)
195
+
196
+ // Получаем CSRF токен
197
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
198
+
199
+ if (!csrfToken) {
200
+ throw new Error('CSRF токен не найден. Добавьте <meta name="csrf-token" content="{{ csrf_token() }}"> в HTML.');
201
+ }
202
+
203
+ const requestBody = {
204
+ boardId: boardId,
205
+ boardData: data
206
+ };
207
+
208
+ const response = await fetch(this.options.saveEndpoint, {
209
+ method: 'POST',
210
+ headers: {
211
+ 'Content-Type': 'application/json',
212
+ 'Accept': 'application/json',
213
+ 'X-CSRF-TOKEN': csrfToken,
214
+ 'X-Requested-With': 'XMLHttpRequest'
215
+ },
216
+ credentials: 'same-origin', // Важно для работы с куки Laravel
217
+ body: JSON.stringify(requestBody)
218
+ });
219
+
220
+ if (!response.ok) {
221
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
222
+
223
+ try {
224
+ const errorData = await response.json();
225
+ if (errorData.message) {
226
+ errorMessage = errorData.message;
227
+ }
228
+ if (errorData.errors) {
229
+ // Laravel validation errors
230
+ const validationErrors = Object.values(errorData.errors).flat();
231
+ errorMessage += '. ' + validationErrors.join(', ');
232
+ }
233
+ } catch (parseError) {
234
+ // Если не удалось распарсить JSON ошибки, используем стандартное сообщение
235
+ console.warn('Не удалось распарсить ошибку сервера:', parseError);
236
+ }
237
+
238
+ throw new Error(errorMessage);
239
+ }
240
+
241
+ return await response.json();
242
+ }
243
+
244
+ /**
245
+ * Обработка ошибок сохранения
246
+ */
247
+ async handleSaveError(error, data) {
248
+ this.retryCount++;
249
+ this.updateSaveStatus('error', error.message);
250
+
251
+ // Эмитируем событие ошибки
252
+ this.eventBus.emit(Events.Save.Error, {
253
+ error: error.message,
254
+ retryCount: this.retryCount,
255
+ maxRetries: this.options.maxRetries
256
+ });
257
+
258
+ // Повторная попытка сохранения
259
+ if (this.retryCount < this.options.maxRetries) {
260
+
261
+
262
+ setTimeout(() => {
263
+ this.saveImmediately(data);
264
+ }, this.options.retryDelay * this.retryCount); // Увеличиваем задержку с каждой попыткой
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Загрузка данных с сервера
270
+ */
271
+ async loadBoardData(boardId) {
272
+ try {
273
+ const response = await fetch(`${this.options.loadEndpoint}/${boardId}`, {
274
+ method: 'GET',
275
+ headers: {
276
+ 'Accept': 'application/json',
277
+ 'X-Requested-With': 'XMLHttpRequest'
278
+ }
279
+ });
280
+
281
+ if (!response.ok) {
282
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
283
+ }
284
+
285
+ const result = await response.json();
286
+
287
+ if (result.success) {
288
+ this.lastSavedData = result.data;
289
+ this.hasUnsavedChanges = false;
290
+
291
+ // Эмитируем событие загрузки
292
+ this.eventBus.emit(Events.Save.Loaded, {
293
+ data: result.data,
294
+ timestamp: new Date().toISOString()
295
+ });
296
+
297
+ return result.data;
298
+ } else {
299
+ throw new Error(result.message || 'Ошибка загрузки');
300
+ }
301
+
302
+ } catch (error) {
303
+ console.error('Ошибка загрузки данных:', error);
304
+ this.eventBus.emit(Events.Save.LoadError, { error: error.message });
305
+ throw error;
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Обновление статуса сохранения
311
+ */
312
+ updateSaveStatus(status, message = '') {
313
+ this.saveStatus = status;
314
+
315
+ // Эмитируем событие изменения статуса
316
+ this.eventBus.emit(Events.Save.StatusChanged, {
317
+ status,
318
+ message,
319
+ hasUnsavedChanges: this.hasUnsavedChanges,
320
+ timestamp: new Date().toISOString()
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Принудительное сохранение (вызывается пользователем)
326
+ */
327
+ async forceSave() {
328
+ if (this.saveTimer) {
329
+ clearTimeout(this.saveTimer);
330
+ this.saveTimer = null;
331
+ }
332
+
333
+ await this.saveImmediately();
334
+ }
335
+
336
+ /**
337
+ * Включение/выключение автосохранения
338
+ */
339
+ setAutoSave(enabled) {
340
+ this.options.autoSave = enabled;
341
+
342
+ if (!enabled && this.saveTimer) {
343
+ clearTimeout(this.saveTimer);
344
+ this.saveTimer = null;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Получение текущего статуса
350
+ */
351
+ getStatus() {
352
+ return {
353
+ saveStatus: this.saveStatus,
354
+ hasUnsavedChanges: this.hasUnsavedChanges,
355
+ isRequestInProgress: this.isRequestInProgress,
356
+ retryCount: this.retryCount,
357
+ autoSaveEnabled: this.options.autoSave
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Очистка ресурсов
363
+ */
364
+ destroy() {
365
+ if (this.saveTimer) {
366
+ clearTimeout(this.saveTimer);
367
+ }
368
+
369
+ // Финальное сохранение перед уничтожением
370
+ if (this.hasUnsavedChanges && this.options.autoSave) {
371
+ this.saveImmediately();
372
+ }
373
+
374
+ // Удаляем обработчики событий (константы)
375
+ this.eventBus.off(Events.Grid.BoardDataChanged);
376
+ this.eventBus.off(Events.Object.Created);
377
+ this.eventBus.off(Events.Object.Updated);
378
+ this.eventBus.off(Events.Object.Deleted);
379
+ this.eventBus.off(Events.Tool.DragEnd);
380
+ }
381
+ }
@@ -0,0 +1,64 @@
1
+ export class StateManager {
2
+ constructor(eventBus) {
3
+ this.eventBus = eventBus;
4
+ this.state = {
5
+ board: {},
6
+ objects: [],
7
+ selectedObjects: [],
8
+ isDirty: false
9
+ };
10
+ }
11
+
12
+ loadBoard(boardData) {
13
+ this.state.board = boardData;
14
+ this.state.objects = boardData.objects || [];
15
+ this.eventBus.emit('board:loaded', boardData);
16
+ }
17
+
18
+ addObject(objectData) {
19
+ this.state.objects.push(objectData);
20
+ this.markDirty();
21
+ this.eventBus.emit('object:created', objectData);
22
+ }
23
+
24
+ removeObject(objectId) {
25
+ this.state.objects = this.state.objects.filter(obj => obj.id !== objectId);
26
+ this.markDirty();
27
+ this.eventBus.emit('object:deleted', objectId);
28
+ }
29
+
30
+ updateObjectPosition(objectId, position) {
31
+ const object = this.state.objects.find(obj => obj.id === objectId);
32
+ if (object) {
33
+ object.position = position;
34
+ this.markDirty();
35
+ this.eventBus.emit('object:updated', { objectId, position });
36
+ }
37
+ }
38
+
39
+ getObjects() {
40
+ return [...this.state.objects];
41
+ }
42
+
43
+ serialize() {
44
+ return {
45
+ ...this.state.board,
46
+ objects: this.state.objects
47
+ };
48
+ }
49
+
50
+ markDirty() {
51
+ this.state.isDirty = true;
52
+
53
+ // Уведомляем SaveManager о том, что состояние изменилось
54
+ // Это нужно для Undo/Redo операций
55
+ this.eventBus.emit('state:changed', {
56
+ reason: 'state_marked_dirty',
57
+ timestamp: Date.now()
58
+ });
59
+ }
60
+
61
+ isDirty() {
62
+ return this.state.isDirty;
63
+ }
64
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Базовый класс для всех команд в системе Undo/Redo
3
+ * Реализует паттерн Command
4
+ */
5
+ export class BaseCommand {
6
+ constructor(type, description = '') {
7
+ this.type = type;
8
+ this.description = description;
9
+ this.timestamp = Date.now();
10
+ this.id = `cmd_${this.timestamp}_${Math.random().toString(36).substr(2, 9)}`;
11
+ this.eventBus = null;
12
+ }
13
+
14
+ /**
15
+ * Устанавливает EventBus для отправки событий
16
+ */
17
+ setEventBus(eventBus) {
18
+ this.eventBus = eventBus;
19
+ }
20
+
21
+ /**
22
+ * Отправляет событие через EventBus
23
+ */
24
+ emit(eventName, data) {
25
+ if (this.eventBus) {
26
+ this.eventBus.emit(eventName, data);
27
+ } else {
28
+ console.warn(`EventBus не установлен для команды ${this.id}`);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Выполнить команду
34
+ * Должен быть переопределен в наследниках
35
+ */
36
+ execute() {
37
+ throw new Error('execute() method must be implemented in subclass');
38
+ }
39
+
40
+ /**
41
+ * Отменить команду
42
+ * Должен быть переопределен в наследниках
43
+ */
44
+ undo() {
45
+ throw new Error('undo() method must be implemented in subclass');
46
+ }
47
+
48
+ /**
49
+ * Можно ли объединить с другой командой (для группировки мелких изменений)
50
+ */
51
+ canMergeWith(otherCommand) {
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * Объединить с другой командой
57
+ */
58
+ mergeWith(otherCommand) {
59
+ throw new Error('mergeWith() method must be implemented when canMergeWith returns true');
60
+ }
61
+
62
+ /**
63
+ * Получить описание команды для отладки
64
+ */
65
+ toString() {
66
+ return `${this.type}: ${this.description} (${new Date(this.timestamp).toLocaleTimeString()})`;
67
+ }
68
+ }
@@ -0,0 +1,44 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ import { Events } from '../events/Events.js';
3
+
4
+ /**
5
+ * Команда копирования объекта
6
+ */
7
+ export class CopyObjectCommand extends BaseCommand {
8
+ constructor(coreMoodboard, objectId) {
9
+ super();
10
+ this.coreMoodboard = coreMoodboard;
11
+ this.objectId = objectId;
12
+ this.objectData = null;
13
+ }
14
+
15
+ execute() {
16
+ // Находим объект в состоянии
17
+ const objects = this.coreMoodboard.state.state.objects;
18
+ const object = objects.find(obj => obj.id === this.objectId);
19
+
20
+ if (object) {
21
+ // Создаем глубокую копию данных объекта
22
+ this.objectData = JSON.parse(JSON.stringify(object));
23
+
24
+ // Сохраняем в буфер обмена приложения
25
+ this.coreMoodboard.clipboard = {
26
+ type: 'object',
27
+ data: this.objectData
28
+ };
29
+
30
+ this.emit(Events.Object.Updated, {
31
+ objectId: this.objectId,
32
+ objectData: this.objectData
33
+ });
34
+ }
35
+ }
36
+
37
+ undo() {
38
+ // Копирование не нужно отменять - это не меняет состояние доски
39
+ }
40
+
41
+ getDescription() {
42
+ return `Копировать объект ${this.objectId}`;
43
+ }
44
+ }
@@ -0,0 +1,46 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ import { Events } from '../events/Events.js';
3
+
4
+ /**
5
+ * Команда создания объекта
6
+ */
7
+ export class CreateObjectCommand extends BaseCommand {
8
+ constructor(coreMoodboard, objectData) {
9
+ super('create_object', `Создать ${objectData.type}`);
10
+ this.coreMoodboard = coreMoodboard;
11
+ this.objectData = { ...objectData }; // Копируем данные объекта
12
+ this.wasExecuted = false;
13
+ }
14
+
15
+ execute() {
16
+ if (this.wasExecuted) {
17
+ // При redo - восстанавливаем объект
18
+ this.coreMoodboard.state.addObject(this.objectData);
19
+ this.coreMoodboard.pixi.createObject(this.objectData);
20
+
21
+ } else {
22
+ // При первом выполнении - создаем новый объект
23
+ this.coreMoodboard.state.addObject(this.objectData);
24
+ this.coreMoodboard.pixi.createObject(this.objectData);
25
+ this.wasExecuted = true;
26
+
27
+ }
28
+
29
+ this.coreMoodboard.eventBus.emit(Events.Object.Created, {
30
+ objectId: this.objectData.id,
31
+ objectData: this.objectData
32
+ });
33
+ }
34
+
35
+ undo() {
36
+ // Удаляем объект из состояния и PIXI
37
+ this.coreMoodboard.state.removeObject(this.objectData.id);
38
+ this.coreMoodboard.pixi.removeObject(this.objectData.id);
39
+
40
+
41
+
42
+ this.coreMoodboard.eventBus.emit(Events.Object.Deleted, {
43
+ objectId: this.objectData.id
44
+ });
45
+ }
46
+ }