@sequent-org/moodboard 1.4.32 → 1.4.33

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 (136) hide show
  1. package/package.json +5 -1
  2. package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
  3. package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
  4. package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  5. package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  6. package/src/assets/icons/attachments.svg +3 -1
  7. package/src/assets/icons/comments.svg +2 -2
  8. package/src/assets/icons/connector.svg +6 -0
  9. package/src/assets/icons/emoji.svg +6 -1
  10. package/src/assets/icons/frame.svg +4 -1
  11. package/src/assets/icons/image.svg +5 -1
  12. package/src/assets/icons/laser.svg +1 -0
  13. package/src/assets/icons/lasso.svg +5 -0
  14. package/src/assets/icons/mindmap.svg +10 -2
  15. package/src/assets/icons/note.svg +4 -1
  16. package/src/assets/icons/pan.svg +5 -2
  17. package/src/assets/icons/pencil.svg +4 -1
  18. package/src/assets/icons/reactions.svg +5 -0
  19. package/src/assets/icons/redo.svg +3 -2
  20. package/src/assets/icons/select.svg +2 -8
  21. package/src/assets/icons/shapes.svg +5 -1
  22. package/src/assets/icons/text-add.svg +15 -1
  23. package/src/assets/icons/undo.svg +3 -2
  24. package/src/assets/reactions/1f44d.svg +20 -0
  25. package/src/assets/reactions/1f44e.svg +20 -0
  26. package/src/assets/reactions/2705.svg +20 -0
  27. package/src/assets/reactions/274c.svg +19 -0
  28. package/src/assets/reactions/2753.svg +20 -0
  29. package/src/assets/reactions/2764.svg +22 -0
  30. package/src/assets/reactions/2b50.svg +19 -0
  31. package/src/assets/reactions/plus-one.svg +25 -0
  32. package/src/core/PixiEngine.js +23 -0
  33. package/src/core/bootstrap/CoreInitializer.js +43 -0
  34. package/src/core/commands/GroupDeleteCommand.js +13 -1
  35. package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
  36. package/src/core/commands/UpdateTextStyleCommand.js +17 -6
  37. package/src/core/commands/index.js +3 -0
  38. package/src/core/events/Events.js +22 -0
  39. package/src/core/flows/LayerAndViewportFlow.js +1 -0
  40. package/src/core/flows/ObjectLifecycleFlow.js +155 -7
  41. package/src/core/index.js +28 -1
  42. package/src/grid/CrossGridZoomPhases.js +3 -3
  43. package/src/initNoBundler.js +1 -1
  44. package/src/moodboard/DataManager.js +28 -0
  45. package/src/moodboard/MoodBoard.js +27 -0
  46. package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
  47. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
  48. package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
  49. package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
  50. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
  51. package/src/objects/ConnectorObject.js +2 -2
  52. package/src/objects/FrameObject.js +119 -59
  53. package/src/objects/ShapeObject.js +49 -74
  54. package/src/objects/shape/ShapeDrawer.js +210 -0
  55. package/src/services/ConnectorBindingResolver.js +112 -0
  56. package/src/services/ConnectorRouter.js +210 -0
  57. package/src/services/comments/CommentService.js +344 -0
  58. package/src/tools/object-tools/CommentTool.js +85 -0
  59. package/src/tools/object-tools/DrawingTool.js +110 -10
  60. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  61. package/src/tools/object-tools/SelectTool.js +25 -1
  62. package/src/tools/object-tools/TextTool.js +6 -1
  63. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  64. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  65. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  66. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  67. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  68. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  69. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  70. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  71. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  72. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  73. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  74. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  75. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  76. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  77. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  78. package/src/ui/CommentPopover.js +6 -0
  79. package/src/ui/CommentsBar.js +91 -0
  80. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  81. package/src/ui/ContextMenu.js +25 -0
  82. package/src/ui/DrawingPropertiesPanel.js +362 -0
  83. package/src/ui/FilePropertiesPanel.js +5 -0
  84. package/src/ui/FramePropertiesPanel.js +5 -0
  85. package/src/ui/HtmlTextLayer.js +246 -66
  86. package/src/ui/NotePropertiesPanel.js +6 -0
  87. package/src/ui/ShapePropertiesPanel.js +307 -0
  88. package/src/ui/TextPropertiesPanel.js +100 -1
  89. package/src/ui/Toolbar.js +25 -2
  90. package/src/ui/Topbar.js +2 -2
  91. package/src/ui/animation/HoverLiftController.js +6 -7
  92. package/src/ui/chat/ChatComposer.js +58 -7
  93. package/src/ui/chat/ChatWindow.js +60 -143
  94. package/src/ui/comments/CommentListPanel.js +213 -0
  95. package/src/ui/comments/CommentPinLayer.js +448 -0
  96. package/src/ui/comments/CommentThreadPopover.js +539 -0
  97. package/src/ui/comments/commentFormat.js +32 -0
  98. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  99. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  100. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  103. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  104. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  105. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  106. package/src/ui/connectors/ConnectorLayer.js +264 -57
  107. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  108. package/src/ui/handles/HandlesEventBridge.js +1 -0
  109. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  110. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  111. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  112. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  113. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  114. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  115. package/src/ui/styles/chat.css +709 -19
  116. package/src/ui/styles/index.css +1 -0
  117. package/src/ui/styles/panels.css +112 -2
  118. package/src/ui/styles/shape-properties-panel.css +250 -0
  119. package/src/ui/styles/toolbar.css +7 -2
  120. package/src/ui/styles/topbar.css +1 -1
  121. package/src/ui/styles/workspace.css +257 -6
  122. package/src/ui/text-properties/TextFormatControls.js +88 -0
  123. package/src/ui/text-properties/TextListRenderer.js +137 -0
  124. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  125. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  126. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  127. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  128. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  129. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  130. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  131. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  132. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  133. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  134. package/src/utils/iconLoader.js +17 -16
  135. package/src/utils/markdown.js +14 -0
  136. package/src/utils/richText.js +125 -0
