@sequent-org/moodboard 1.4.32 → 1.4.34

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 (137) 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/ai/ChatSessionController.js +14 -8
  58. package/src/services/comments/CommentService.js +344 -0
  59. package/src/tools/object-tools/CommentTool.js +85 -0
  60. package/src/tools/object-tools/DrawingTool.js +110 -10
  61. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  62. package/src/tools/object-tools/SelectTool.js +25 -1
  63. package/src/tools/object-tools/TextTool.js +6 -1
  64. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  65. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  66. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  67. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  68. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  69. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  70. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  71. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  72. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  73. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  74. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  75. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  76. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  77. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  78. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  79. package/src/ui/CommentPopover.js +6 -0
  80. package/src/ui/CommentsBar.js +91 -0
  81. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  82. package/src/ui/ContextMenu.js +25 -0
  83. package/src/ui/DrawingPropertiesPanel.js +362 -0
  84. package/src/ui/FilePropertiesPanel.js +5 -0
  85. package/src/ui/FramePropertiesPanel.js +5 -0
  86. package/src/ui/HtmlTextLayer.js +246 -66
  87. package/src/ui/NotePropertiesPanel.js +6 -0
  88. package/src/ui/ShapePropertiesPanel.js +307 -0
  89. package/src/ui/TextPropertiesPanel.js +100 -1
  90. package/src/ui/Toolbar.js +25 -2
  91. package/src/ui/Topbar.js +2 -2
  92. package/src/ui/animation/HoverLiftController.js +6 -7
  93. package/src/ui/chat/ChatComposer.js +63 -9
  94. package/src/ui/chat/ChatWindow.js +329 -166
  95. package/src/ui/comments/CommentListPanel.js +213 -0
  96. package/src/ui/comments/CommentPinLayer.js +448 -0
  97. package/src/ui/comments/CommentThreadPopover.js +539 -0
  98. package/src/ui/comments/commentFormat.js +32 -0
  99. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  100. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  103. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  104. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  105. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  106. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  107. package/src/ui/connectors/ConnectorLayer.js +264 -57
  108. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  109. package/src/ui/handles/HandlesEventBridge.js +1 -0
  110. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  111. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  112. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  113. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  114. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  115. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  116. package/src/ui/styles/chat.css +710 -18
  117. package/src/ui/styles/index.css +1 -0
  118. package/src/ui/styles/panels.css +112 -2
  119. package/src/ui/styles/shape-properties-panel.css +250 -0
  120. package/src/ui/styles/toolbar.css +7 -2
  121. package/src/ui/styles/topbar.css +1 -1
  122. package/src/ui/styles/workspace.css +257 -6
  123. package/src/ui/text-properties/TextFormatControls.js +88 -0
  124. package/src/ui/text-properties/TextListRenderer.js +137 -0
  125. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  126. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  127. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  128. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  129. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  130. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  131. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  132. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  133. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  134. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  135. package/src/utils/iconLoader.js +17 -16
  136. package/src/utils/markdown.js +14 -0
  137. package/src/utils/richText.js +125 -0
@@ -123,6 +123,7 @@ export class PlacementInputRouter {
123
123
 
124
124
  let props = host.pending.properties || {};
125
125
  const isTextWithEditing = host.pending.type === 'text' && props.editOnCreate;
126
+ const isShapeType = host.pending.type === 'shape';
126
127
  const isImage = host.pending.type === 'image';
127
128
  const isFile = host.pending.type === 'file';
128
129
  const presetSize = {
@@ -250,12 +251,32 @@ export class PlacementInputRouter {
250
251
  y: Math.round(worldPoint.y - side / 2)
251
252
  };
252
253
  }
254
+ if (isShapeType) {
255
+ const handleShapeCreated = (objectData) => {
256
+ if (objectData.type === 'shape') {
257
+ host.eventBus.off(Events.Object.Created, handleShapeCreated);
258
+ host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
259
+ setTimeout(() => {
260
+ host.eventBus.emit(Events.Tool.ObjectEdit, {
261
+ object: {
262
+ id: objectData.id,
263
+ type: 'shape',
264
+ position: objectData.position,
265
+ properties: { content: '' }
266
+ },
267
+ create: true
268
+ });
269
+ }, 50);
270
+ }
271
+ };
272
+ host.eventBus.on(Events.Object.Created, handleShapeCreated);
273
+ }
253
274
  host.payloadFactory.emitGenericPlacement(host.pending.type, position, props);
254
275
  }
