@sequent-org/moodboard 1.2.119 → 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 +6 -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 -1773
  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 -999
  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
@@ -0,0 +1,38 @@
1
+ import { WorkspaceManager } from '../WorkspaceManager.js';
2
+ import { DataManager } from '../DataManager.js';
3
+ import { ActionHandler } from '../ActionHandler.js';
4
+ import { AlignmentGuides } from '../../tools/AlignmentGuides.js';
5
+ import { ImageUploadService } from '../../services/ImageUploadService.js';
6
+ import { SettingsApplier } from '../../services/SettingsApplier.js';
7
+
8
+ export function createWorkspaceManager(board) {
9
+ board.workspaceManager = new WorkspaceManager(board.container, board.options);
10
+ }
11
+
12
+ export function createMoodBoardManagers(board) {
13
+ board.settingsApplier = new SettingsApplier(
14
+ board.coreMoodboard.eventBus,
15
+ board.coreMoodboard.pixi,
16
+ board.coreMoodboard.boardService || null
17
+ );
18
+
19
+ board.coreMoodboard.settingsApplier = board.settingsApplier;
20
+
21
+ board.dataManager = new DataManager(board.coreMoodboard);
22
+ board.actionHandler = new ActionHandler(board.dataManager, board.workspaceManager);
23
+ }
24
+
25
+ export function wireMoodBoardServices(board) {
26
+ board.alignmentGuides = new AlignmentGuides(
27
+ board.coreMoodboard.eventBus,
28
+ board.coreMoodboard.pixi.app,
29
+ () => board.coreMoodboard.state.getObjects()
30
+ );
31
+
32
+ board.imageUploadService = new ImageUploadService(board.coreMoodboard.apiClient);
33
+ board.coreMoodboard.imageUploadService = board.imageUploadService;
34
+
35
+ if (board.settingsApplier && board.topbar) {
36
+ board.settingsApplier.setUI({ topbar: board.topbar });
37
+ }
38
+ }
@@ -0,0 +1,109 @@
1
+ import { Toolbar } from '../../ui/Toolbar.js';
2
+ import { SaveStatus } from '../../ui/SaveStatus.js';
3
+ import { Topbar } from '../../ui/Topbar.js';
4
+ import { ZoomPanel } from '../../ui/ZoomPanel.js';
5
+ import { MapPanel } from '../../ui/MapPanel.js';
6
+ import { ContextMenu } from '../../ui/ContextMenu.js';
7
+ import { HtmlTextLayer } from '../../ui/HtmlTextLayer.js';
8
+ import { HtmlHandlesLayer } from '../../ui/HtmlHandlesLayer.js';
9
+ import { CommentPopover } from '../../ui/CommentPopover.js';
10
+ import { TextPropertiesPanel } from '../../ui/TextPropertiesPanel.js';
11
+ import { FramePropertiesPanel } from '../../ui/FramePropertiesPanel.js';
12
+ import { NotePropertiesPanel } from '../../ui/NotePropertiesPanel.js';
13
+ import { FilePropertiesPanel } from '../../ui/FilePropertiesPanel.js';
14
+ import { bindToolbarEvents, bindTopbarEvents } from '../integration/MoodBoardEventBindings.js';
15
+
16
+ function initToolbar(board) {
17
+ board.toolbar = new Toolbar(
18
+ board.toolbarContainer,
19
+ board.coreMoodboard.eventBus,
20
+ board.options.theme,
21
+ {
22
+ emojiBasePath: board.options.emojiBasePath || null,
23
+ }
24
+ );
25
+
26
+ if (typeof window !== 'undefined') {
27
+ window.reloadIcon = (iconName) => board.toolbar.reloadToolbarIcon(iconName);
28
+ }
29
+
30
+ board.saveStatus = new SaveStatus(
31
+ board.workspaceElement,
32
+ board.coreMoodboard.eventBus
33
+ );
34
+
35
+ bindToolbarEvents(board);
36
+ }
37
+
38
+ function initTopbar(board) {
39
+ board.topbar = new Topbar(
40
+ board.topbarContainer,
41
+ board.coreMoodboard.eventBus,
42
+ board.options.theme
43
+ );
44
+
45
+ try {
46
+ const app = board.coreMoodboard?.pixi?.app;
47
+ const colorInt = (app?.renderer?.background && app.renderer.background.color) || app?.renderer?.backgroundColor;
48
+ if (typeof colorInt === 'number') {
49
+ const boardHex = `#${colorInt.toString(16).padStart(6, '0')}`;
50
+ const btnHex = board.topbar.mapBoardToBtnHex(boardHex);
51
+ board.topbar.setPaintButtonHex(btnHex || '#B3E5FC');
52
+ }
53
+ } catch (_) {}
54
+
55
+ bindTopbarEvents(board);
56
+ }
57
+
58
+ function initZoombar(board) {
59
+ board.zoombar = new ZoomPanel(
60
+ board.workspaceElement,
61
+ board.coreMoodboard.eventBus
62
+ );
63
+ }
64
+
65
+ function initMapbar(board) {
66
+ board.mapbar = new MapPanel(
67
+ board.workspaceElement,
68
+ board.coreMoodboard.eventBus
69
+ );
70
+ }
71
+
72
+ function initContextMenu(board) {
73
+ board.contextMenu = new ContextMenu(
74
+ board.canvasContainer,
75
+ board.coreMoodboard.eventBus
76
+ );
77
+ }
78
+
79
+ function initHtmlLayersAndPanels(board) {
80
+ board.htmlTextLayer = new HtmlTextLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
81
+ board.htmlTextLayer.attach();
82
+
83
+ board.htmlHandlesLayer = new HtmlHandlesLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
84
+ board.htmlHandlesLayer.attach();
85
+
86
+ if (typeof window !== 'undefined') {
87
+ window.moodboardHtmlTextLayer = board.htmlTextLayer;
88
+ window.moodboardHtmlHandlesLayer = board.htmlHandlesLayer;
89
+ }
90
+
91
+ board.commentPopover = new CommentPopover(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
92
+ board.commentPopover.attach();
93
+
94
+ board.textPropertiesPanel = new TextPropertiesPanel(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
95
+ board.textPropertiesPanel.attach();
96
+
97
+ board.framePropertiesPanel = new FramePropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
98
+ board.notePropertiesPanel = new NotePropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
99
+ board.filePropertiesPanel = new FilePropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
100
+ }
101
+
102
+ export function createMoodBoardUi(board) {
103
+ initToolbar(board);
104
+ initTopbar(board);
105
+ initZoombar(board);
106
+ initMapbar(board);
107
+ initContextMenu(board);
108
+ initHtmlLayersAndPanels(board);
109
+ }
@@ -0,0 +1,65 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+
3
+ export function bindToolbarEvents(board) {
4
+ board.coreMoodboard.eventBus.on(Events.UI.ToolbarAction, (action) => {
5
+ board.actionHandler.handleToolbarAction(action);
6
+ });
7
+ }
8
+
9
+ export function bindTopbarEvents(board) {
10
+ board.coreMoodboard.eventBus.on(Events.UI.PaintPick, ({ color }) => {
11
+ if (!color) {
12
+ return;
13
+ }
14
+
15
+ if (board.settingsApplier && typeof board.settingsApplier.set === 'function') {
16
+ board.settingsApplier.set({ backgroundColor: color });
17
+ } else {
18
+ const hex = (typeof color === 'string' && color.startsWith('#'))
19
+ ? parseInt(color.slice(1), 16)
20
+ : color;
21
+ if (board.coreMoodboard?.pixi?.app?.renderer) {
22
+ board.coreMoodboard.pixi.app.renderer.backgroundColor = hex;
23
+ }
24
+ board.coreMoodboard.eventBus.emit(Events.Grid.BoardDataChanged, { settings: { backgroundColor: color } });
25
+ }
26
+ });
27
+ }
28
+
29
+ export function bindSaveCallbacks(board) {
30
+ if (!board.coreMoodboard || !board.coreMoodboard.eventBus) {
31
+ return;
32
+ }
33
+
34
+ if (typeof board.options.onSave === 'function') {
35
+ board.coreMoodboard.eventBus.on('save:success', (data) => {
36
+ try {
37
+ let screenshot = null;
38
+ if (board.coreMoodboard.pixi && board.coreMoodboard.pixi.app && board.coreMoodboard.pixi.app.view) {
39
+ screenshot = board.createCombinedScreenshot('image/jpeg', 0.6);
40
+ }
41
+
42
+ board.options.onSave({
43
+ success: true,
44
+ data: data,
45
+ screenshot: screenshot,
46
+ boardId: board.options.boardId,
47
+ });
48
+ } catch (error) {
49
+ console.warn('⚠️ Ошибка в коллбеке onSave:', error);
50
+ }
51
+ });
52
+
53
+ board.coreMoodboard.eventBus.on('save:error', (data) => {
54
+ try {
55
+ board.options.onSave({
56
+ success: false,
57
+ error: data.error,
58
+ boardId: board.options.boardId,
59
+ });
60
+ } catch (error) {
61
+ console.warn('⚠️ Ошибка в коллбеке onSave:', error);
62
+ }
63
+ });
64
+ }
65
+ }
@@ -0,0 +1,82 @@
1
+ function getSeedData(board) {
2
+ return board.data || { objects: [] };
3
+ }
4
+
5
+ function invokeOnLoad(board, payload) {
6
+ if (typeof board.options.onLoad === 'function') {
7
+ board.options.onLoad(payload);
8
+ }
9
+ }
10
+
11
+ export function getCsrfToken(board) {
12
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
13
+ || window.csrfToken
14
+ || board.options.csrfToken
15
+ || '';
16
+ }
17
+
18
+ export async function loadExistingBoard(board) {
19
+ try {
20
+ const boardId = board.options.boardId;
21
+
22
+ if (!boardId || !board.options.apiUrl) {
23
+ console.log('📦 MoodBoard: нет boardId или apiUrl, загружаем пустую доску');
24
+ const seedData = getSeedData(board);
25
+ board.dataManager.loadData(seedData);
26
+ invokeOnLoad(board, { success: true, data: seedData });
27
+ return;
28
+ }
29
+
30
+ console.log(`📦 MoodBoard: загружаем доску ${boardId} с ${board.options.apiUrl}`);
31
+
32
+ const loadUrl = board.options.apiUrl.endsWith('/')
33
+ ? `${board.options.apiUrl}load/${boardId}`
34
+ : `${board.options.apiUrl}/load/${boardId}`;
35
+
36
+ const response = await fetch(loadUrl, {
37
+ method: 'GET',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'X-CSRF-TOKEN': getCsrfToken(board),
41
+ },
42
+ });
43
+
44
+ if (!response.ok) {
45
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
46
+ }
47
+
48
+ const boardData = await response.json();
49
+
50
+ if (boardData && boardData.data) {
51
+ console.log('✅ MoodBoard: данные загружены с сервера', boardData.data);
52
+ board.dataManager.loadData(boardData.data);
53
+ invokeOnLoad(board, { success: true, data: boardData.data });
54
+ } else {
55
+ console.log('📦 MoodBoard: нет данных с сервера, загружаем пустую доску');
56
+ const seedData = getSeedData(board);
57
+ board.dataManager.loadData(seedData);
58
+ invokeOnLoad(board, { success: true, data: seedData });
59
+ }
60
+ } catch (error) {
61
+ console.warn('⚠️ MoodBoard: ошибка загрузки доски, создаем новую:', error.message);
62
+ const seedData = getSeedData(board);
63
+ board.dataManager.loadData(seedData);
64
+ invokeOnLoad(board, { success: false, error: error.message, data: seedData });
65
+ }
66
+ }
67
+
68
+ export async function loadFromApi(board, boardId = null) {
69
+ const targetBoardId = boardId || board.options.boardId;
70
+ if (!targetBoardId) {
71
+ throw new Error('boardId не указан');
72
+ }
73
+
74
+ const originalBoardId = board.options.boardId;
75
+ board.options.boardId = targetBoardId;
76
+
77
+ try {
78
+ await loadExistingBoard(board);
79
+ } finally {
80
+ board.options.boardId = originalBoardId;
81
+ }
82
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ createCompositionCanvas,
3
+ drawHtmlTextOverlay,
4
+ drawPixiCanvas,
5
+ getPixiCanvas,
6
+ wrapText,
7
+ } from './MoodBoardScreenshotCanvas.js';
8
+
9
+ export { wrapText };
10
+
11
+ export function createCombinedScreenshot(board, format = 'image/jpeg', quality = 0.6) {
12
+ if (!board.coreMoodboard || !board.coreMoodboard.pixi || !board.coreMoodboard.pixi.app || !board.coreMoodboard.pixi.app.view) {
13
+ throw new Error('Canvas не найден');
14
+ }
15
+
16
+ try {
17
+ const pixiCanvas = getPixiCanvas(board);
18
+ const { canvas, ctx } = createCompositionCanvas(pixiCanvas);
19
+
20
+ drawPixiCanvas(ctx, pixiCanvas);
21
+ drawHtmlTextOverlay(ctx);
22
+
23
+ return canvas.toDataURL(format, quality);
24
+ } catch (error) {
25
+ console.warn('⚠️ Ошибка при создании объединенного скриншота, используем только PIXI canvas:', error);
26
+ const canvas = getPixiCanvas(board);
27
+ return canvas.toDataURL(format, quality);
28
+ }
29
+ }
30
+
31
+ export function exportScreenshot(board, format = 'image/jpeg', quality = 0.6) {
32
+ return board.createCombinedScreenshot(format, quality);
33
+ }
@@ -0,0 +1,98 @@
1
+ export function getPixiCanvas(board) {
2
+ return board.coreMoodboard.pixi.app.view;
3
+ }
4
+
5
+ export function createCompositionCanvas(sourceCanvas) {
6
+ const combinedCanvas = document.createElement('canvas');
7
+ combinedCanvas.width = sourceCanvas.width;
8
+ combinedCanvas.height = sourceCanvas.height;
9
+
10
+ return {
11
+ canvas: combinedCanvas,
12
+ ctx: combinedCanvas.getContext('2d'),
13
+ };
14
+ }
15
+
16
+ export function drawPixiCanvas(ctx, pixiCanvas) {
17
+ ctx.drawImage(pixiCanvas, 0, 0);
18
+ }
19
+
20
+ export function wrapText(ctx, text, maxWidth) {
21
+ const lines = [];
22
+
23
+ if (!text || maxWidth <= 0) {
24
+ return [text];
25
+ }
26
+
27
+ let currentLine = '';
28
+
29
+ for (let i = 0; i < text.length; i += 1) {
30
+ const char = text[i];
31
+ const testLine = currentLine + char;
32
+ const metrics = ctx.measureText(testLine);
33
+
34
+ if (metrics.width > maxWidth && currentLine !== '') {
35
+ lines.push(currentLine);
36
+ currentLine = char;
37
+ } else {
38
+ currentLine = testLine;
39
+ }
40
+ }
41
+
42
+ if (currentLine) {
43
+ lines.push(currentLine);
44
+ }
45
+
46
+ return lines.length > 0 ? lines : [text];
47
+ }
48
+
49
+ function isDrawableTextElement(textEl, computedStyle) {
50
+ const text = textEl.textContent || '';
51
+
52
+ if (computedStyle.visibility === 'hidden' || computedStyle.opacity === '0' || !text.trim()) {
53
+ return false;
54
+ }
55
+
56
+ return true;
57
+ }
58
+
59
+ function drawTextElement(ctx, textEl, index) {
60
+ try {
61
+ const computedStyle = window.getComputedStyle(textEl);
62
+
63
+ if (!isDrawableTextElement(textEl, computedStyle)) {
64
+ return;
65
+ }
66
+
67
+ const text = textEl.textContent || '';
68
+ const left = parseInt(textEl.style.left) || 0;
69
+ const top = parseInt(textEl.style.top) || 0;
70
+
71
+ const fontSize = parseInt(computedStyle.fontSize) || 18;
72
+ const fontFamily = computedStyle.fontFamily || 'Arial, sans-serif';
73
+ const color = computedStyle.color || '#000000';
74
+
75
+ ctx.font = `${fontSize}px ${fontFamily}`;
76
+ ctx.fillStyle = color;
77
+ ctx.textAlign = 'left';
78
+ ctx.textBaseline = 'top';
79
+
80
+ const elementWidth = parseInt(textEl.style.width) || 182;
81
+ const lines = wrapText(ctx, text, elementWidth);
82
+ const lineHeight = fontSize * 1.3;
83
+
84
+ lines.forEach((line, lineIndex) => {
85
+ const yPos = top + (lineIndex * lineHeight) + 2;
86
+ ctx.fillText(line, left, yPos);
87
+ });
88
+ } catch (error) {
89
+ console.warn(`⚠️ Ошибка при рисовании текста ${index + 1}:`, error);
90
+ }
91
+ }
92
+
93
+ export function drawHtmlTextOverlay(ctx) {
94
+ const textElements = document.querySelectorAll('.mb-text');
95
+ textElements.forEach((textEl, index) => {
96
+ drawTextElement(ctx, textEl, index);
97
+ });
98
+ }
@@ -0,0 +1,97 @@
1
+ export function safeDestroy(obj, name) {
2
+ if (obj) {
3
+ try {
4
+ if (typeof obj.destroy === 'function') {
5
+ obj.destroy();
6
+ } else {
7
+ console.warn(`Объект ${name} не имеет метода destroy()`);
8
+ }
9
+ } catch (error) {
10
+ console.error(`Ошибка при уничтожении ${name}:`, error);
11
+ }
12
+ }
13
+ }
14
+
15
+ export function destroyMoodBoard(board) {
16
+ if (board.destroyed) {
17
+ console.warn('MoodBoard уже был уничтожен');
18
+ return;
19
+ }
20
+
21
+ board.destroyed = true;
22
+
23
+ safeDestroy(board.toolbar, 'toolbar');
24
+ board.toolbar = null;
25
+
26
+ safeDestroy(board.topbar, 'topbar');
27
+ board.topbar = null;
28
+
29
+ safeDestroy(board.saveStatus, 'saveStatus');
30
+ board.saveStatus = null;
31
+
32
+ safeDestroy(board.textPropertiesPanel, 'textPropertiesPanel');
33
+ board.textPropertiesPanel = null;
34
+
35
+ safeDestroy(board.framePropertiesPanel, 'framePropertiesPanel');
36
+ board.framePropertiesPanel = null;
37
+
38
+ safeDestroy(board.notePropertiesPanel, 'notePropertiesPanel');
39
+ board.notePropertiesPanel = null;
40
+
41
+ safeDestroy(board.filePropertiesPanel, 'filePropertiesPanel');
42
+ board.filePropertiesPanel = null;
43
+
44
+ safeDestroy(board.alignmentGuides, 'alignmentGuides');
45
+ board.alignmentGuides = null;
46
+
47
+ // HTML-слои (текст и ручки) также нужно корректно уничтожать,
48
+ // чтобы удалить DOM и отписаться от глобальных слушателей resize/DPR
49
+ safeDestroy(board.htmlTextLayer, 'htmlTextLayer');
50
+ board.htmlTextLayer = null;
51
+
52
+ safeDestroy(board.htmlHandlesLayer, 'htmlHandlesLayer');
53
+ board.htmlHandlesLayer = null;
54
+
55
+ safeDestroy(board.commentPopover, 'commentPopover');
56
+ board.commentPopover = null;
57
+
58
+ safeDestroy(board.contextMenu, 'contextMenu');
59
+ board.contextMenu = null;
60
+
61
+ safeDestroy(board.zoombar, 'zoombar');
62
+ board.zoombar = null;
63
+
64
+ safeDestroy(board.mapbar, 'mapbar');
65
+ board.mapbar = null;
66
+
67
+ safeDestroy(board.coreMoodboard, 'coreMoodboard');
68
+ board.coreMoodboard = null;
69
+
70
+ safeDestroy(board.workspaceManager, 'workspaceManager');
71
+ board.workspaceManager = null;
72
+
73
+ board.dataManager = null;
74
+ board.actionHandler = null;
75
+
76
+ if (board.container) {
77
+ board.container.classList.remove('moodboard-root');
78
+ }
79
+ board.container = null;
80
+
81
+ if (typeof window !== 'undefined') {
82
+ if (window.moodboardHtmlTextLayer === board.htmlTextLayer) {
83
+ window.moodboardHtmlTextLayer = null;
84
+ }
85
+ if (window.moodboardHtmlHandlesLayer === board.htmlHandlesLayer) {
86
+ window.moodboardHtmlHandlesLayer = null;
87
+ }
88
+ }
89
+
90
+ if (typeof board.options.onDestroy === 'function') {
91
+ try {
92
+ board.options.onDestroy();
93
+ } catch (error) {
94
+ console.warn('⚠️ Ошибка в коллбеке onDestroy:', error);
95
+ }
96
+ }
97
+ }
@@ -221,19 +221,30 @@ export class FileObject {
221
221
  g.lineTo(x + size * 0.8 - cornerSize, y);
222
222
  g.endFill();
223
223
 
224
- // Текст расширения на иконке
224
+ // Текст расширения на иконке — удаляем старый перед созданием нового (предотвращает утечку при _redraw)
225
+ if (this._extensionText) {
226
+ try {
227
+ this.container.removeChild(this._extensionText);
228
+ } catch (_) {}
229
+ try {
230
+ this._extensionText.destroy();
231
+ } catch (_) {}
232
+ this._extensionText = null;
233
+ }
225
234
  if (extension && extension.length <= 4) {
226
- const extensionText = new PIXI.Text(extension.toUpperCase(), {
235
+ this._extensionText = new PIXI.Text(extension.toUpperCase(), {
227
236
  fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
228
237
  fontSize: Math.max(8, size * 0.2),
229
238
  fill: 0xFFFFFF,
230
239
  align: 'center',
231
240
  fontWeight: 'bold'
232
241
  });
233
- extensionText.anchor.set(0.5, 0.5);
234
- extensionText.x = x + size * 0.4;
235
- extensionText.y = y + size * 0.7;
236
- this.container.addChild(extensionText);
242
+ this._extensionText.anchor.set(0.5, 0.5);
243
+ this._extensionText.x = x + size * 0.4;
244
+ this._extensionText.y = y + size * 0.7;
245
+ this.container.addChild(this._extensionText);
246
+ } else {
247
+ this._extensionText = null;
237
248
  }
238
249
  }
239
250
 
@@ -31,6 +31,7 @@ export class FrameObject {
31
31
  }
32
32
  this.cornerRadius = Number.isFinite(cssCornerRadius) ? cssCornerRadius : 6;
33
33
  this.title = this.objectData.title || this.objectData.properties?.title || 'Новый';
34
+ this._borderVisible = true;
34
35
 
35
36
  // Создаем контейнер для фрейма и заголовка
36
37
  this.container = new PIXI.Container();
@@ -58,9 +59,16 @@ export class FrameObject {
58
59
 
59
60
  // Подписываемся на события зума для компенсации масштабирования заголовка
60
61
  if (this.eventBus) {
61
- this.eventBus.on(Events.UI.ZoomPercent, this._onZoomChange.bind(this));
62
+ this._boundOnZoomChange = this._onZoomChange.bind(this);
63
+ this.eventBus.on(Events.UI.ZoomPercent, this._boundOnZoomChange);
64
+ this._boundOnSelectionAdd = this._onSelectionAdd.bind(this);
65
+ this._boundOnSelectionRemove = this._onSelectionRemove.bind(this);
66
+ this._boundOnSelectionClear = this._onSelectionClear.bind(this);
67
+ this.eventBus.on(Events.Tool.SelectionAdd, this._boundOnSelectionAdd);
68
+ this.eventBus.on(Events.Tool.SelectionRemove, this._boundOnSelectionRemove);
69
+ this.eventBus.on(Events.Tool.SelectionClear, this._boundOnSelectionClear);
62
70
  }
63
-
71
+
64
72
  this._draw(this.width, this.height, this.fillColor);
65
73
  // Применяем начальный масштаб и обрезку заголовка
66
74
  this._updateTitleScale();
@@ -87,6 +95,27 @@ export class FrameObject {
87
95
  this._redrawPreserveTransform(this.width, this.height, this.fillColor);
88
96
  }
89
97
 
98
+ /** Скрыть/показать серую рамку (при выделении скрываем, чтобы не накладывалась на синюю) */
99
+ setBorderVisible(visible) {
100
+ if (this._borderVisible === visible) return;
101
+ this._borderVisible = visible;
102
+ this._redrawPreserveTransform(this.width, this.height, this.fillColor);
103
+ }
104
+
105
+ _onSelectionAdd(data) {
106
+ const myId = this.objectData?.id ?? this.container?._mb?.objectId;
107
+ if (data?.object === myId) this.setBorderVisible(false);
108
+ }
109
+
110
+ _onSelectionRemove(data) {
111
+ if (data?.object === (this.objectData?.id ?? this.container?._mb?.objectId)) this.setBorderVisible(true);
112
+ }
113
+
114
+ _onSelectionClear(data) {
115
+ const myId = this.objectData?.id ?? this.container?._mb?.objectId;
116
+ if (data?.objects?.includes(myId)) this.setBorderVisible(true);
117
+ }
118
+
90
119
  /**
91
120
  * Установить заголовок фрейма
92
121
  * @param {string} title Новый заголовок
@@ -132,7 +161,7 @@ export class FrameObject {
132
161
  const pivotX = width / 2;
133
162
  const pivotY = height / 2;
134
163
 
135
- this._draw(width, height, color);
164
+ this._draw(width, height, color, this._borderVisible);
136
165
 
137
166
  container.pivot.set(pivotX, pivotY);
138
167
  container.x = x;
@@ -145,15 +174,17 @@ export class FrameObject {
145
174
 
146
175
  /**
147
176
  * Базовая отрисовка
177
+ * @param {boolean} showStroke — рисовать ли серую рамку (скрываем при выделении)
148
178
  */
149
- _draw(width, height, color) {
179
+ _draw(width, height, color, showStroke = true) {
150
180
  const g = this.graphics;
151
181
  g.clear();
152
- // Рисуем с выравниванием обводки внутрь, чтобы внешний контур был ровно width x height
153
- try {
154
- g.lineStyle({ width: this.borderWidth, color: this.strokeColor, alpha: 1, alignment: 1 });
155
- } catch (e) {
156
- g.lineStyle(this.borderWidth, this.strokeColor, 1);
182
+ if (showStroke) {
183
+ try {
184
+ g.lineStyle({ width: this.borderWidth, color: this.strokeColor, alpha: 1, alignment: 1 });
185
+ } catch (e) {
186
+ g.lineStyle(this.borderWidth, this.strokeColor, 1);
187
+ }
157
188
  }
158
189
  g.beginFill(typeof color === 'number' ? color : 0xFFFFFF, 1);
159
190
  g.drawRoundedRect(0, 0, Math.max(0, width), Math.max(0, height), this.cornerRadius);
@@ -282,7 +313,16 @@ export class FrameObject {
282
313
  */
283
314
  destroy() {
284
315
  if (this.eventBus) {
285
- this.eventBus.off(Events.UI.ZoomPercent, this._onZoomChange.bind(this));
316
+ if (this._boundOnZoomChange) {
317
+ this.eventBus.off(Events.UI.ZoomPercent, this._boundOnZoomChange);
318
+ this._boundOnZoomChange = null;
319
+ }
320
+ if (this._boundOnSelectionAdd) {
321
+ this.eventBus.off(Events.Tool.SelectionAdd, this._boundOnSelectionAdd);
322
+ this.eventBus.off(Events.Tool.SelectionRemove, this._boundOnSelectionRemove);
323
+ this.eventBus.off(Events.Tool.SelectionClear, this._boundOnSelectionClear);
324
+ this._boundOnSelectionAdd = this._boundOnSelectionRemove = this._boundOnSelectionClear = null;
325
+ }
286
326
  }
287
327
  if (this.container) {
288
328
  this.container.destroy({ children: true });
@@ -186,10 +186,11 @@ export class NoteObject {
186
186
  if (!size) return;
187
187
  let w = Math.max(80, size.width || this.width);
188
188
  let h = Math.max(60, size.height || this.height);
189
- // Держим квадрат
190
- const side = Math.max(w, h);
191
- this.width = side;
192
- this.height = side;
189
+ // Политика квадратного resize применяется на уровне gesture/flow.
190
+ // Здесь важно уважать уже нормализованный размер, иначе group-resize
191
+ // начинает расходиться с рассчитанной рамкой группы.
192
+ this.width = w;
193
+ this.height = h;
193
194
 
194
195
  this._redraw();
195
196
  this._updateTextPosition();