@sequent-org/moodboard 1.4.29 → 1.4.31

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 (59) hide show
  1. package/package.json +3 -1
  2. package/src/core/PixiEngine.js +34 -5
  3. package/src/core/bootstrap/CoreInitializer.js +4 -0
  4. package/src/core/commands/CreateConnectorCommand.js +25 -0
  5. package/src/core/commands/GroupMoveCommand.js +2 -2
  6. package/src/core/commands/MoveObjectCommand.js +1 -1
  7. package/src/core/commands/UpdateConnectorCommand.js +38 -0
  8. package/src/core/events/Events.js +1 -0
  9. package/src/mindmap/MindmapCompoundContract.js +1 -0
  10. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +14 -0
  11. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +18 -0
  12. package/src/objects/ConnectorObject.js +85 -0
  13. package/src/objects/DrawingObject.js +47 -0
  14. package/src/objects/MindmapObject.js +21 -3
  15. package/src/objects/NoteObject.js +16 -8
  16. package/src/objects/ObjectFactory.js +3 -1
  17. package/src/objects/ShapeObject.js +1 -1
  18. package/src/services/ConnectorBindingResolver.js +204 -0
  19. package/src/services/ai/AiClient.js +30 -2
  20. package/src/services/ai/ChatSessionController.js +1 -0
  21. package/src/tools/ToolManager.js +3 -0
  22. package/src/tools/manager/PointerGestureController.js +206 -0
  23. package/src/tools/manager/ToolEventRouter.js +10 -0
  24. package/src/tools/manager/ToolManagerGuards.js +3 -1
  25. package/src/tools/manager/ToolManagerLifecycle.js +70 -58
  26. package/src/tools/object-tools/ConnectorTool.js +147 -0
  27. package/src/tools/object-tools/PlacementTool.js +2 -2
  28. package/src/tools/object-tools/connector/ConnectorDragController.js +296 -0
  29. package/src/tools/object-tools/connector/connectorGesture.js +108 -0
  30. package/src/tools/object-tools/placement/GhostController.js +4 -4
  31. package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -2
  32. package/src/tools/object-tools/placement/PlacementInputRouter.js +5 -5
  33. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +11 -2
  34. package/src/tools/object-tools/selection/SelectInputRouter.js +33 -4
  35. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +12 -0
  36. package/src/tools/object-tools/selection/SelectToolSetup.js +3 -0
  37. package/src/tools/object-tools/selection/TextEditorDomFactory.js +1 -2
  38. package/src/tools/object-tools/selection/TextEditorSyncService.js +4 -4
  39. package/src/tools/object-tools/selection/TextInlineEditorController.js +21 -3
  40. package/src/tools/object-tools/selection/TransformInteractionController.js +4 -6
  41. package/src/ui/HtmlTextLayer.js +212 -5
  42. package/src/ui/animation/HoverLiftController.js +395 -0
  43. package/src/ui/chat/ChatComposer.js +7 -5
  44. package/src/ui/chat/ChatWindow.js +652 -112
  45. package/src/ui/chat/icons.js +17 -1
  46. package/src/ui/connectors/ConnectionAnchorsLayer.js +231 -0
  47. package/src/ui/connectors/ConnectorLayer.js +251 -0
  48. package/src/ui/handles/HandlesDomRenderer.js +11 -7
  49. package/src/ui/handles/HandlesInteractionController.js +65 -34
  50. package/src/ui/handles/HandlesPositioningService.js +41 -6
  51. package/src/ui/mindmap/MindmapCollapseGraph.js +169 -0
  52. package/src/ui/mindmap/MindmapCollapseLayer.js +380 -0
  53. package/src/ui/mindmap/MindmapConnectionLayer.js +50 -25
  54. package/src/ui/mindmap/MindmapHtmlTextLayer.js +223 -2
  55. package/src/ui/mindmap/MindmapLayoutConfig.js +12 -0
  56. package/src/ui/styles/chat.css +40 -3
  57. package/src/ui/styles/toolbar.css +6 -0
  58. package/src/ui/styles/workspace.css +83 -21
  59. package/src/ui/toolbar/ToolbarPopupsController.js +1 -1
