@sequent-org/moodboard 1.4.30 → 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 +0 -5
  44. package/src/ui/chat/ChatWindow.js +167 -35
  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 +1 -0
  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,251 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../core/events/Events.js';
3
+ import { ConnectorBindingResolver, distanceToSegment } from '../../services/ConnectorBindingResolver.js';
4
+
5
+ const HIT_TEST_SCREEN_PX = 8;
6
+ const ARROW_LEN = 12;
7
+ const ARROW_HALF = 4;
8
+ const DASH_LEN = 8;
9
+ const GAP_LEN = 5;
10
+
11
+ function asArray(value) {
12
+ return Array.isArray(value) ? value : [];
13
+ }
14
+
15
+ /**
16
+ * Рисует пунктирную линию через последовательность moveTo/lineTo.
17
+ * @param {PIXI.Graphics} g
18
+ */
19
+ function drawDashedLine(g, x1, y1, x2, y2) {
20
+ const dx = x2 - x1;
21
+ const dy = y2 - y1;
22
+ const len = Math.hypot(dx, dy);
23
+ if (len < 1e-6) return;
24
+ const ux = dx / len;
25
+ const uy = dy / len;
26
+ let dist = 0;
27
+ let drawing = true;
28
+ g.moveTo(Math.round(x1), Math.round(y1));
29
+ while (dist < len) {
30
+ const step = drawing ? DASH_LEN : GAP_LEN;
31
+ const next = Math.min(dist + step, len);
32
+ const px = x1 + ux * next;
33
+ const py = y1 + uy * next;
34
+ if (drawing) {
35
+ g.lineTo(Math.round(px), Math.round(py));
36
+ } else {
37
+ g.moveTo(Math.round(px), Math.round(py));
38
+ }
39
+ dist = next;
40
+ drawing = !drawing;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Рисует треугольный наконечник стрелки в точке `to`, направление from→to.
46
+ * Вызывать после g.lineStyle(0) не нужно — сбрасывает линию сам.
47
+ * @param {PIXI.Graphics} g
48
+ * @param {{ x: number, y: number }} from
49
+ * @param {{ x: number, y: number }} to
50
+ * @param {number} color PIXI-цвет
51
+ */
52
+ function drawArrow(g, from, to, color) {
53
+ const dx = to.x - from.x;
54
+ const dy = to.y - from.y;
55
+ const len = Math.hypot(dx, dy);
56
+ if (len < 1e-6) return;
57
+ const ux = dx / len;
58
+ const uy = dy / len;
59
+ // перпендикуляр
60
+ const px = -uy;
61
+ const py = ux;
62
+ const bx = to.x - ux * ARROW_LEN;
63
+ const by = to.y - uy * ARROW_LEN;
64
+ g.lineStyle(0);
65
+ g.beginFill(color, 1);
66
+ g.drawPolygon([
67
+ Math.round(to.x), Math.round(to.y),
68
+ Math.round(bx + px * ARROW_HALF), Math.round(by + py * ARROW_HALF),
69
+ Math.round(bx - px * ARROW_HALF), Math.round(by - py * ARROW_HALF),
70
+ ]);
71
+ g.endFill();
72
+ }
73
+
74
+ /**
75
+ * ConnectorLayer — слой рендера универсальных коннекторов.
76
+ *
77
+ * Паттерн: MindmapConnectionLayer (один PIXI.Graphics, полная перерисовка на события).
78
+ * Рисует connector-объекты из state.objects в worldLayer.
79
+ * Резолвинг end-point: ConnectorBindingResolver.resolve() двумя проходами
80
+ * (грубый → точный) для корректной проекции на кромку при isExact=false.
81
+ */
82
+ export class ConnectorLayer {
83
+ /**
84
+ * @param {Object} eventBus Экземпляр EventBus
85
+ * @param {Object} core Экземпляр CoreMoodBoard
86
+ */
87
+ constructor(eventBus, core) {
88
+ this.eventBus = eventBus;
89
+ this.core = core;
90
+ this.graphics = null;
91
+ this.subscriptions = [];
92
+ this._eventsAttached = false;
93
+ /** @type {Array<{ id: string, start: {x,y}, end: {x,y} }>} */
94
+ this._lastSegments = [];
95
+ }
96
+
97
+ /** Инициализирует слой: подписки на события и первый рендер. */
98
+ attach() {
99
+ if (!this.core?.pixi) return;
100
+ if (!this._eventsAttached) {
101
+ this._attachEvents();
102
+ }
103
+ this.updateAll();
104
+ }
105
+
106
+ /** Уничтожает слой: отписка от событий и очистка PIXI-объектов. */
107
+ destroy() {
108
+ this._detachEvents();
109
+ if (this.graphics) {
110
+ this.graphics.clear();
111
+ this.graphics.removeFromParent();
112
+ this.graphics.destroy();
113
+ this.graphics = null;
114
+ }
115
+ this._lastSegments = [];
116
+ this.eventBus = null;
117
+ this.core = null;
118
+ }
119
+
120
+ _attachEvents() {
121
+ if (this._eventsAttached) return;
122
+ const bindings = [
123
+ [Events.Object.Created, () => this.updateAll()],
124
+ [Events.Object.Deleted, () => this.updateAll()],
125
+ [Events.Object.Updated, () => this.updateAll()],
126
+ [Events.Object.StateChanged, () => this.updateAll()],
127
+ [Events.Tool.DragUpdate, () => this.updateAll()],
128
+ [Events.Tool.DragEnd, () => this.updateAll()],
129
+ [Events.Tool.ResizeUpdate, () => this.updateAll()],
130
+ [Events.Tool.ResizeEnd, () => this.updateAll()],
131
+ [Events.Tool.GroupDragUpdate, () => this.updateAll()],
132
+ [Events.Tool.GroupResizeUpdate, () => this.updateAll()],
133
+ [Events.Tool.RotateUpdate, () => this.updateAll()],
134
+ [Events.Tool.PanUpdate, () => this.updateAll()],
135
+ [Events.UI.ZoomPercent, () => this.updateAll()],
136
+ [Events.History.Changed, () => this.updateAll()],
137
+ [Events.Board.Loaded, () => this.updateAll()],
138
+ ];
139
+ bindings.forEach(([event, handler]) => {
140
+ this.eventBus.on(event, handler);
141
+ this.subscriptions.push([event, handler]);
142
+ });
143
+ this._eventsAttached = true;
144
+ }
145
+
146
+ _detachEvents() {
147
+ if (typeof this.eventBus?.off !== 'function') {
148
+ this.subscriptions = [];
149
+ this._eventsAttached = false;
150
+ return;
151
+ }
152
+ this.subscriptions.forEach(([event, handler]) => this.eventBus.off(event, handler));
153
+ this.subscriptions = [];
154
+ this._eventsAttached = false;
155
+ }
156
+
157
+ /** Перерисовывает все коннекторы из state. */
158
+ updateAll() {
159
+ const objects = asArray(this.core?.state?.state?.objects);
160
+ const connectors = objects.filter((o) => o?.type === 'connector');
161
+
162
+ if (connectors.length === 0) {
163
+ if (this.graphics) this.graphics.clear();
164
+ this._lastSegments = [];
165
+ return;
166
+ }
167
+
168
+ if (!this.graphics) {
169
+ this.graphics = new PIXI.Graphics();
170
+ this.graphics.name = 'connector-layer';
171
+ this.graphics.zIndex = 3;
172
+ const world = this.core?.pixi?.worldLayer || this.core?.pixi?.app?.stage;
173
+ world?.addChild?.(this.graphics);
174
+ }
175
+
176
+ const byId = new Map(objects.map((o) => [o.id, o]));
177
+ const g = this.graphics;
178
+ g.clear();
179
+ this._lastSegments = [];
180
+
181
+ connectors.forEach((connector) => {
182
+ const style = connector?.properties?.style ?? {};
183
+ const startTerm = connector?.properties?.start;
184
+ const endTerm = connector?.properties?.end;
185
+ if (!startTerm || !endTerm) return;
186
+
187
+ const startTarget = startTerm.boundId ? (byId.get(startTerm.boundId) ?? null) : null;
188
+ const endTarget = endTerm.boundId ? (byId.get(endTerm.boundId) ?? null) : null;
189
+
190
+ // Двухпроходное резолвание для корректной проекции isExact=false:
191
+ // проход 1 — грубые точки (без взаимной информации)
192
+ const roughStart = ConnectorBindingResolver.resolve(startTerm, startTarget, null);
193
+ const roughEnd = ConnectorBindingResolver.resolve(endTerm, endTarget, null);
194
+ // проход 2 — уточнение с кромочной проекцией
195
+ const start = ConnectorBindingResolver.resolve(startTerm, startTarget, roughEnd);
196
+ const end = ConnectorBindingResolver.resolve(endTerm, endTarget, start);
197
+
198
+ const sx = Math.round(start.x);
199
+ const sy = Math.round(start.y);
200
+ const ex = Math.round(end.x);
201
+ const ey = Math.round(end.y);
202
+
203
+ const color = style.stroke ?? 0x2563EB;
204
+ const width = style.width ?? 2;
205
+ const isDash = !!style.dash;
206
+ const head = style.head ?? { start: false, end: true };
207
+
208
+ try {
209
+ g.lineStyle({ width, color, alpha: 1, alignment: 0, cap: 'round', join: 'round' });
210
+ } catch (_) {
211
+ g.lineStyle(width, color, 1, 0);
212
+ }
213
+
214
+ if (isDash) {
215
+ drawDashedLine(g, sx, sy, ex, ey);
216
+ } else {
217
+ g.moveTo(sx, sy);
218
+ g.lineTo(ex, ey);
219
+ }
220
+
221
+ if (head?.end) drawArrow(g, { x: sx, y: sy }, { x: ex, y: ey }, color);
222
+ if (head?.start) drawArrow(g, { x: ex, y: ey }, { x: sx, y: sy }, color);
223
+
224
+ this._lastSegments.push({ id: connector.id, start: { x: sx, y: sy }, end: { x: ex, y: ey } });
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Возвращает id ближайшего коннектора, если worldPoint в пределах порога.
230
+ * Порог задан в экранных пикселях, пересчитывается в world через текущий scale.
231
+ *
232
+ * @param {{ x: number, y: number }} worldPoint
233
+ * @returns {string|null}
234
+ */
235
+ hitTest(worldPoint) {
236
+ if (this._lastSegments.length === 0) return null;
237
+ // worldLayer.scale.x = zoom; 1 screen px = 1/scale world units
238
+ const scale = this.core?.pixi?.worldLayer?.scale?.x ?? 1;
239
+ const worldThreshold = HIT_TEST_SCREEN_PX / scale;
240
+ let closest = null;
241
+ let minDist = worldThreshold;
242
+ for (const seg of this._lastSegments) {
243
+ const d = distanceToSegment(worldPoint, seg.start, seg.end);
244
+ if (d < minDist) {
245
+ minDist = d;
246
+ closest = seg.id;
247
+ }
248
+ }
249
+ return closest;
250
+ }
251
+ }
@@ -1223,6 +1223,8 @@ export class HandlesDomRenderer {
1223
1223
  transformOrigin: 'center center',
1224
1224
  transform: `rotate(${rotation}deg)`,
1225
1225
  });
1226
+ box.style.setProperty('--box-w', `${width}px`);
1227
+ box.style.setProperty('--box-h', `${height}px`);
1226
1228
  this.host.layer.appendChild(box);
1227
1229
  if (this.host._handlesSuppressed) {
1228
1230
  this.host.visible = true;
@@ -1252,12 +1254,12 @@ export class HandlesDomRenderer {
1252
1254
  h.style.cursor = cursor;
1253
1255
  });
1254
1256
  h.addEventListener('mouseleave', () => {
1255
- h.style.background = HANDLES_ACCENT_COLOR;
1257
+ h.style.background = '#ffffff';
1256
1258
  h.style.borderColor = HANDLES_ACCENT_COLOR;
1257
1259
  });
1258
1260
 
1259
1261
  if (!isNonResizableTarget) {
1260
- h.addEventListener('mousedown', (e) => this.host._onHandleDown(e, box));
1262
+ h.addEventListener('pointerdown', (e) => this.host._onHandleDown(e, box));
1261
1263
  }
1262
1264
 
1263
1265
  box.appendChild(h);
@@ -1286,7 +1288,7 @@ export class HandlesDomRenderer {
1286
1288
  });
1287
1289
  if (isNonResizableTarget) e.dataset.lockedHidden = '1';
1288
1290
  if (!isNonResizableTarget) {
1289
- e.addEventListener('mousedown', (evt) => this.host._onEdgeResizeDown(evt));
1291
+ e.addEventListener('pointerdown', (evt) => this.host._onEdgeResizeDown(evt));
1290
1292
  }
1291
1293
  box.appendChild(e);
1292
1294
  };
@@ -1341,7 +1343,7 @@ export class HandlesDomRenderer {
1341
1343
  svgEl.style.height = '100%';
1342
1344
  svgEl.style.display = 'block';
1343
1345
  }
1344
- rotateHandle.addEventListener('mousedown', (e) => this.host._onRotateHandleDown(e, box));
1346
+ rotateHandle.addEventListener('pointerdown', (e) => this.host._onRotateHandleDown(e, box));
1345
1347
  }
1346
1348
  box.appendChild(rotateHandle);
1347
1349
 
@@ -1626,7 +1628,7 @@ export class HandlesDomRenderer {
1626
1628
  btn.style.left = `${Math.round(left + width + centerOffset)}px`;
1627
1629
  }
1628
1630
  btn.style.top = `${centerY}px`;
1629
- btn.addEventListener('mousedown', (evt) => {
1631
+ btn.addEventListener('pointerdown', (evt) => {
1630
1632
  evt.preventDefault();
1631
1633
  evt.stopPropagation();
1632
1634
  });
@@ -1651,7 +1653,7 @@ export class HandlesDomRenderer {
1651
1653
  const centerOffset = edgeGap + buttonRadius;
1652
1654
  btn.style.left = `${centerX}px`;
1653
1655
  btn.style.top = `${Math.round(top + height + centerOffset)}px`;
1654
- btn.addEventListener('mousedown', (evt) => {
1656
+ btn.addEventListener('pointerdown', (evt) => {
1655
1657
  evt.preventDefault();
1656
1658
  evt.stopPropagation();
1657
1659
  });
@@ -1680,7 +1682,7 @@ export class HandlesDomRenderer {
1680
1682
  showInModelButton.innerHTML = `${REVIT_SHOW_IN_MODEL_ICON_SVG}<span>Показать в модели</span>`;
1681
1683
  showInModelButton.style.left = `${Math.round(left + width / 2)}px`;
1682
1684
  showInModelButton.style.top = `${Math.round(top - 34)}px`;
1683
- showInModelButton.addEventListener('mousedown', (evt) => {
1685
+ showInModelButton.addEventListener('pointerdown', (evt) => {
1684
1686
  evt.preventDefault();
1685
1687
  evt.stopPropagation();
1686
1688
  });
@@ -1702,6 +1704,8 @@ export class HandlesDomRenderer {
1702
1704
  repositionBoxChildren(box) {
1703
1705
  const width = parseFloat(box.style.width);
1704
1706
  const height = parseFloat(box.style.height);
1707
+ box.style.setProperty('--box-w', `${width}px`);
1708
+ box.style.setProperty('--box-h', `${height}px`);
1705
1709
  const cx = width / 2;
1706
1710
  const cy = height / 2;
1707
1711
 
@@ -42,10 +42,10 @@ export class HandlesInteractionController {
42
42
  const screenX = cssRect.left - offsetLeft;
43
43
  const screenY = cssRect.top - offsetTop;
44
44
  return {
45
- x: ((screenX * rendererRes) - tx) / s,
46
- y: ((screenY * rendererRes) - ty) / s,
47
- width: (cssRect.width * rendererRes) / s,
48
- height: (cssRect.height * rendererRes) / s,
45
+ x: (screenX - tx) / s,
46
+ y: (screenY - ty) / s,
47
+ width: cssRect.width / s,
48
+ height: cssRect.height / s,
49
49
  };
50
50
  }
51
51
 
@@ -155,6 +155,37 @@ export class HandlesInteractionController {
155
155
  const dir = e.currentTarget.dataset.dir;
156
156
  const id = e.currentTarget.dataset.id;
157
157
  const isGroup = id === '__group__';
158
+
159
+ // Детектируем двойной клик/тап по ручке текстового объекта → открываем редактор вместо resize.
160
+ // Необходимо, т.к. при dblclick второй mousedown попадает на HTML-ручку (stopPropagation
161
+ // обрывает bubbling до canvas), поэтому нативный dblclick до canvas не доходит.
162
+ if (!isGroup) {
163
+ const _now = performance.now();
164
+ if (!this._lastHandleDownTime) this._lastHandleDownTime = {};
165
+ const _prevTime = this._lastHandleDownTime[id];
166
+ this._lastHandleDownTime[id] = _now;
167
+ if (_prevTime !== undefined && (_now - _prevTime) < 300) {
168
+ const _typeReq = { objectId: id, pixiObject: null };
169
+ this.host.eventBus.emit(Events.Tool.GetObjectPixi, _typeReq);
170
+ const _mbType = _typeReq.pixiObject?._mb?.type;
171
+ if (_mbType === 'text' || _mbType === 'simple-text' || _mbType === 'note') {
172
+ const _posData = { objectId: id, position: null };
173
+ this.host.eventBus.emit(Events.Tool.GetObjectPosition, _posData);
174
+ if (_posData.position) {
175
+ this.host.eventBus.emit(Events.Tool.ObjectEdit, {
176
+ id,
177
+ type: _mbType,
178
+ position: _posData.position,
179
+ properties: _typeReq.pixiObject?._mb?.properties || {},
180
+ caretClick: { clientX: e.clientX, clientY: e.clientY },
181
+ create: false,
182
+ });
183
+ }
184
+ return;
185
+ }
186
+ }
187
+ }
188
+
158
189
  const world = this.host.core.pixi.worldLayer || this.host.core.pixi.app.stage;
159
190
  const s = world?.scale?.x || 1;
160
191
  const tx = world?.x || 0;
@@ -328,10 +359,10 @@ export class HandlesInteractionController {
328
359
  const screenY = (newTop - offsetTop);
329
360
  const screenW = newW;
330
361
  const screenH = newH;
331
- const worldX = ((screenX * rendererRes) - tx) / s;
332
- const worldY = ((screenY * rendererRes) - ty) / s;
333
- const worldW = (screenW * rendererRes) / s;
334
- const worldH = (screenH * rendererRes) / s;
362
+ const worldX = (screenX - tx) / s;
363
+ const worldY = (screenY - ty) / s;
364
+ const worldW = screenW / s;
365
+ const worldH = screenH / s;
335
366
 
336
367
  if (isGroup) {
337
368
  this.host.eventBus.emit(Events.Tool.GroupResizeUpdate, {
@@ -359,8 +390,8 @@ export class HandlesInteractionController {
359
390
  };
360
391
 
361
392
  const onUp = () => {
362
- document.removeEventListener('mousemove', onMove);
363
- document.removeEventListener('mouseup', onUp);
393
+ document.removeEventListener('pointermove', onMove);
394
+ document.removeEventListener('pointerup', onUp);
364
395
  const endCSS = {
365
396
  left: parseFloat(box.style.left),
366
397
  top: parseFloat(box.style.top),
@@ -371,10 +402,10 @@ export class HandlesInteractionController {
371
402
  const screenY = (endCSS.top - offsetTop);
372
403
  const screenW = endCSS.width;
373
404
  const screenH = endCSS.height;
374
- const worldX = ((screenX * rendererRes) - tx) / s;
375
- const worldY = ((screenY * rendererRes) - ty) / s;
376
- const worldW = (screenW * rendererRes) / s;
377
- const worldH = (screenH * rendererRes) / s;
405
+ const worldX = (screenX - tx) / s;
406
+ const worldY = (screenY - ty) / s;
407
+ const worldW = screenW / s;
408
+ const worldH = screenH / s;
378
409
 
379
410
  if (isGroup) {
380
411
  this.host.eventBus.emit(Events.Tool.GroupResizeEnd, { objects });
@@ -406,7 +437,7 @@ export class HandlesInteractionController {
406
437
  el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
407
438
  el.style.height = 'auto';
408
439
  const measured = Math.max(1, Math.round(el.scrollHeight));
409
- const worldH2 = (measured * rendererRes) / s;
440
+ const worldH2 = measured / s;
410
441
  const fixData = {
411
442
  object: id,
412
443
  size: { width: worldW, height: worldH2 },
@@ -419,8 +450,8 @@ export class HandlesInteractionController {
419
450
  }
420
451
  };
421
452
 
422
- document.addEventListener('mousemove', onMove);
423
- document.addEventListener('mouseup', onUp);
453
+ document.addEventListener('pointermove', onMove);
454
+ document.addEventListener('pointerup', onUp);
424
455
  }
425
456
 
426
457
  onEdgeResizeDown(e) {
@@ -600,10 +631,10 @@ export class HandlesInteractionController {
600
631
  const screenY = (newTop - offsetTop);
601
632
  const screenW = newW;
602
633
  const screenH = newH;
603
- const worldX = ((screenX * rendererRes) - tx) / s;
604
- const worldY = ((screenY * rendererRes) - ty) / s;
605
- const worldW = (screenW * rendererRes) / s;
606
- const worldH = (screenH * rendererRes) / s;
634
+ const worldX = (screenX - tx) / s;
635
+ const worldY = (screenY - ty) / s;
636
+ const worldW = screenW / s;
637
+ const worldH = screenH / s;
607
638
  const edgePositionChanged = (newLeft !== startCSS.left) || (newTop !== startCSS.top);
608
639
 
609
640
  if (isGroup) {
@@ -624,8 +655,8 @@ export class HandlesInteractionController {
624
655
  };
625
656
 
626
657
  const onUp = () => {
627
- document.removeEventListener('mousemove', onMove);
628
- document.removeEventListener('mouseup', onUp);
658
+ document.removeEventListener('pointermove', onMove);
659
+ document.removeEventListener('pointerup', onUp);
629
660
  const endCSS = {
630
661
  left: parseFloat(box.style.left),
631
662
  top: parseFloat(box.style.top),
@@ -636,10 +667,10 @@ export class HandlesInteractionController {
636
667
  const screenY = (endCSS.top - offsetTop);
637
668
  const screenW = endCSS.width;
638
669
  const screenH = endCSS.height;
639
- const worldX = ((screenX * rendererRes) - tx) / s;
640
- const worldY = ((screenY * rendererRes) - ty) / s;
641
- const worldW = (screenW * rendererRes) / s;
642
- const worldH = (screenH * rendererRes) / s;
670
+ const worldX = (screenX - tx) / s;
671
+ const worldY = (screenY - ty) / s;
672
+ const worldW = screenW / s;
673
+ const worldH = screenH / s;
643
674
 
644
675
  if (isGroup) {
645
676
  this.host.eventBus.emit(Events.Tool.GroupResizeEnd, { objects });
@@ -654,7 +685,7 @@ export class HandlesInteractionController {
654
685
  el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
655
686
  el.style.height = 'auto';
656
687
  const measured = Math.max(1, Math.round(el.scrollHeight));
657
- finalWorldH = (measured * rendererRes) / s;
688
+ finalWorldH = measured / s;
658
689
  }
659
690
  } catch (_) {}
660
691
  }
@@ -670,8 +701,8 @@ export class HandlesInteractionController {
670
701
  }
671
702
  };
672
703
 
673
- document.addEventListener('mousemove', onMove);
674
- document.addEventListener('mouseup', onUp);
704
+ document.addEventListener('pointermove', onMove);
705
+ document.addEventListener('pointerup', onUp);
675
706
  }
676
707
 
677
708
  onRotateHandleDown(e, box) {
@@ -741,8 +772,8 @@ export class HandlesInteractionController {
741
772
  };
742
773
 
743
774
  const onRotateUp = (ev) => {
744
- document.removeEventListener('mousemove', onRotateMove);
745
- document.removeEventListener('mouseup', onRotateUp);
775
+ document.removeEventListener('pointermove', onRotateMove);
776
+ document.removeEventListener('pointerup', onRotateUp);
746
777
 
747
778
  if (handleElement) {
748
779
  handleElement.style.cursor = 'grab';
@@ -766,7 +797,7 @@ export class HandlesInteractionController {
766
797
  }
767
798
  };
768
799
 
769
- document.addEventListener('mousemove', onRotateMove);
770
- document.addEventListener('mouseup', onRotateUp);
800
+ document.addEventListener('pointermove', onRotateMove);
801
+ document.addEventListener('pointerup', onRotateUp);
771
802
  }
772
803
  }
@@ -102,14 +102,49 @@ export class HandlesPositioningService {
102
102
  const screenX = cssRect.left - offsetLeft;
103
103
  const screenY = cssRect.top - offsetTop;
104
104
  return {
105
- x: ((screenX * rendererRes) - tx) / s,
106
- y: ((screenY * rendererRes) - ty) / s,
107
- width: (cssRect.width * rendererRes) / s,
108
- height: (cssRect.height * rendererRes) / s,
105
+ x: (screenX - tx) / s,
106
+ y: (screenY - ty) / s,
107
+ width: cssRect.width / s,
108
+ height: cssRect.height / s,
109
109
  };
110
110
  }
111
111
 
112
112
  getSingleSelectionWorldBounds(id, pixi) {
113
+ // Для текстовых объектов (type=text/simple-text) рамку строим по реальному DOM-боксу
114
+ // букв, а не по сохранённому width/height из state. Только для неповёрнутых объектов:
115
+ // getBoundingClientRect повёрнутого элемента — axis-aligned, даст неверный размер.
116
+ if (typeof document !== 'undefined') {
117
+ const textEl = document.querySelector(`.mb-text[data-id="${id}"]`);
118
+ if (textEl) {
119
+ const rotationData = { objectId: id, rotation: 0 };
120
+ this.host.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
121
+ if (Math.abs(rotationData.rotation || 0) < 0.001) {
122
+ try {
123
+ const r = textEl.getBoundingClientRect();
124
+ // Пустой/создаваемый текст: .mb-text схлопнут (высота ~2px),
125
+ // плейсхолдер живёт в textarea редактора, а не в этом div.
126
+ // В этом случае DOM-боксу не доверяем — падаем в state-размер ниже.
127
+ const hasContent = !!(textEl.textContent && textEl.textContent.trim());
128
+ if (hasContent && r.width > 2 && r.height > 2) {
129
+ const view = this.host.core.pixi.app.view;
130
+ const viewRect = view.getBoundingClientRect();
131
+ const { world } = this.getWorldTransform();
132
+ // getBoundingClientRect в CSS pixels; viewRect тоже; разность —
133
+ // глобальные PIXI-координаты (toGlobal/toLocal работают в CSS px).
134
+ const tl = world.toLocal(new PIXI.Point(r.left - viewRect.left, r.top - viewRect.top));
135
+ const br = world.toLocal(new PIXI.Point(r.right - viewRect.left, r.bottom - viewRect.top));
136
+ return {
137
+ x: Math.min(tl.x, br.x),
138
+ y: Math.min(tl.y, br.y),
139
+ width: Math.max(1, Math.abs(br.x - tl.x)),
140
+ height: Math.max(1, Math.abs(br.y - tl.y)),
141
+ };
142
+ }
143
+ } catch (_) {}
144
+ }
145
+ }
146
+ }
147
+
113
148
  const positionData = { objectId: id, position: null };
114
149
  const sizeData = { objectId: id, size: null };
115
150
  this.host.eventBus.emit(Events.Tool.GetObjectPosition, positionData);
@@ -199,8 +234,8 @@ export class HandlesPositioningService {
199
234
  const screenX = centerX - offsetLeft;
200
235
  const screenY = centerY - offsetTop;
201
236
  return {
202
- x: ((screenX * rendererRes) - tx) / s,
203
- y: ((screenY * rendererRes) - ty) / s,
237
+ x: (screenX - tx) / s,
238
+ y: (screenY - ty) / s,
204
239
  };
205
240
  }
206
241
  }