@@ -2,6 +2,73 @@ import { CoreMoodBoard } from '../../core/index.js';
2
2
  import { createMoodBoardManagers, wireMoodBoardServices } from './MoodBoardManagersFactory.js';
3
3
  import { createMoodBoardUi } from './MoodBoardUiFactory.js';
4
4
  import { bindSaveCallbacks } from '../integration/MoodBoardEventBindings.js';
5
+ import { CommentService } from '../../services/comments/CommentService.js';
6
+ import { CommentPinLayer } from '../../ui/comments/CommentPinLayer.js';
7
+ import { CommentsBar } from '../../ui/CommentsBar.js';
8
+ import { CommentThreadPopover } from '../../ui/comments/CommentThreadPopover.js';
9
+ import { CommentTool } from '../../tools/object-tools/CommentTool.js';
10
+ import { CommentListPanel } from '../../ui/comments/CommentListPanel.js';
11
+
12
+ export async function wireCommentsSubsystem(board) {
13
+ if (!board.options.enableComments || !board.options.comments) return;
14
+
15
+ const core = board.coreMoodboard;
16
+ const boardId = board.options.boardId || 'workspace-board';
17
+
18
+ board.commentService = new CommentService({
19
+ eventBus: core.eventBus,
20
+ boardId,
21
+ adapter: board.options.comments,
22
+ currentUser: board.options.currentUser || null,
23
+ });
24
+ board.commentService.attach();
25
+
26
+ board.commentThreadPopover = new CommentThreadPopover(
27
+ board.canvasContainer,
28
+ core.eventBus,
29
+ core,
30
+ board.commentService
31
+ );
32
+ board.commentThreadPopover.attach();
33
+
34
+ board.commentPinLayer = new CommentPinLayer(
35
+ board.canvasContainer,
36
+ core.eventBus,
37
+ core,
38
+ board.commentService
39
+ );
40
+ board.commentPinLayer.attach();
41
+
42
+ board.commentsBar = new CommentsBar(board.workspaceElement, core.eventBus);
43
+ board.commentsBar.attach();
44
+
45
+ board.commentListPanel = new CommentListPanel(board.workspaceElement, core.eventBus, core, board.commentService);
46
+ board.commentListPanel.attach();
47
+
48
+ const commentTool = new CommentTool(
49
+ core.eventBus,
50
+ core,
51
+ board.commentService,
52
+ board.commentThreadPopover
53
+ );
54
+ core.toolManager.registerTool(commentTool);
55
+
56
+ board.comments = {
57
+ applyRemote: (event) => board.commentService.applyRemote(event),
58
+ openThread: (threadId) => board.commentService.openThread(threadId),
59
+ };
60
+
61
+ try {
62
+ await board.commentService.loadInitial();
63
+ board.commentPinLayer.rebuild();
64
+ const initialThreadId = board.options.initialThreadId;
65
+ if (initialThreadId != null) {
66
+ board.commentService.openThread(Number(initialThreadId));
67
+ }
68
+ } catch (err) {
69
+ console.error('Comments load failed:', err);
70
+ }
71
+ }
5
72
 