@@ -0,0 +1,108 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../../core/events/Events.js';
3
+
4
+ /**
5
+ * Переиспользуемые хелперы жеста коннектора.
6
+ * Все функции принимают eventBus и данные явными аргументами — без this.
7
+ */
8
+
9
+ /**
10
+ * Возвращает world-точку терминала.
11
+ * Свободный терминал: terminal.point напрямую.
12
+ * Привязанный: top-left объекта + anchor * size (CONNECTORS.md раздел 3).
13
+ */
14
+ export function terminalWorldPoint(eventBus, terminal) {
15
+ if (!terminal) return { x: 0, y: 0 };
16
+ if (terminal.point) return terminal.point;
17
+
18
+ const posData = { objectId: terminal.boundId, position: null };
19
+ const sizeData = { objectId: terminal.boundId, size: null };
20
+ eventBus.emit(Events.Tool.GetObjectPosition, posData);
21
+ eventBus.emit(Events.Tool.GetObjectSize, sizeData);
22
+
23
+ const pos = posData.position;
24
+ const size = sizeData.size;
25
+ if (pos && size) {
26
+ return {
27
+ x: pos.x + (terminal.anchor?.x ?? 0.5) * (size.width || 0),
28
+ y: pos.y + (terminal.anchor?.y ?? 0.5) * (size.height || 0),
29
+ };
30
+ }
31
+ return { x: 0, y: 0 };
32
+ }
33
+
34
+ /**
35
+ * Нормализованный якорь по позиции клика внутри bbox объекта.
36
+ * Если объект не найден — возвращает центр { x:0.5, y:0.5 }.
37
+ */
38
+ export function computeAnchor(eventBus, objectId, worldPt) {
39
+ const posData = { objectId, position: null };
40
+ const sizeData = { objectId, size: null };
41
+ eventBus.emit(Events.Tool.GetObjectPosition, posData);
42
+ eventBus.emit(Events.Tool.GetObjectSize, sizeData);
43
+
44
+ const pos = posData.position;
45
+ const size = sizeData.size;
46
+ if (pos && size && size.width > 0 && size.height > 0) {
47
+ return {
48
+ x: Math.max(0, Math.min(1, (worldPt.x - pos.x) / size.width)),
49
+ y: Math.max(0, Math.min(1, (worldPt.y - pos.y) / size.height)),
50
+ };
51
+ }
52
+ return { x: 0.5, y: 0.5 };
53
+ }
54
+
55
+ /**
56
+ * Рисует превью линии со стрелкой в PIXI-графику (PIXI 7 API).
57
+ * graphics — PIXI.Graphics, уже добавленный в worldLayer.
58
+ */
59
+ export function drawPreview(graphics, fromWorldPt, toWorldPt) {
60
+ graphics.clear();
61
+
62
+ graphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.7, cap: 'round' });
63
+ graphics.moveTo(fromWorldPt.x, fromWorldPt.y);
64
+ graphics.lineTo(toWorldPt.x, toWorldPt.y);
65
+
66
+ const dx = toWorldPt.x - fromWorldPt.x;
67
+ const dy = toWorldPt.y - fromWorldPt.y;
68
+ const len = Math.hypot(dx, dy);
69
+ if (len > 10) {
70
+ const ux = dx / len;
71
+ const uy = dy / len;
72
+ const aLen = 10;
73
+ const aAng = 0.4;
74
+ graphics.beginFill(0x2563EB, 0.7);
75
+ graphics.drawPolygon([
76
+ toWorldPt.x, toWorldPt.y,
77
+ toWorldPt.x - aLen * (ux * Math.cos(aAng) - uy * Math.sin(aAng)),
78
+ toWorldPt.y - aLen * (uy * Math.cos(aAng) + ux * Math.sin(aAng)),
79
+ toWorldPt.x - aLen * (ux * Math.cos(-aAng) - uy * Math.sin(-aAng)),
80
+ toWorldPt.y - aLen * (uy * Math.cos(-aAng) + ux * Math.sin(-aAng)),
81
+ ]);
82
+ graphics.endFill();
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Создаёт объект коннектора через core.createObject с дефолтным стилем.
88
+ * position — top-left от min(startPt, endPt).
89
+ */
90
+ export function createConnectorFromTerminals(core, eventBus, sourceTerminal, endTerminal) {
91
+ const startPt = terminalWorldPoint(eventBus, sourceTerminal);
92
+ const endPt = terminalWorldPoint(eventBus, endTerminal);
93
+ const position = {
94
+ x: Math.min(startPt.x, endPt.x),
95
+ y: Math.min(startPt.y, endPt.y),
96
+ };
97
+ core.createObject('connector', position, {
98
+ start: sourceTerminal,
99
+ end: endTerminal,
100
+ style: {
101
+ stroke: 0x2563EB,
102
+ width: 2,
103
+ dash: false,
104
+ head: { start: false, end: true },
105
+ route: 'straight',
106
+ },
107
+ });
108
+ }
@@ -431,7 +431,7 @@ export class GhostController {
431
431
  const kind = host.pending.properties?.kind || 'square';
432
432
  const width = 100;
433
433
  const height = 100;
434
- const fillColor = 0x3b82f6;
434
+ const fillColor = 0xffffff;
435
435
  const cornerRadius = host.pending.properties?.cornerRadius || 10;
436
436
 
437
437
  const shapeGraphics = new PIXI.Graphics();
@@ -523,9 +523,9 @@ export class GhostController {
523
523
  const fillAlpha = (typeof host.pending.properties?.fillAlpha === 'number')
524
524
  ? host.pending.properties.fillAlpha
525
525
  : 0.25;
526
- const strokeWidth = (typeof host.pending.properties?.strokeWidth === 'number')
527
- ? host.pending.properties.strokeWidth
528
- : 1;
526
+ // Толщина обводки согласована с MindmapObject и ветками: 3 экранных пикселя.
527
+ const worldScale = Math.max(0.01, host.world?.scale?.x || 1);
528
+ const strokeWidth = 3 / worldScale;
529
529
  const fontSize = Math.max(1, Math.round(host.pending.properties?.fontSize || MINDMAP_LAYOUT.fontSize));
530
530
  const fontFamily = host.pending.properties?.fontFamily || 'Roboto, Arial, sans-serif';
531
531
  const textColor = host.pending.properties?.textColor || 0x212121;
@@ -37,7 +37,7 @@ export class PlacementEventsBridge {
37
37
  }
38
38
  if (this.host.pending.placeOnMouseUp && this.host.app && this.host.app.view) {
39
39
  const onUp = (ev) => {
40
- this.host.app.view.removeEventListener('mouseup', onUp);
40
+ this.host.app.view.removeEventListener('pointerup', onUp);
41
41
  if (!this.host.pending) return;
42
42
  const worldPoint = this.host._toWorld(ev.x, ev.y);
43
43
  const position = {
@@ -50,7 +50,7 @@ export class PlacementEventsBridge {
50
50
  this.host.hideGhost();
51
51
  this.host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
52
52
  };
53
- this.host.app.view.addEventListener('mouseup', onUp, { once: true });
53
+ this.host.app.view.addEventListener('pointerup', onUp, { once: true });
54
54
  }
55
55
  }
56
56
  });
@@ -61,7 +61,7 @@ export class PlacementInputRouter {
61
61
  host.pending = null;
62
62
  host.hideGhost();
63
63
  if (host.app && host.app.view) {
64
- host.app.view.removeEventListener('mousemove', host._onFrameDrawMoveBound);
64
+ host.app.view.removeEventListener('pointermove', host._onFrameDrawMoveBound);
65
65
  host.app.view.style.cursor = '';
66
66
  }
67
67
  host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
@@ -84,7 +84,7 @@ export class PlacementInputRouter {
84
84
  if (!host.pending) return;
85
85
  if (host.pending.placeOnMouseUp) {
86
86
  const onUp = (ev) => {
87
- host.app.view.removeEventListener('mouseup', onUp);
87
+ host.app.view.removeEventListener('pointerup', onUp);
88
88
  const worldPoint = host._toWorld(ev.x, ev.y);
89
89
  const position = {
90
90
  x: Math.round(worldPoint.x - (host.pending.size?.width ?? 100) / 2),
@@ -96,7 +96,7 @@ export class PlacementInputRouter {
96
96
  host.hideGhost();
97
97
  host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
98
98
  };
99
- host.app.view.addEventListener('mouseup', onUp, { once: true });
99
+ host.app.view.addEventListener('pointerup', onUp, { once: true });
100
100
  return;
101
101
  }
102
102
  if (host.pending.type === 'frame-draw') {
@@ -110,8 +110,8 @@ export class PlacementInputRouter {
110
110
  }
111
111
  host._onFrameDrawMoveBound = (ev) => host._onFrameDrawMove(ev);
112
112
  host._onFrameDrawUpBound = (ev) => host._onFrameDrawUp(ev);
113
- host.app.view.addEventListener('mousemove', host._onFrameDrawMoveBound);
114
- host.app.view.addEventListener('mouseup', host._onFrameDrawUpBound, { once: true });
113
+ host.app.view.addEventListener('pointermove', host._onFrameDrawMoveBound);
114
+ host.app.view.addEventListener('pointerup', host._onFrameDrawUpBound, { once: true });
115
115
  return;
116
116
  }
117
117
 
@@ -13,7 +13,7 @@ import {
13
13
  showStaticTextAfterEditing,
14
14
  updateGlobalTextEditorHandlesLayer,
15
15
  } from './TextEditorLifecycleRegistry.js';
16
- import { MINDMAP_LAYOUT } from '../../../ui/mindmap/MindmapLayoutConfig.js';
16
+ import { MINDMAP_LAYOUT, MINDMAP_AUTOFIT } from '../../../ui/mindmap/MindmapLayoutConfig.js';
17
17
 
18
18
  function applyMindmapCaretFromClick({ create, objectId, object, textarea }) {
19
19
  try {
@@ -417,9 +417,18 @@ export function openMindmapEditor(object, create = false) {
417
417
  const placeholderWidth = measureMindmapTextWidthPx(textarea, measureEl, textarea.placeholder || '');
418
418
  const baseCssWidth = Math.max(1, Math.round(stableBaseWorldWidth * getWorldToCssScale()));
419
419
  const placeholderCssWidth = Math.max(1, Math.ceil(placeholderWidth + padding.left + padding.right));
420
- const nextCssWidth = hasText
420
+ const rawNextCssWidth = hasText
421
421
  ? Math.max(1, Math.ceil(textWidth + padding.left + padding.right))
422
422
  : Math.max(baseCssWidth, placeholderCssWidth);
423
+ const level = properties?.mindmap?.level ?? 0;
424
+ const isRoot = level === 0;
425
+ const minCssW = Math.max(1, Math.round(
426
+ (isRoot ? MINDMAP_AUTOFIT.ROOT_MIN_WIDTH : MINDMAP_AUTOFIT.CHILD_MIN_WIDTH) * getWorldToCssScale()
427
+ ));
428
+ const maxCssW = Math.max(1, Math.round(
429
+ (isRoot ? MINDMAP_AUTOFIT.ROOT_MAX_WIDTH : MINDMAP_AUTOFIT.CHILD_MAX_WIDTH) * getWorldToCssScale()
430
+ ));
431
+ const nextCssWidth = Math.max(minCssW, Math.min(maxCssW, rawNextCssWidth));
423
432
  const lineCount = getEditorLineCount();
424
433
  const lineHeightPx = getEditorLineHeightPx();
425
434
  const baseCssHeight = Math.max(1, Math.round(stableBaseWorldHeight * getWorldToCssScale()));
@@ -1,5 +1,7 @@
1
1
  import { Events } from '../../../core/events/Events.js';
2
2
 
3
+ const DRAG_START_THRESHOLD_PX = 4;
4
+
3
5
  export function onMouseDown(event) {
4
6
  // Если активен текстовый редактор, закрываем его при клике вне
5
7
  if (this.textEditor.active) {
@@ -20,6 +22,7 @@ export function onMouseDown(event) {
20
22
  return; // Прерываем выполнение, чтобы не обрабатывать клик дальше
21
23
  }
22
24
 
25
+ this._pendingDrag = null;
23
26
  this.isMultiSelect = event.originalEvent.ctrlKey || event.originalEvent.metaKey || event.originalEvent.shiftKey;
24
27
 
25
28
  // Проверяем, что под курсором
@@ -34,8 +37,8 @@ export function onMouseDown(event) {
34
37
  const gb = this.computeGroupBounds();
35
38
  const insideGroup = this.isPointInBounds({ x: event.x, y: event.y }, { x: gb.x, y: gb.y, width: gb.width, height: gb.height });
36
39
  if (insideGroup) {
37
- // Если клик внутри группы (по объекту или пустому месту), сохраняем выделение и начинаем перетаскивание группы
38
- this.startGroupDrag(event);
40
+ // Если клик внутри группы откладываем до превышения порога смещения
41
+ this._pendingDrag = { isGroup: true, objectId: null, downX: event.x, downY: event.y, event };
39
42
  return;
40
43
  }
41
44
  // Вне группы — обычная логика
@@ -116,8 +119,8 @@ export function onMouseDown(event) {
116
119
  }
117
120
  }
118
121
  }
119
- // Старт перетаскивания как если бы кликнули по объекту
120
- this.startDrag(selId, event);
122
+ // Откладываем drag до превышения порога смещения
123
+ this._pendingDrag = { isGroup: false, objectId: selId, downX: event.x, downY: event.y, event };
121
124
  return;
122
125
  }
123
126
  }
@@ -140,6 +143,19 @@ export function onMouseMove(event) {
140
143
  this.updateResize(event);
141
144
  } else if (this.isRotating || this.isGroupRotating) {
142
145
  this.updateRotate(event);
146
+ } else if (this._pendingDrag) {
147
+ const dx = event.x - this._pendingDrag.downX;
148
+ const dy = event.y - this._pendingDrag.downY;
149
+ if (Math.hypot(dx, dy) > DRAG_START_THRESHOLD_PX) {
150
+ const pending = this._pendingDrag;
151
+ this._pendingDrag = null;
152
+ if (pending.isGroup) {
153
+ this.startGroupDrag(pending.event);
154
+ } else {
155
+ this.startDrag(pending.objectId, pending.event);
156
+ }
157
+ this.updateDrag(event);
158
+ }
143
159
  } else if (this.isDragging || this.isGroupDragging) {
144
160
  this.updateDrag(event);
145
161
  } else if (this.isBoxSelect) {
@@ -147,10 +163,23 @@ export function onMouseMove(event) {
147
163
  } else {
148
164
  // Обновляем курсор в зависимости от того, что под мышью
149
165
  this.updateCursor(event);
166
+
167
+ // Эмитим событие наведения на объект
168
+ const hitData = { x: event.x, y: event.y, result: null };
169
+ this.emit(Events.Tool.HitTest, hitData);
170
+ const hoveredObjectId = hitData.result?.object || null;
171
+ if (this.lastHoveredObjectId !== hoveredObjectId) {
172
+ this.lastHoveredObjectId = hoveredObjectId;
173
+ this.emit(Events.Object.Hover, { objectId: hoveredObjectId });
174
+ }
150
175
  }
151
176
  }
152
177
 
153
178
  export function onMouseUp(event) {
179
+ if (this._pendingDrag) {
180
+ this._pendingDrag = null;
181
+ return;
182
+ }
154
183
  if (this.isResizing || this.isGroupResizing) {
155
184
  this.endResize();
156
185
  } else if (this.isRotating || this.isGroupRotating) {
@@ -93,6 +93,12 @@ export function deactivateSelectTool(superDeactivate) {
93
93
  if (this.app && this.app.view) {
94
94
  this.app.view.style.cursor = '';
95
95
  }
96
+
97
+ // Эмитим Hover с null при выходе из инструмента
98
+ if (this.lastHoveredObjectId !== null) {
99
+ this.lastHoveredObjectId = null;
100
+ this.emit(Events.Object.Hover, { objectId: null });
101
+ }
96
102
  }
97
103
 
98
104
  export function destroySelectTool(superDestroy) {
@@ -105,6 +111,12 @@ export function destroySelectTool(superDestroy) {
105
111
  unregisterSelectToolCoreSubscriptions(this);
106
112
  this.clearSelection();
107
113
 
114
+ // Эмитим Hover с null при уничтожении инструмента
115
+ if (this.lastHoveredObjectId !== null) {
116
+ this.lastHoveredObjectId = null;
117
+ this.emit(Events.Object.Hover, { objectId: null });
118
+ }
119
+
108
120
  // Уничтожаем ручки изменения размера
109
121
  if (this.resizeHandles) {
110
122
  this.resizeHandles.destroy();
@@ -48,6 +48,9 @@ export function initializeSelectToolState(instance) {
48
48
  instance.currentX = 0;
49
49
  instance.currentY = 0;
50
50
 
51
+ // Последний наведённый объект (для избежания спама Hover событий)
52
+ instance.lastHoveredObjectId = null;
53
+
51
54
  // Состояние поворота
52
55
  instance.isRotating = false;
53
56
  instance.rotateCenter = null;
@@ -31,8 +31,7 @@ export function applyInitialTextEditorTextareaStyles(textarea, { effectiveFontPx
31
31
  textarea.style.height = `${initialHeightPx}px`;
32
32
  textarea.setAttribute('rows', '1');
33
33
  textarea.style.overflowY = 'hidden';
34
- textarea.style.whiteSpace = 'pre-wrap';
35
- textarea.style.wordBreak = 'break-word';
34
+ textarea.style.whiteSpace = 'pre';
36
35
  textarea.style.letterSpacing = '0px';
37
36
  textarea.style.fontKerning = 'normal';
38
37
  }
@@ -8,14 +8,14 @@ export function createRegularTextAutoSize({
8
8
  minHBound,
9
9
  onSizeChange,
10
10
  }) {
11
- const MAX_AUTO_WIDTH = 360;
12
-
13
11
  return () => {
14
12
  textarea.style.width = 'auto';
15
13
  textarea.style.height = 'auto';
16
14
 
17
- const naturalW = Math.max(1, Math.ceil(textarea.scrollWidth + 1));
18
- const targetW = Math.round(Math.min(MAX_AUTO_WIDTH, Math.max(minWBound, naturalW)));
15
+ const fontPx = parseFloat(textarea.style.fontSize) || 16;
16
+ const rightPad = Math.ceil(fontPx * 0.7) + 6;
17
+ const naturalW = Math.max(1, Math.ceil(textarea.scrollWidth + rightPad));
18
+ const targetW = Math.round(Math.max(minWBound, naturalW));
19
19
  textarea.style.width = `${targetW}px`;
20
20
  wrapper.style.width = `${targetW}px`;
21
21
 
@@ -101,6 +101,14 @@ export function openTextEditor(object, create = false) {
101
101
  }
102
102
  // Прячем глобальные HTML-ручки на время редактирования, чтобы не было второй рамки
103
103
  hideGlobalTextEditorHandlesLayer();
104
+ // Подавляем пересоздание ручек при паразитных ResizeUpdate (тач double-tap):
105
+ // host.update() внутри HandlesEventBridge вызывается при каждом ResizeUpdate,
106
+ // _handlesSuppressed=true гарантирует что showBounds не создаст ручки поверх textarea.
107
+ try {
108
+ if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer) {
109
+ window.moodboardHtmlHandlesLayer._handlesSuppressed = true;
110
+ }
111
+ } catch (_) {}
104
112
 
105
113
  const app = this.app;
106
114
  const world = app?.stage?.getChildByName && app.stage.getChildByName('worldLayer');
@@ -144,7 +152,7 @@ export function openTextEditor(object, create = false) {
144
152
 
145
153
  const textarea = createTextEditorTextarea(content || '');
146
154
  // Без доступного статичного HTML-элемента (часто при create) не оставляем Caveat как fallback.
147
- textarea.style.fontFamily = isNote ? 'Caveat, Arial, cursive' : 'Roboto, Arial, sans-serif';
155
+ textarea.style.fontFamily = 'Caveat, Arial, cursive';
148
156
 
149
157
  // Вычисляем межстрочный интервал; подгоняем к реальным значениям HTML-отображения
150
158
  let lhInitial = computeTextEditorLineHeightPx(effectiveFontPx);
@@ -370,7 +378,9 @@ export function openTextEditor(object, create = false) {
370
378
 
371
379
  // Если создаём новый текст — длина поля ровно как placeholder
372
380
  if (create && !isNote) {
373
- const startWidth = Math.max(1, measureTextEditorPlaceholderWidth(textarea, 'Напишите что-нибудь'));
381
+ // +25% запас на Caveat vs Arial: при незагруженном Caveat span рендерится в Arial,
382
+ // а Caveat (рукописный шрифт) заметно шире для кириллицы.
383
+ const startWidth = Math.max(1, Math.ceil(measureTextEditorPlaceholderWidth(textarea, 'Напишите что-нибудь') * 1.25));
374
384
  const startHeight = Math.max(1, lhInitial - BASELINE_FIX + 10); // +5px сверху и +5px снизу
375
385
  textarea.style.width = `${startWidth}px`;
376
386
  textarea.style.height = `${startHeight}px`;
@@ -397,7 +407,7 @@ export function openTextEditor(object, create = false) {
397
407
  ? ({ widthPx, heightPx }) => {
398
408
  try {
399
409
  const scaleX = (worldLayerRef?.scale?.x) || 1;
400
- const widthWorld = Math.max(1, Math.round(widthPx * viewRes / scaleX));
410
+ const widthWorld = Math.max(1, Math.ceil(widthPx * viewRes / scaleX));
401
411
  const heightWorld = Math.max(1, Math.round(heightPx * viewRes / scaleX));
402
412
  const posReq = { objectId, position: null };
403
413
  this.eventBus.emit(Events.Tool.GetObjectPosition, posReq);
@@ -485,5 +495,13 @@ export function openTextEditor(object, create = false) {
485
495
  }
486
496
 
487
497
  export function closeTextEditor(commit) {
498
+ // Снимаем подавление ручек до вызова closeTextEditorFromState,
499
+ // т.к. тот в конце вызывает updateGlobalTextEditorHandlesLayer() → update() → showBounds,
500
+ // и ручки должны пересоздаться нормально.
501
+ try {
502
+ if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer) {
503
+ window.moodboardHtmlHandlesLayer._handlesSuppressed = false;
504
+ }
505
+ } catch (_) {}
488
506
  return closeTextEditorFromState(this, commit);
489
507
  }
@@ -10,18 +10,16 @@ export function handleObjectSelect(objectId, event) {
10
10
  if (this.isMultiSelect) {
11
11
  this.removeFromSelection(objectId);
12
12
  } else if (this.selection.size() > 1) {
13
- // Перетаскивание группы
14
- this.startGroupDrag(event);
13
+ this._pendingDrag = { isGroup: true, objectId: null, downX: event.x, downY: event.y, event };
15
14
  } else {
16
- // Начинаем перетаскивание
17
- this.startDrag(objectId, event);
15
+ this._pendingDrag = { isGroup: false, objectId, downX: event.x, downY: event.y, event };
18
16
  }
19
17
  } else {
20
18
  this.addToSelection(objectId);
21
19
  if (this.selection.size() > 1) {
22
- this.startGroupDrag(event);
20
+ this._pendingDrag = { isGroup: true, objectId: null, downX: event.x, downY: event.y, event };
23
21
  } else {
24
- this.startDrag(objectId, event);
22
+ this._pendingDrag = { isGroup: false, objectId, downX: event.x, downY: event.y, event };
25
23
  }
26
24
  }
27
25
  }