@sequent-org/moodboard 1.2.119 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -1
- package/src/assets/icons/rotate-icon.svg +1 -1
- package/src/core/HistoryManager.js +16 -16
- package/src/core/KeyboardManager.js +48 -539
- package/src/core/PixiEngine.js +9 -9
- package/src/core/SaveManager.js +56 -31
- package/src/core/bootstrap/CoreInitializer.js +65 -0
- package/src/core/commands/DeleteObjectCommand.js +8 -0
- package/src/core/commands/GroupDeleteCommand.js +75 -0
- package/src/core/commands/GroupRotateCommand.js +6 -0
- package/src/core/commands/UpdateContentCommand.js +52 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
- package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
- package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
- package/src/core/commands/UpdateTextStyleCommand.js +90 -0
- package/src/core/commands/index.js +6 -0
- package/src/core/events/Events.js +6 -0
- package/src/core/flows/ClipboardFlow.js +553 -0
- package/src/core/flows/LayerAndViewportFlow.js +283 -0
- package/src/core/flows/ObjectLifecycleFlow.js +336 -0
- package/src/core/flows/SaveFlow.js +34 -0
- package/src/core/flows/TransformFlow.js +277 -0
- package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
- package/src/core/index.js +41 -1773
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
- package/src/core/keyboard/KeyboardContextGuards.js +35 -0
- package/src/core/keyboard/KeyboardEventRouter.js +92 -0
- package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
- package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
- package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
- package/src/core/rendering/ObjectRenderer.js +3 -7
- package/src/grid/BaseGrid.js +26 -0
- package/src/grid/CrossGrid.js +7 -6
- package/src/grid/DotGrid.js +89 -33
- package/src/grid/DotGridZoomPhases.js +42 -0
- package/src/grid/LineGrid.js +22 -21
- package/src/moodboard/MoodBoard.js +31 -532
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
- package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
- package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
- package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
- package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
- package/src/objects/FileObject.js +17 -6
- package/src/objects/FrameObject.js +50 -10
- package/src/objects/NoteObject.js +5 -4
- package/src/services/BoardService.js +42 -2
- package/src/services/FrameService.js +83 -42
- package/src/services/ResizePolicyService.js +152 -0
- package/src/services/SettingsApplier.js +7 -2
- package/src/services/ZoomPanController.js +35 -9
- package/src/tools/ToolManager.js +30 -537
- package/src/tools/board-tools/PanTool.js +5 -11
- package/src/tools/manager/ToolActivationController.js +49 -0
- package/src/tools/manager/ToolEventRouter.js +396 -0
- package/src/tools/manager/ToolManagerGuards.js +33 -0
- package/src/tools/manager/ToolManagerLifecycle.js +110 -0
- package/src/tools/manager/ToolRegistry.js +33 -0
- package/src/tools/object-tools/DrawingTool.js +48 -14
- package/src/tools/object-tools/PlacementTool.js +50 -1049
- package/src/tools/object-tools/PlacementToolV2.js +88 -0
- package/src/tools/object-tools/SelectTool.js +174 -2681
- package/src/tools/object-tools/placement/GhostController.js +504 -0
- package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
- package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
- package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
- package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
- package/src/tools/object-tools/selection/CursorController.js +78 -0
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
- package/src/tools/object-tools/selection/HitTestService.js +102 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
- package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
- package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
- package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
- package/src/ui/FilePropertiesPanel.js +61 -32
- package/src/ui/FramePropertiesPanel.js +176 -101
- package/src/ui/HtmlHandlesLayer.js +121 -999
- package/src/ui/MapPanel.js +12 -7
- package/src/ui/NotePropertiesPanel.js +17 -2
- package/src/ui/TextPropertiesPanel.js +124 -738
- package/src/ui/Toolbar.js +82 -1181
- package/src/ui/Topbar.js +23 -25
- package/src/ui/ZoomPanel.js +16 -5
- package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
- package/src/ui/handles/HandlesDomRenderer.js +278 -0
- package/src/ui/handles/HandlesEventBridge.js +102 -0
- package/src/ui/handles/HandlesInteractionController.js +772 -0
- package/src/ui/handles/HandlesPositioningService.js +206 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
- package/src/ui/styles/toolbar.css +2 -0
- package/src/ui/styles/workspace.css +13 -6
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
- package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +665 -0
- package/src/ui/toolbar/ToolbarRenderer.js +97 -0
- package/src/ui/toolbar/ToolbarStateController.js +79 -0
- package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
- package/src/utils/emojiLoaderNoBundler.js +1 -1
package/src/core/PixiEngine.js
CHANGED
|
@@ -211,12 +211,14 @@ export class PixiEngine {
|
|
|
211
211
|
if (pixiObject instanceof PIXI.Sprite) {
|
|
212
212
|
console.log('🗑️ PixiEngine: очищаем ресурсы изображения/эмоджи');
|
|
213
213
|
|
|
214
|
-
// Очищаем текстуру (
|
|
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
|
-
|
|
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
|
|
386
|
+
if (!pixiObject) return;
|
|
385
387
|
const meta = pixiObject._mb || {};
|
|
386
|
-
if (meta.type !== 'frame') return;
|
|
387
|
-
|
|
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
|
/**
|
package/src/core/SaveManager.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
}
|
|
109
|
+
};
|
|
110
|
+
window.addEventListener('beforeunload', this._domHandlers.beforeUnload, { capture: true });
|
|
101
111
|
|
|
102
112
|
// Дополнительно: обработка быстрого ухода со страницы (pagehide надёжнее в части браузеров)
|
|
103
|
-
|
|
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
|
-
}
|
|
122
|
+
};
|
|
123
|
+
window.addEventListener('pagehide', this._domHandlers.pageHide, { capture: true });
|
|
113
124
|
|
|
114
125
|
// Подстраховка на случай, когда вкладка просто уходит в фон без beforeunload/pagehide
|
|
115
|
-
|
|
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.
|
|
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
|
+
}
|