255
276
 
256
277
  host.pending = null;
257
278
  host.hideGhost();
258
- if (!isTextWithEditing && !(isFile && props.selectFileOnPlace)) {
279
+ if (!isTextWithEditing && !isShapeType && !(isFile && props.selectFileOnPlace)) {
259
280
  host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
260
281
  }
261
282
  }
@@ -1,8 +1,24 @@
1
1
  import * as PIXI from 'pixi.js';
2
2
 
3
+ import { Events } from '../../../core/events/Events.js';
4
+
3
5
  /**
4
- * BoxSelectController — управление рамкой выделения и выбором по пересечению
6
+ * BoxSelectController — управление рамкой выделения и выбором по пересечению.
7
+ *
8
+ * Помимо обычного `setSelection` по intersection, на завершение жеста (`end`)
9
+ * эмитит `Tool.BoxSelectCommit` с дополнительным набором `objects` — id объектов,
10
+ * чьи bbox целиком содержатся в финальном rect (strict contains). Этот канал
11
+ * нужен подписчикам, которым важно отличать «попали целиком» от «зацепили краем»
12
+ * (например, добавление reference-картинок в чат-композер).
5
13
  */
14
+ function isRectContainedInRect(outer, inner) {
15
+ if (!outer || !inner) return false;
16
+ return inner.x >= outer.x
17
+ && inner.y >= outer.y
18
+ && inner.x + inner.width <= outer.x + outer.width
19
+ && inner.y + inner.height <= outer.y + outer.height;
20
+ }
21
+
6
22
  export class BoxSelectController {
7
23
  constructor({ app, selection, emit, setSelection, clearSelection, rectIntersectsRect }) {
8
24
  this.app = app;
@@ -32,6 +48,7 @@ export class BoxSelectController {
32
48
  this.selectionGraphics.name = 'selection-box';
33
49
  this.app.stage.addChild(this.selectionGraphics);
34
50
  }
51
+ this.emit(Events.Tool.BoxSelectStart, {});
35
52
  }
36
53
 
37
54
  update(mouse) {
@@ -78,13 +95,17 @@ export class BoxSelectController {
78
95
  const y = Math.min(this.selectionBox.startY, this.selectionBox.endY);
79
96
  const w = Math.abs(this.selectionBox.endX - this.selectionBox.startX);
80
97
  const h = Math.abs(this.selectionBox.endY - this.selectionBox.startY);
98
+ let commitBox = null;
99
+ let containedIds = [];
100
+ let matched = [];
81
101
  if (w >= 2 && h >= 2) {
82
102
  const box = { x, y, width: w, height: h };
103
+ commitBox = box;
83
104
  const request = { objects: [] };
84
105
  this.emit('get:all:objects', request);
85
- const matched = [];
86
106
  for (const item of request.objects) {
87
107
  if (this.rectIntersectsRect(box, item.bounds)) matched.push(item.id);
108
+ if (isRectContainedInRect(box, item.bounds)) containedIds.push(item.id);
88
109
  }
89
110
  if (matched.length > 0) {
90
111
  if (this.isMultiSelect) {
@@ -94,6 +115,7 @@ export class BoxSelectController {
94
115
  }
95
116
  }
96
117
  }
118
+ this.emit(Events.Tool.BoxSelectCommit, { box: commitBox, objects: containedIds, selected: matched });
97
119
  this.isActive = false;
98
120
  this.selectionBox = null;
99
121
  if (this.selectionGraphics && this.selectionGraphics.parent) this.selectionGraphics.parent.removeChild(this.selectionGraphics);
@@ -0,0 +1,139 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../../core/events/Events.js';
3
+ import {
4
+ createFrameTitleEditorWrapper,
5
+ createFrameTitleEditorInput,
6
+ } from './InlineEditorDomFactory.js';
7
+ import { toScreenWithContainerOffset } from './InlineEditorPositioningService.js';
8
+
9
+ export function openFrameTitleEditor(object, _create = false) {
10
+ let objectId, properties;
11
+
12
+ const objData = object.object || object;
13
+ objectId = objData.id || object.id || null;
14
+ properties = objData.properties || object.properties || {};
15
+
16
+ const currentTitle = properties.title || 'Фрейм';
17
+
18
+ if (this.textEditor.active) {
19
+ if (this.textEditor.objectType === 'frame') {
20
+ this._closeFrameTitleEditor(true);
21
+ } else if (this.textEditor.objectType === 'file') {
22
+ this._closeFileNameEditor(true);
23
+ } else {
24
+ this._closeTextEditor(true);
25
+ }
26
+ }
27
+
28
+ if (!objectId) return;
29
+
30
+ const pixiReq = { objectId, pixiObject: null };
31
+ this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
32
+
33
+ let frameInstance = null;
34
+ if (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance) {
35
+ frameInstance = pixiReq.pixiObject._mb.instance;
36
+ }
37
+
38
+ if (!frameInstance || typeof frameInstance.hideTitle !== 'function') return;
39
+
40
+ frameInstance.hideTitle();
41
+
42
+ const view = this.app?.view || document.querySelector('canvas');
43
+ if (!view || !view.parentElement) {
44
+ frameInstance.showTitle();
45
+ return;
46
+ }
47
+
48
+ const titleLayer = frameInstance.titleLayer;
49
+ const screenPos = titleLayer
50
+ ? toScreenWithContainerOffset(titleLayer, view, 0, 0)
51
+ : { x: 0, y: 0 };
52
+
53
+ // Ширина редактора = ширина titleBg в экранных пикселях (с учётом зум-компенсации)
54
+ let inputWidth = 150;
55
+ if (titleLayer && frameInstance.titleBg) {
56
+ const bgRight = toScreenWithContainerOffset(titleLayer, view, frameInstance.titleBg.width || 150, 0);
57
+ inputWidth = Math.max(100, Math.round(bgRight.x - screenPos.x));
58
+ }
59
+
60
+ const wrapper = createFrameTitleEditorWrapper();
61
+ const input = createFrameTitleEditorInput(currentTitle);
62
+ wrapper.style.width = `${inputWidth}px`;
63
+
64
+ wrapper.appendChild(input);
65
+ view.parentElement.appendChild(wrapper);
66
+
67
+ wrapper.style.left = `${Math.round(screenPos.x)}px`;
68
+ wrapper.style.top = `${Math.round(screenPos.y)}px`;
69
+
70
+ this.textEditor = {
71
+ active: true,
72
+ objectId,
73
+ textarea: input,
74
+ wrapper,
75
+ position: null,
76
+ properties,
77
+ objectType: 'frame',
78
+ isResizing: false,
79
+ closing: false,
80
+ _frameInstance: frameInstance,
81
+ };
82
+
83
+ input.focus();
84
+ input.select();
85
+
86
+ const finalize = (commit) => this._closeFrameTitleEditor(commit);
87
+
88
+ input.addEventListener('blur', () => finalize(true));
89
+ input.addEventListener('keydown', (e) => {
90
+ if (e.key === 'Enter') {
91
+ e.preventDefault();
92
+ finalize(true);
93
+ } else if (e.key === 'Escape') {
94
+ e.preventDefault();
95
+ finalize(false);
96
+ }
97
+ });
98
+ }
99
+
100
+ export function closeFrameTitleEditor(commit) {
101
+ if (!this.textEditor || !this.textEditor.textarea || this.textEditor.closing) return;
102
+
103
+ this.textEditor.closing = true;
104
+
105
+ const input = this.textEditor.textarea;
106
+ const value = input.value.trim();
107
+ const commitValue = commit && value.length > 0;
108
+ const objectId = this.textEditor.objectId;
109
+ const oldTitle = (this.textEditor.properties?.title) || 'Фрейм';
110
+ const frameInstance = this.textEditor._frameInstance;
111
+
112
+ if (this.textEditor.wrapper && this.textEditor.wrapper.parentNode) {
113
+ this.textEditor.wrapper.remove();
114
+ }
115
+
116
+ if (frameInstance && typeof frameInstance.showTitle === 'function') {
117
+ frameInstance.showTitle();
118
+ }
119
+
120
+ if (commitValue && objectId && value !== oldTitle) {
121
+ this.eventBus.emit(Events.Object.StateChanged, {
122
+ objectId,
123
+ updates: { properties: { title: value } },
124
+ });
125
+ }
126
+
127
+ this.textEditor = {
128
+ active: false,
129
+ objectId: null,
130
+ textarea: null,
131
+ wrapper: null,
132
+ world: null,
133
+ position: null,
134
+ properties: null,
135
+ objectType: 'text',
136
+ isResizing: false,
137
+ closing: false,
138
+ };
139
+ }
@@ -10,6 +10,10 @@ import {
10
10
  closeMindmapEditor as closeMindmapEditorViaController,
11
11
  openMindmapEditor as openMindmapEditorViaController,
12
12
  } from './MindmapInlineEditorController.js';
13
+ import {
14
+ closeFrameTitleEditor as closeFrameTitleEditorViaController,
15
+ openFrameTitleEditor as openFrameTitleEditorViaController,
16
+ } from './FrameTitleInlineEditorController.js';
13
17
 
14
18
  export function openTextEditor(object, create = false) {
15
19
  return openTextEditorViaController.call(this, object, create);
@@ -37,3 +41,11 @@ export function closeTextEditor(commit) {
37
41
  export function closeMindmapEditor(commit) {
38
42
  return closeMindmapEditorViaController.call(this, commit);
39
43
  }
44
+
45
+ export function openFrameTitleEditor(object, create = false) {
46
+ return openFrameTitleEditorViaController.call(this, object, create);
47
+ }
48
+
49
+ export function closeFrameTitleEditor(commit) {
50
+ return closeFrameTitleEditorViaController.call(this, commit);
51
+ }
@@ -30,6 +30,42 @@ export function createFileNameEditorWrapper() {
30
30
  return wrapper;
31
31
  }
32
32
 
33
+ export function createFrameTitleEditorWrapper() {
34
+ const wrapper = document.createElement('div');
35
+ wrapper.className = 'moodboard-frame-title-editor';
36
+ wrapper.style.cssText = `
37
+ position: absolute;
38
+ z-index: 1000;
39
+ background: white;
40
+ border: 2px solid #2563eb;
41
+ border-radius: 6px;
42
+ padding: 3px 6px;
43
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
44
+ min-width: 100px;
45
+ max-width: 320px;
46
+ font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
47
+ `;
48
+ return wrapper;
49
+ }
50
+
51
+ export function createFrameTitleEditorInput(title) {
52
+ const input = document.createElement('input');
53
+ input.type = 'text';
54
+ input.value = title;
55
+ input.style.cssText = `
56
+ border: none;
57
+ outline: none;
58
+ background: transparent;
59
+ font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
60
+ font-size: 14px;
61
+ font-weight: 500;
62
+ width: 100%;
63
+ padding: 1px 2px;
64
+ color: #1f2937;
65
+ `;
66
+ return input;
67
+ }
68
+
33
69
  export function createFileNameEditorInput(fileName) {
34
70
  const input = document.createElement('input');
35
71
  input.type = 'text';
@@ -0,0 +1,125 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * Проверяет, находится ли точка внутри полигона (алгоритм ray casting).
5
+ */
6
+ function pointInPolygon(point, polygon) {
7
+ let inside = false;
8
+ const n = polygon.length;
9
+ for (let i = 0, j = n - 1; i < n; j = i++) {
10
+ const xi = polygon[i].x, yi = polygon[i].y;
11
+ const xj = polygon[j].x, yj = polygon[j].y;
12
+ if ((yi > point.y) !== (yj > point.y) &&
13
+ point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi) {
14
+ inside = !inside;
15
+ }
16
+ }
17
+ return inside;
18
+ }
19
+
20
+ /**
21
+ * Проверяет, пересекается ли полигон лассо с прямоугольником объекта (bounds).
22
+ * Достаточно, что хотя бы один угол прямоугольника — внутри полигона.
23
+ */
24
+ function lassoIntersectsRect(polygon, rect) {
25
+ if (polygon.length < 3) return false;
26
+ const corners = [
27
+ { x: rect.x, y: rect.y },
28
+ { x: rect.x + rect.width, y: rect.y },
29
+ { x: rect.x + rect.width, y: rect.y + rect.height },
30
+ { x: rect.x, y: rect.y + rect.height },
31
+ { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }
32
+ ];
33
+ return corners.some((c) => pointInPolygon(c, polygon));
34
+ }
35
+
36
+ /**
37
+ * LassoSelectController — выделение объектов произвольным контуром.
38
+ * Аналог BoxSelectController, но с polygon hit-test вместо AABB.
39
+ */
40
+ export class LassoSelectController {
41
+ constructor({ app, selection, emit, setSelection, clearSelection }) {
42
+ this.app = app;
43
+ this.selection = selection;
44
+ this.emit = emit;
45
+ this.setSelection = setSelection;
46
+ this.clearSelection = clearSelection;
47
+
48
+ this.isActive = false;
49
+ this.points = [];
50
+ this.lassoGraphics = null;
51
+ this.initialSelection = null;
52
+ this.isMultiSelect = false;
53
+ }
54
+
55
+ start(mouse, isMultiSelect) {
56
+ this.isActive = true;
57
+ this.isMultiSelect = !!isMultiSelect;
58
+ this.points = [{ x: mouse.x, y: mouse.y }];
59
+ this.initialSelection = this.selection.toArray();
60
+ if (!this.isMultiSelect) this.clearSelection();
61
+
62
+ if (this.app && this.app.stage) {
63
+ this.app.stage.sortableChildren = true;
64
+ this.lassoGraphics = new PIXI.Graphics();
65
+ this.lassoGraphics.zIndex = 2000;
66
+ this.lassoGraphics.name = 'lasso-select';
67
+ this.app.stage.addChild(this.lassoGraphics);
68
+ }
69
+ }
70
+
71
+ update(mouse) {
72
+ if (!this.isActive) return;
73
+ this.points.push({ x: mouse.x, y: mouse.y });
74
+ this._redraw();
75
+ this._updateSelection();
76
+ }
77
+
78
+ end() {
79
+ if (!this.isActive) return;
80
+ this._updateSelection();
81
+ this.isActive = false;
82
+ this.points = [];
83
+ this._destroyGraphics();
84
+ }
85
+
86
+ _updateSelection() {
87
+ if (this.points.length < 3) return;
88
+ const request = { objects: [] };
89
+ this.emit('get:all:objects', request);
90
+ const matched = [];
91
+ for (const item of request.objects) {
92
+ if (lassoIntersectsRect(this.points, item.bounds)) matched.push(item.id);
93
+ }
94
+ let newSelection;
95
+ if (this.isMultiSelect && this.initialSelection) {
96
+ const base = new Set(this.initialSelection);
97
+ for (const id of matched) base.add(id);
98
+ newSelection = Array.from(base);
99
+ } else {
100
+ newSelection = matched;
101
+ }
102
+ this.setSelection(newSelection);
103
+ }
104
+
105
+ _redraw() {
106
+ if (!this.lassoGraphics || this.points.length < 2) return;
107
+ this.lassoGraphics.clear();
108
+ this.lassoGraphics.lineStyle(2, 0x3b82f6, 0.9);
109
+ this.lassoGraphics.beginFill(0x3b82f6, 0.08);
110
+ this.lassoGraphics.moveTo(this.points[0].x, this.points[0].y);
111
+ for (let i = 1; i < this.points.length; i++) {
112
+ this.lassoGraphics.lineTo(this.points[i].x, this.points[i].y);
113
+ }
114
+ this.lassoGraphics.closePath();
115
+ this.lassoGraphics.endFill();
116
+ }
117
+
118
+ _destroyGraphics() {
119
+ if (this.lassoGraphics) {
120
+ if (this.lassoGraphics.parent) this.lassoGraphics.parent.removeChild(this.lassoGraphics);
121
+ this.lassoGraphics.destroy();
122
+ this.lassoGraphics = null;
123
+ }
124
+ }
125
+ }
@@ -600,6 +600,7 @@ export function openMindmapEditor(object, create = false) {
600
600
  [Events.Tool.GroupRotateUpdate, onGroupSync],
601
601
  [Events.UI.ZoomPercent, () => syncEditorBoundsToObject()],
602
602
  [Events.Tool.PanUpdate, () => syncEditorBoundsToObject()],
603
+ [Events.Viewport.Changed, () => syncEditorBoundsToObject()],
603
604
  ]);
604
605
 
605
606
  const initialContent = String(properties.content || '');
@@ -1,6 +1,17 @@
1
1
  import { Events } from '../../../core/events/Events.js';
2
2
 
3
3
  const DRAG_START_THRESHOLD_PX = 4;
4
+ const TEXT_EDITOR_STYLE_KEYS = ['fontFamily', 'fontSize', 'color', 'backgroundColor', 'markdown', 'bold', 'italic', 'underline', 'strikethrough', 'textAlign', 'lineHeight', 'listType', 'listChecked'];
5
+
6
+ function pickTextEditorProperties(properties = {}) {
7
+ const picked = {};
8
+ TEXT_EDITOR_STYLE_KEYS.forEach((key) => {
9
+ if (properties[key] !== undefined) {
10
+ picked[key] = properties[key];
11
+ }
12
+ });
13
+ return picked;
14
+ }
4
15
 
5
16
  export function onMouseDown(event) {
6
17
  // Если активен текстовый редактор, закрываем его при клике вне
@@ -124,8 +135,12 @@ export function onMouseDown(event) {
124
135
  return;
125
136
  }
126
137
  }
127
- // Иначе — начинаем рамку выделения
128
- this.startBoxSelect(event);
138
+ // Иначе — начинаем рамку выделения (или лассо в режиме лассо)
139
+ if (this.lassoMode) {
140
+ this.startLassoSelect(event);
141
+ } else {
142
+ this.startBoxSelect(event);
143
+ }
129
144
  }
130
145
  }
131
146
 
@@ -158,6 +173,8 @@ export function onMouseMove(event) {
158
173
  }
159
174
  } else if (this.isDragging || this.isGroupDragging) {
160
175
  this.updateDrag(event);
176
+ } else if (this.isLassoSelect) {
177
+ this.updateLassoSelect(event);
161
178
  } else if (this.isBoxSelect) {
162
179
  this.updateBoxSelect(event);
163
180
  } else {
@@ -186,6 +203,8 @@ export function onMouseUp(event) {
186
203
  this.endRotate();
187
204
  } else if (this.isDragging || this.isGroupDragging) {
188
205
  this.endDrag();
206
+ } else if (this.isLassoSelect) {
207
+ this.endLassoSelect();
189
208
  } else if (this.isBoxSelect) {
190
209
  this.endBoxSelect();
191
210
  }
@@ -203,20 +222,28 @@ export function onDoubleClick(event) {
203
222
  const isText = !!(pix && pix._mb && pix._mb.type === 'text');
204
223
  const isNote = !!(pix && pix._mb && pix._mb.type === 'note');
205
224
  const isFile = !!(pix && pix._mb && pix._mb.type === 'file');
225
+ const isShape = !!(pix && pix._mb && pix._mb.type === 'shape');
226
+ const isFrame = !!(pix && pix._mb && pix._mb.type === 'frame');
206
227
 
207
228
  if (isText) {
208
229
  // Получаем позицию объекта для редактирования
209
230
  const posData = { objectId: hitResult.object, position: null };
210
231
  this.emit(Events.Tool.GetObjectPosition, posData);
211
232
 
212
- // Получаем содержимое из properties объекта
213
- const textContent = pix._mb?.properties?.content || '';
233
+ // Передаём в inline-editor не только контент, но и текущие текстовые стили:
234
+ // иначе теряются listType/textAlign/lineHeight и Enter/визуальное совпадение
235
+ // в режиме редактирования расходятся со статичным рендером.
236
+ const textProperties = pix._mb?.properties || {};
237
+ const textContent = textProperties.content || '';
214
238
 
215
239
  this.emit(Events.Tool.ObjectEdit, {
216
240
  id: hitResult.object,
217
241
  type: 'text',
218
242
  position: posData.position,
219
- properties: { content: textContent },
243
+ properties: {
244
+ ...pickTextEditorProperties(textProperties),
245
+ content: textContent,
246
+ },
220
247
  caretClick: {
221
248
  clientX: event?.originalEvent?.clientX,
222
249
  clientY: event?.originalEvent?.clientY
@@ -256,6 +283,36 @@ export function onDoubleClick(event) {
256
283
  });
257
284
  return;
258
285
  }
286
+
287
+ if (isShape) {
288
+ const posData = { objectId: hitResult.object, position: null };
289
+ this.emit(Events.Tool.GetObjectPosition, posData);
290
+ const shapeContent = pix._mb?.properties?.content || '';
291
+ this.emit(Events.Tool.ObjectEdit, {
292
+ id: hitResult.object,
293
+ type: 'shape',
294
+ position: posData.position,
295
+ properties: { content: shapeContent },
296
+ caretClick: {
297
+ clientX: event?.originalEvent?.clientX,
298
+ clientY: event?.originalEvent?.clientY,
299
+ },
300
+ create: false,
301
+ });
302
+ return;
303
+ }
304
+
305
+ if (isFrame) {
306
+ const frameProps = pix._mb?.properties || {};
307
+ this.emit(Events.Tool.ObjectEdit, {
308
+ id: hitResult.object,
309
+ type: 'frame',
310
+ properties: { title: frameProps.title || 'Фрейм' },
311
+ create: false,
312
+ });
313
+ return;
314
+ }
315
+
259
316
  this.editObject(hitResult.object);
260
317
  }
261
318
  }
@@ -285,6 +342,8 @@ export function onKeyDown(event) {
285
342
  if (event.key === 'Escape') {
286
343
  if (this.textEditor.objectType === 'file') {
287
344
  this._closeFileNameEditor(false);
345
+ } else if (this.textEditor.objectType === 'frame') {
346
+ this._closeFrameTitleEditor(false);
288
347
  } else {
289
348
  this._closeTextEditor(false);
290
349
  }
@@ -8,6 +8,7 @@ import { GroupResizeController } from './GroupResizeController.js';
8
8
  import { GroupRotateController } from './GroupRotateController.js';
9
9
  import { GroupDragController } from './GroupDragController.js';
10
10
  import { BoxSelectController } from './BoxSelectController.js';
11
+ import { LassoSelectController } from './LassoSelectController.js';
11
12
 
12
13
  export function activateSelectTool(app, defaultCursor, superActivate) {
13
14
  superActivate();
@@ -67,6 +68,13 @@ export function activateSelectTool(app, defaultCursor, superActivate) {
67
68
  clearSelection: () => this.clearSelection(),
68
69
  rectIntersectsRect: (a, b) => this.rectIntersectsRect(a, b)
69
70
  });
71
+ this._lassoSelect = new LassoSelectController({
72
+ app,
73
+ selection: this.selection,
74
+ emit: (event, payload) => this.emit(event, payload),
75
+ setSelection: (ids) => this.setSelection(ids),
76
+ clearSelection: () => this.clearSelection()
77
+ });
70
78
  } else if (!app) {
71
79
  console.log('❌ PIXI app не передан в activate');
72
80
  } else {
@@ -76,10 +84,12 @@ export function activateSelectTool(app, defaultCursor, superActivate) {
76
84
  export function deactivateSelectTool(superDeactivate) {
77
85
  superDeactivate();
78
86
 
79
- // Закрываем текстовый/файловый редактор если открыт
87
+ // Закрываем текстовый/файловый/frame редактор если открыт
80
88
  if (this.textEditor.active) {
81
89
  if (this.textEditor.objectType === 'file') {
82
90
  this._closeFileNameEditor(true);
91
+ } else if (this.textEditor.objectType === 'frame') {
92
+ this._closeFrameTitleEditor(true);
83
93
  } else {
84
94
  this._closeTextEditor(true);
85
95
  }
@@ -64,6 +64,10 @@ export function initializeSelectToolState(instance) {
64
64
  instance.selectionGraphics = null; // PIXI.Graphics для визуализации рамки
65
65
  instance.initialSelectionBeforeBox = null; // снимок выделения перед началом box-select
66
66
 
67
+ // Режим лассо-выделения (произвольный контур вместо прямоугольника)
68
+ instance.lassoMode = false;
69
+ instance.isLassoSelect = false;
70
+
67
71
  instance.textEditor = {
68
72
  active: false,
69
73
  objectId: null,
@@ -96,6 +100,8 @@ export function registerSelectToolCoreSubscriptions(instance) {
96
100
  const objectType = object.type || (object.object && object.object.type) || 'text';
97
101
  if (objectType === 'file') {
98
102
  instance._openFileNameEditor(object, object.create || false);
103
+ } else if (objectType === 'frame') {
104
+ instance._openFrameTitleEditor(object, object.create || false);
99
105
  } else if (objectType === 'mindmap') {
100
106
  instance._openMindmapEditor(object, object.create || false);
101
107
  } else {
@@ -121,11 +127,16 @@ export function registerSelectToolCoreSubscriptions(instance) {
121
127
  }
122
128
  };
123
129
 
124
- instance._coreHandlers = { onDuplicateReady, onGroupDuplicateReady, onObjectEdit, onObjectDeleted };
130
+ const onLassoModeSet = (data) => {
131
+ instance.lassoMode = !!(data && data.active);
132
+ };
133
+
134
+ instance._coreHandlers = { onDuplicateReady, onGroupDuplicateReady, onObjectEdit, onObjectDeleted, onLassoModeSet };
125
135
  instance.eventBus.on(Events.Tool.DuplicateReady, onDuplicateReady);
126
136
  instance.eventBus.on(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
127
137
  instance.eventBus.on(Events.Tool.ObjectEdit, onObjectEdit);
128
138
  instance.eventBus.on(Events.Object.Deleted, onObjectDeleted);
139
+ instance.eventBus.on(Events.Lasso.ModeSet, onLassoModeSet);
129
140
  }
130
141
 
131
142
  export function unregisterSelectToolCoreSubscriptions(instance) {
@@ -135,5 +146,6 @@ export function unregisterSelectToolCoreSubscriptions(instance) {
135
146
  instance.eventBus.off(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
136
147
  instance.eventBus.off(Events.Tool.ObjectEdit, onObjectEdit);
137
148
  instance.eventBus.off(Events.Object.Deleted, onObjectDeleted);
149
+ instance.eventBus.off(Events.Lasso.ModeSet, onLassoModeSet);
138
150
  instance._coreHandlers = null;
139
151
  }