6
73
  export async function initCoreMoodBoard(board) {
7
74
  const canvasSize = board.workspaceManager.getCanvasSize();
@@ -10,7 +77,7 @@ export async function initCoreMoodBoard(board) {
10
77
  boardId: board.options.boardId || 'workspace-board',
11
78
  width: canvasSize.width,
12
79
  height: canvasSize.height,
13
- backgroundColor: board.options.theme === 'dark' ? 0x2a2a2a : 0xF7FBFF,
80
+ backgroundColor: board.options.theme === 'dark' ? 0x2a2a2a : 0xDAEEFB,
14
81
  saveEndpoint: board.options.saveEndpoint,
15
82
  loadEndpoint: board.options.loadEndpoint,
16
83
  };
@@ -34,6 +101,7 @@ export async function initializeMoodBoard(board) {
34
101
  await initCoreMoodBoard(board);
35
102
  createMoodBoardManagers(board);
36
103
  createMoodBoardUi(board);
104
+ await wireCommentsSubsystem(board);
37
105
  wireMoodBoardServices(board);
38
106
  bindSaveCallbacks(board);
39
107
 
@@ -10,13 +10,17 @@ import { MindmapHtmlTextLayer } from '../../ui/mindmap/MindmapHtmlTextLayer.js';
10
10
  import { MindmapConnectionLayer } from '../../ui/mindmap/MindmapConnectionLayer.js';
11
11
  import { MindmapCollapseLayer } from '../../ui/mindmap/MindmapCollapseLayer.js';
12
12
  import { ConnectorLayer } from '../../ui/connectors/ConnectorLayer.js';
13
+ import { ConnectorLabelLayer } from '../../ui/connectors/ConnectorLabelLayer.js';
13
14
  import { ConnectionAnchorsLayer } from '../../ui/connectors/ConnectionAnchorsLayer.js';
15
+ import { ConnectorHandlesLayer } from '../../ui/connectors/ConnectorHandlesLayer.js';
14
16
  import { HtmlHandlesLayer } from '../../ui/HtmlHandlesLayer.js';
15
- import { CommentPopover } from '../../ui/CommentPopover.js';
16
17
  import { TextPropertiesPanel } from '../../ui/TextPropertiesPanel.js';
17
18
  import { FramePropertiesPanel } from '../../ui/FramePropertiesPanel.js';
18
19
  import { NotePropertiesPanel } from '../../ui/NotePropertiesPanel.js';
19
20
  import { FilePropertiesPanel } from '../../ui/FilePropertiesPanel.js';
21
+ import { ConnectorPropertiesPanel } from '../../ui/ConnectorPropertiesPanel.js';
22
+ import { ShapePropertiesPanel } from '../../ui/ShapePropertiesPanel.js';
23
+ import { DrawingPropertiesPanel } from '../../ui/DrawingPropertiesPanel.js';
20
24
  import { ChatWindow } from '../../ui/chat/ChatWindow.js';
21
25
  import { bindToolbarEvents, bindTopbarEvents } from '../integration/MoodBoardEventBindings.js';
22
26
 
@@ -29,6 +33,7 @@ function initToolbar(board) {
29
33
  emojiBasePath: board.options.emojiBasePath || null,
30
34
  }
31
35
  );
36
+ board.toolbar.enableComments = !!board.options.enableComments;
32
37
 
33
38
  if (typeof window !== 'undefined') {
34
39
  window.reloadIcon = (iconName) => board.toolbar.reloadToolbarIcon(iconName);
@@ -88,6 +93,9 @@ function initContextMenu(board) {
88
93
  board.canvasContainer,
89
94
  board.coreMoodboard.eventBus
90
95
  );
96
+ if (board.options.enableComments) {
97
+ board.contextMenu.setEnableComments(true);
98
+ }
91
99
  }
92
100
 
93
101
  function initChatWindow(board) {
@@ -110,10 +118,18 @@ function initHtmlLayersAndPanels(board) {
110
118
 
111
119
  board.connectorLayer = new ConnectorLayer(board.coreMoodboard.eventBus, board.coreMoodboard);
112
120
  board.connectorLayer.attach();
121
+ board.coreMoodboard.connectorLayer = board.connectorLayer;
122
+
123
+ board.connectorLabelLayer = new ConnectorLabelLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
124
+ board.connectorLabelLayer.attach();
125
+ board.coreMoodboard.connectorLabelLayer = board.connectorLabelLayer;
113
126
 
114
127
  board.connectionAnchorsLayer = new ConnectionAnchorsLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
115
128
  board.connectionAnchorsLayer.attach();
116
129
 
130
+ board.connectorHandlesLayer = new ConnectorHandlesLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
131
+ board.connectorHandlesLayer.attach();
132
+
117
133
  board.htmlHandlesLayer = new HtmlHandlesLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
118
134
  board.htmlHandlesLayer.attach();
119
135
 
@@ -123,19 +139,21 @@ function initHtmlLayersAndPanels(board) {
123
139
  window.moodboardMindmapConnectionLayer = board.mindmapConnectionLayer;
124
140
  window.moodboardMindmapCollapseLayer = board.mindmapCollapseLayer;
125
141
  window.moodboardConnectorLayer = board.connectorLayer;
142
+ window.moodboardConnectorLabelLayer = board.connectorLabelLayer;
126
143
  window.moodboardConnectionAnchorsLayer = board.connectionAnchorsLayer;
144
+ window.moodboardConnectorHandlesLayer = board.connectorHandlesLayer;
127
145
  window.moodboardHtmlHandlesLayer = board.htmlHandlesLayer;
128
146
  }
129
147
 
130
- board.commentPopover = new CommentPopover(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
131
- board.commentPopover.attach();
132
-
133
148
  board.textPropertiesPanel = new TextPropertiesPanel(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
134
149
  board.textPropertiesPanel.attach();
135
150
 
136
151
  board.framePropertiesPanel = new FramePropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
137
152
  board.notePropertiesPanel = new NotePropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
138
153
  board.filePropertiesPanel = new FilePropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
154
+ board.connectorPropertiesPanel = new ConnectorPropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
155
+ board.shapePropertiesPanel = new ShapePropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
156
+ board.drawingPropertiesPanel = new DrawingPropertiesPanel(board.coreMoodboard.eventBus, board.canvasContainer, board.coreMoodboard);
139
157
  }
140
158
 
141
159
  export function createMoodBoardUi(board) {
@@ -54,7 +54,11 @@ export function bindToolbarEvents(board) {
54
54
  }
55
55
  const createdObject = board.actionHandler.handleToolbarAction(action);
56
56
  if (createdObject?.id) {
57
- focusCreatedObject(board, createdObject);
57
+ // Рисунки карандашом/кистью не выделяем и не переключаем инструмент:
58
+ // пользователь должен сразу продолжить рисовать следующий штрих.
59
+ if (action?.type !== 'drawing') {
60
+ focusCreatedObject(board, createdObject);
61
+ }
58
62
  }
59
63
  });
60
64
 
@@ -1,7 +1,16 @@
1
1
  import { Events } from '../../core/events/Events.js';
2
2
 
3
+ const BOARD_DEFAULTS = {
4
+ backgroundColor: '#daeefb',
5
+ grid: { type: 'cross' },
6
+ };
7
+
3
8
  function getSeedData(board) {
4
- return board.data || { objects: [] };
9
+ const data = board.data || { objects: [] };
10
+ if (!data.settings) {
11
+ return { ...data, settings: { ...BOARD_DEFAULTS } };
12
+ }
13
+ return data;
5
14
  }
6
15
 
7
16
  function invokeOnLoad(board, payload) {
@@ -41,6 +41,9 @@ export function destroyMoodBoard(board) {
41
41
  safeDestroy(board.filePropertiesPanel, 'filePropertiesPanel');
42
42
  board.filePropertiesPanel = null;
43
43
 
44
+ safeDestroy(board.drawingPropertiesPanel, 'drawingPropertiesPanel');
45
+ board.drawingPropertiesPanel = null;
46
+
44
47
  safeDestroy(board.alignmentGuides, 'alignmentGuides');
45
48
  board.alignmentGuides = null;
46
49
 
@@ -64,6 +67,9 @@ export function destroyMoodBoard(board) {
64
67
  safeDestroy(board.connectionAnchorsLayer, 'connectionAnchorsLayer');
65
68
  board.connectionAnchorsLayer = null;
66
69
 
70
+ safeDestroy(board.connectorHandlesLayer, 'connectorHandlesLayer');
71
+ board.connectorHandlesLayer = null;
72
+
67
73
  safeDestroy(board.htmlHandlesLayer, 'htmlHandlesLayer');
68
74
  board.htmlHandlesLayer = null;
69
75
 
@@ -118,6 +124,9 @@ export function destroyMoodBoard(board) {
118
124
  if (window.moodboardConnectionAnchorsLayer === board.connectionAnchorsLayer) {
119
125
  window.moodboardConnectionAnchorsLayer = null;
120
126
  }
127
+ if (window.moodboardConnectorHandlesLayer === board.connectorHandlesLayer) {
128
+ window.moodboardConnectorHandlesLayer = null;
129
+ }
121
130
  if (window.moodboardHtmlHandlesLayer === board.htmlHandlesLayer) {
122
131
  window.moodboardHtmlHandlesLayer = null;
123
132
  }
@@ -29,8 +29,8 @@ export class ConnectorObject {
29
29
  stroke: 0x2563EB,
30
30
  width: 2,
31
31
  dash: false,
32
- head: { start: false, end: true },
33
- route: 'straight',
32
+ head: { start: 'none', end: 'arrow' },
33
+ route: 'elbow',
34
34
  ...(props.style || {}),
35
35
  };
36
36
 
@@ -40,22 +40,28 @@ export class FrameObject {
40
40
  this.graphics = new PIXI.Graphics();
41
41
  this.container.addChild(this.graphics);
42
42
 
43
- // Текст заголовка
44
- this.baseFontSize = 14; // Сохраняем оригинальный размер шрифта
45
- this.currentWorldScale = 1.0; // Текущий масштаб мира
46
- this.originalTitle = this.title; // Сохраняем оригинальный заголовок
43
+ // Заголовок фрейма — слой над верхней границей с собственной подложкой
44
+ this.baseFontSize = 14;
45
+ this.currentWorldScale = 1.0;
46
+ this.originalTitle = this.title;
47
+
48
+ // Под-контейнер: масштаб компенсирует зум, поэтому заголовок всегда одного размера на экране
49
+ this.titleLayer = new PIXI.Container();
50
+ this.titleLayer.eventMode = 'none'; // не перехватывать указатель
51
+
52
+ this.titleBg = new PIXI.Graphics();
53
+ this.titleLayer.addChild(this.titleBg);
54
+
47
55
  this.titleText = new PIXI.Text(this.title, {
48
- fontFamily: 'Arial, sans-serif',
56
+ fontFamily: 'Inter, Arial, sans-serif',
49
57
  fontSize: this.baseFontSize,
50
58
  fill: 0x333333,
51
- fontWeight: 'bold'
59
+ fontWeight: '500'
52
60
  });
53
- // Размещаем заголовок внутри верхней части фрейма, чтобы не влиять на внешние границы
54
61
  this.titleText.anchor.set(0, 0);
55
- this.titleText.scale.set(1); // Инициализируем базовый масштаб
56
- this.titleText.x = 8;
57
- this.titleText.y = 4;
58
- this.container.addChild(this.titleText);
62
+ this.titleLayer.addChild(this.titleText);
63
+
64
+ this.container.addChild(this.titleLayer);
59
65
 
60
66
  // Подписываемся на события зума для компенсации масштабирования заголовка
61
67
  if (this.eventBus) {
@@ -69,6 +75,24 @@ export class FrameObject {
69
75
  this.eventBus.on(Events.Tool.SelectionClear, this._boundOnSelectionClear);
70
76
  }
71
77
 
78
+ // Логические габариты фрейма = только прямоугольник, без плавающего
79
+ // заголовка над верхней границей. Заголовок — отдельный слой с
80
+ // отрицательным y, и по умолчанию он раздул бы getLocalBounds вверх.
81
+ // Через него width/height контейнера считает GetObjectPosition
82
+ // (position.y = centerY - height/2) — лишняя высота сверху уводила
83
+ // рамку выделения вверх. Переопределяем getLocalBounds, чтобы
84
+ // width/height отражали именно прямоугольник. getBounds НЕ трогаем:
85
+ // hover-lift DropShadowFilter берёт область из getBounds, и заголовок
86
+ // не должен обрезаться фильтром.
87
+ this.container.getLocalBounds = (rect) => {
88
+ const b = rect || new PIXI.Rectangle();
89
+ b.x = 0;
90
+ b.y = 0;
91
+ b.width = this.width;
92
+ b.height = this.height;
93
+ return b;
94
+ };
95
+
72
96
  this._draw(this.width, this.height, this.fillColor);
73
97
  // Применяем начальный масштаб и обрезку заголовка
74
98
  this._updateTitleScale();
@@ -116,6 +140,27 @@ export class FrameObject {
116
140
  if (data?.objects?.includes(myId)) this.setBorderVisible(true);
117
141
  }
118
142
 
143
+ /**
144
+ * Применить текущий масштаб мира к заголовку.
145
+ * Нужно при создании объекта: viewport-зум восстанавливается раньше,
146
+ * чем фрейм успевает подписаться на ZoomPercent, поэтому стартовый зум
147
+ * до него не доходит и заголовок остаётся в мировом масштабе (мелкий).
148
+ * @param {number} worldScale Текущий масштаб мира (world.scale.x)
149
+ */
150
+ applyWorldScale(worldScale) {
151
+ if (typeof worldScale !== 'number' || !(worldScale > 0)) return;
152
+ this.currentWorldScale = worldScale;
153
+ this._updateTitleScale();
154
+ }
155
+
156
+ hideTitle() {
157
+ if (this.titleLayer) this.titleLayer.visible = false;
158
+ }
159
+
160
+ showTitle() {
161
+ if (this.titleLayer) this.titleLayer.visible = true;
162
+ }
163
+
119
164
  /**
120
165
  * Установить заголовок фрейма
121
166
  * @param {string} title Новый заголовок
@@ -213,98 +258,113 @@ export class FrameObject {
213
258
  }
214
259
 
215
260
  /**
216
- * Обновить масштаб заголовка для компенсации зума
261
+ * Масштаб и позиция слоя заголовка компенсируем зум, держим постоянный экранный размер
217
262
  */
218
263
  _updateTitleScale() {
219
- if (!this.titleText) return;
220
-
221
- // Компенсируем зум мира обратным масштабированием заголовка
264
+ if (!this.titleLayer) return;
265
+
222
266
  const compensationScale = 1 / this.currentWorldScale;
223
-
224
- // Используем scale вместо fontSize для избежания размытия
225
- this.titleText.scale.set(compensationScale);
226
-
227
- // Корректируем позицию заголовка с учетом изменения масштаба
228
- this.titleText.x = 8 * compensationScale;
229
- this.titleText.y = 4 * compensationScale;
230
-
231
- // Обновляем текст с учетом нового масштаба
267
+
268
+ // Весь слой масштабируется обратно содержимое выглядит одинаково на любом зуме
269
+ this.titleLayer.scale.set(compensationScale);
270
+
271
+ // Высота подложки в базовых пикселях: baseFontSize + 4px сверху + 4px снизу
272
+ const labelBaseH = this.baseFontSize + 8;
273
+ const gap = 4; // зазор между нижним краем подписи и верхней границей фрейма
274
+
275
+ // Позиционируем над фреймом (y=0 верхний край фрейма в локальных координатах контейнера)
276
+ this.titleLayer.x = 0;
277
+ this.titleLayer.y = -Math.round((labelBaseH + gap) * compensationScale);
278
+
232
279
  this._updateTitleText();
233
280
  }
234
281
 
235
282
  /**
236
- * Обновить текст заголовка с учетом доступной ширины
283
+ * Обновить текст заголовка и перерисовать подложку
237
284
  */
238
285
  _updateTitleText() {
239
286
  if (!this.titleText) return;
240
287
 
241
288
  const truncatedText = this._truncateTextToFit(this.originalTitle);
242
289
  this.titleText.text = truncatedText;
290
+ this._redrawTitleBg();
243
291
  }
244
292
 
245
293
  /**
246
- * Обрезать текст до доступной ширины с добавлением многоточия
294
+ * Нарисовать скруглённую подложку под текущую ширину текста
295
+ */
296
+ _redrawTitleBg() {
297
+ if (!this.titleBg || !this.titleText) return;
298
+
299
+ const padH = 8; // горизонтальный отступ с каждой стороны
300
+ const padV = 4; // вертикальный отступ с каждой стороны
301
+
302
+ // Измеряем текст в базовых единицах
303
+ const style = new PIXI.TextStyle({
304
+ fontFamily: this.titleText.style.fontFamily,
305
+ fontSize: this.baseFontSize,
306
+ fontWeight: this.titleText.style.fontWeight
307
+ });
308
+ const metrics = PIXI.TextMetrics.measureText(this.titleText.text || '', style);
309
+
310
+ const bgW = Math.max(1, Math.round(metrics.width + padH * 2));
311
+ const bgH = Math.round(this.baseFontSize + padV * 2);
312
+
313
+ const g = this.titleBg;
314
+ g.clear();
315
+ try {
316
+ g.lineStyle({ width: 1, color: this.strokeColor, alpha: 1 });
317
+ } catch (_) {
318
+ g.lineStyle(1, this.strokeColor, 1);
319
+ }
320
+ g.beginFill(0xFFFFFF, 1);
321
+ g.drawRoundedRect(0, 0, bgW, bgH, 6);
322
+ g.endFill();
323
+
324
+ // Текст внутри подложки
325
+ this.titleText.x = padH;
326
+ this.titleText.y = padV;
327
+ }
328
+
329
+ /**
330
+ * Обрезать текст до ширины фрейма (потолок) с добавлением многоточия.
331
+ * Сравниваем в базовых пикселях — слой уже компенсирует зум отдельно.
247
332
  * @param {string} text Исходный текст
248
- * @returns {string} Обрезанный текст с многоточием или оригинальный текст
333
+ * @returns {string} Обрезанный текст или оригинал
249
334
  */
250
335
  _truncateTextToFit(text) {
251
336
  if (!text || !this.titleText) return text;
252
337
 
253
- // Компенсация масштаба для правильного расчета размеров
254
- const compensationScale = 1 / this.currentWorldScale;
255
-
256
- // Доступная ширина = ширина фрейма - отступы слева и справа (с учетом масштаба)
257
- const leftPadding = 8 * compensationScale;
258
- const rightPadding = 8 * compensationScale;
259
- const availableWidth = this.width - leftPadding - rightPadding;
338
+ // Подложка не должна быть шире самого фрейма (8px паддинг с каждой стороны)
339
+ const availableWidth = Math.max(1, this.width - 16);
260
340
 
261
- // Создаем временный стиль для измерения текста
262
- // Используем базовый размер шрифта, а масштаб учтем отдельно
263
341
  const style = new PIXI.TextStyle({
264
342
  fontFamily: this.titleText.style.fontFamily,
265
343
  fontSize: this.baseFontSize,
266
344
  fontWeight: this.titleText.style.fontWeight
267
345
  });
268
346
 
269
- // Измеряем ширину оригинального текста с учетом масштаба
270
347
  const textMetrics = PIXI.TextMetrics.measureText(text, style);
271
- const scaledTextWidth = textMetrics.width * compensationScale;
272
-
273
- // Если текст помещается, возвращаем его как есть
274
- if (scaledTextWidth <= availableWidth) {
275
- return text;
276
- }
348
+ if (textMetrics.width <= availableWidth) return text;
277
349
 
278
- // Измеряем ширину многоточия с учетом масштаба
279
350
  const ellipsisMetrics = PIXI.TextMetrics.measureText('...', style);
280
- const ellipsisWidth = ellipsisMetrics.width * compensationScale;
281
-
282
- // Доступная ширина для текста без многоточия
283
- const textAvailableWidth = availableWidth - ellipsisWidth;
284
-
285
- if (textAvailableWidth <= 0) {
286
- return '...';
287
- }
351
+ const textAvailableWidth = availableWidth - ellipsisMetrics.width;
352
+ if (textAvailableWidth <= 0) return '...';
288
353
 
289
- // Бинарный поиск оптимальной длины текста
290
354
  let left = 0;
291
355
  let right = text.length;
292
356
  let result = '';
293
-
294
357
  while (left <= right) {
295
358
  const mid = Math.floor((left + right) / 2);
296
359
  const subText = text.substring(0, mid);
297
- const subTextMetrics = PIXI.TextMetrics.measureText(subText, style);
298
- const scaledSubTextWidth = subTextMetrics.width * compensationScale;
299
-
300
- if (scaledSubTextWidth <= textAvailableWidth) {
360
+ const subMetrics = PIXI.TextMetrics.measureText(subText, style);
361
+ if (subMetrics.width <= textAvailableWidth) {
301
362
  result = subText;
302
363
  left = mid + 1;
303
364
  } else {
304
365
  right = mid - 1;
305
366
  }
306
367
  }
307
-
308
368
  return result + '...';
309
369
  }
310
370