@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
@@ -0,0 +1,85 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { BaseTool } from '../BaseTool.js';
3
+ import { Events } from '../../core/events/Events.js';
4
+
5
+ /**
6
+ * Инструмент «комментарий» — клик по холсту открывает черновик треда у world-точки.
7
+ */
8
+ export class CommentTool extends BaseTool {
9
+ constructor(eventBus, core, commentService, threadPopover) {
10
+ super('comment', eventBus);
11
+ this.core = core;
12
+ this.commentService = commentService;
13
+ this.threadPopover = threadPopover;
14
+ this.cursor = CommentTool._buildCursor();
15
+ this.app = null;
16
+ this.world = null;
17
+ }
18
+
19
+ activate(app) {
20
+ super.activate(app);
21
+ this.app = app;
22
+ this.world = this.core?.pixi?.worldLayer || app?.stage;
23
+ if (this.app?.view) {
24
+ this.app.view.style.cursor = this.cursor;
25
+ }
26
+ }
27
+
28
+ deactivate() {
29
+ if (this.app?.view) this.app.view.style.cursor = '';
30
+ this.app = null;
31
+ this.world = null;
32
+ super.deactivate();
33
+ }
34
+
35
+ onMouseDown(event) {
36
+ super.onMouseDown(event);
37
+ if (!this.world) return;
38
+
39
+ const worldPt = this._toWorld(event.x, event.y);
40
+ const hitData = { x: event.x, y: event.y, result: null };
41
+ this.eventBus.emit(Events.Tool.HitTest, hitData);
42
+
43
+ let anchor = null;
44
+ if (hitData.result?.object) {
45
+ const objectId = hitData.result.object;
46
+ const pos = { objectId, position: null };
47
+ this.eventBus.emit(Events.Tool.GetObjectPosition, pos);
48
+ if (pos.position) {
49
+ anchor = {
50
+ anchor_object_id: objectId,
51
+ anchor_dx: worldPt.x - pos.position.x,
52
+ anchor_dy: worldPt.y - pos.position.y,
53
+ };
54
+ }
55
+ }
56
+
57
+ this.threadPopover?.openDraftAt(
58
+ { x: worldPt.x, y: worldPt.y },
59
+ anchor
60
+ );
61
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
62
+ }
63
+
64
+ _toWorld(x, y) {
65
+ if (!this.world) return { x, y };
66
+ const local = this.world.toLocal(new PIXI.Point(x, y));
67
+ return { x: local.x, y: local.y };
68
+ }
69
+
70
+ /**
71
+ * Строит CSS-курсор: залитый пузырь комментария с острым углом снизу слева.
72
+ * Hotspot совпадает с острием уголка (2, 30).
73
+ */
74
+ static _buildCursor() {
75
+ const svg = [
76
+ '<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">',
77
+ '<path d="M12 2 C6.477 2 2 6.477 2 12 C2 14.8 3.1 17.3 5 19.2 L2 22 L7 20.5',
78
+ ' C8.5 21.4 10.2 22 12 22 C17.523 22 22 17.523 22 12 C22 6.477 17.523 2 12 2 Z"',
79
+ ' fill="#193042" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>',
80
+ '</svg>',
81
+ ].join('');
82
+ const url = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}")`;
83
+ return `${url} 2 22, crosshair`;
84
+ }
85
+ }
@@ -5,6 +5,11 @@ import * as PIXI from 'pixi.js';
5
5
  /** Максимум точек в одном штрихе (decimation при превышении). */
6
6
  const MAX_POINTS = 5000;
7
7
 
8
+ /** Шаг примагничивания угла (45°) при зажатом Shift. */
9
+ const SNAP_STEP = Math.PI / 4;
10
+ /** Допуск примагничивания: ±8° вокруг каждого кратного 45°. */
11
+ const SNAP_TOLERANCE = (8 * Math.PI) / 180;
12
+
8
13
  /**
9
14
  * Инструмент рисования (карандаш)
10
15
  */
@@ -22,6 +27,10 @@ export class DrawingTool extends BaseTool {
22
27
  this.app = null;
23
28
  this.world = null;
24
29
 
30
+ // Якорная точка для режима прямой линии (Shift)
31
+ this._straightAnchor = null;
32
+ this._shiftDown = false;
33
+
25
34
  // Параметры кисти по умолчанию
26
35
  this.brush = {
27
36
  color: 0x111827, // чёрный
@@ -124,6 +133,7 @@ export class DrawingTool extends BaseTool {
124
133
  this.world.addChild(this.tempGraphics);
125
134
 
126
135
  const p = this._toWorld(event.x, event.y);
136
+ this._straightAnchor = { ...p };
127
137
  this.points.push(p);
128
138
  this._redrawTemporary();
129
139
  }
@@ -131,11 +141,23 @@ export class DrawingTool extends BaseTool {
131
141
  onMouseMove(event) {
132
142
  super.onMouseMove(event);
133
143
  if (!this.isDrawing) return;
144
+
134
145
  const p = this._toWorld(event.x, event.y);
146
+ const isStraight = !!(event.originalEvent?.shiftKey) && this.brush.mode !== 'eraser';
147
+
148
+ if (isStraight) {
149
+ const end = this._snapEndpoint(this._straightAnchor, p);
150
+ this.points = [this._straightAnchor, end];
151
+ this._redrawTemporary();
152
+ return;
153
+ }
154
+
135
155
  const prev = this.points[this.points.length - 1];
136
156
  // Фильтр слишком частых точек
137
157
  if (!prev || Math.hypot(p.x - prev.x, p.y - prev.y) >= 1) {
138
158
  this.points.push(p);
159
+ // Обновляем якорь — при отпускании Shift штрих продолжится с текущей точки
160
+ this._straightAnchor = { ...p };
139
161
  // Ластик: при движении удаляем все фигуры, пересекаемые текущим сегментом
140
162
  if (this.brush.mode === 'eraser' && prev) {
141
163
  this._eraserSweep(prev, p);
@@ -153,6 +175,26 @@ export class DrawingTool extends BaseTool {
153
175
  }
154
176
  }
155
177
 
178
+ onKeyDown(event) {
179
+ if (event.key === 'Shift' && this.isDrawing && this.brush.mode !== 'eraser') {
180
+ this._shiftDown = true;
181
+ // Перерисовываем прямую до текущей позиции курсора
182
+ if (this.currentPoint) {
183
+ const p = this._toWorld(this.currentPoint.x, this.currentPoint.y);
184
+ const end = this._snapEndpoint(this._straightAnchor, p);
185
+ this.points = [this._straightAnchor, end];
186
+ this._redrawTemporary();
187
+ }
188
+ }
189
+ }
190
+
191
+ onKeyUp(event) {
192
+ if (event.key === 'Shift' && this.isDrawing && this.brush.mode !== 'eraser') {
193
+ this._shiftDown = false;
194
+ // При отпускании Shift якорь уже актуален — продолжение в freehand-режиме
195
+ }
196
+ }
197
+
156
198
  onMouseUp(event) {
157
199
  super.onMouseUp(event);
158
200
  this._finishAndCommit();
@@ -161,6 +203,8 @@ export class DrawingTool extends BaseTool {
161
203
  _finishAndCommit() {
162
204
  if (!this.isDrawing) return;
163
205
  this.isDrawing = false;
206
+ this._straightAnchor = null;
207
+ this._shiftDown = false;
164
208
 
165
209
  // Если ластик — чистим временную линию и выходим (удаление уже произошло onMouseDown)
166
210
  if (this.brush.mode === 'eraser') {
@@ -196,11 +240,15 @@ export class DrawingTool extends BaseTool {
196
240
  if (pointsToUse.length > MAX_POINTS) {
197
241
  pointsToUse = this._decimatePoints(pointsToUse, MAX_POINTS);
198
242
  }
199
- // Нормализуем точки относительно левого-верхнего угла
200
- const normPoints = pointsToUse.map(pt => ({ x: pt.x - minX, y: pt.y - minY }));
243
+
244
+ // position целая (округлённая) точка; нормализуем точки от тех же значений,
245
+ // иначе дробный остаток minX/minY сдвигал бы геометрию относительно position.
246
+ const posX = Math.round(minX);
247
+ const posY = Math.round(minY);
248
+ const normPoints = pointsToUse.map(pt => ({ x: pt.x - posX, y: pt.y - posY }));
201
249
 
202
250
  // Создаем объект типа drawing через существующий пайплайн
203
- const position = { x: Math.round(minX), y: Math.round(minY) };
251
+ const position = { x: posX, y: posY };
204
252
  const properties = {
205
253
  points: normPoints,
206
254
  strokeColor: this.brush.color,
@@ -335,6 +383,29 @@ export class DrawingTool extends BaseTool {
335
383
  g.quadraticCurveTo(pen.x, pen.y, last.x, last.y);
336
384
  }
337
385
 
386
+ /**
387
+ * Примагничивает конечную точку прямой к ближайшему кратному 45°,
388
+ * если угол попадает в допуск SNAP_TOLERANCE. Длина линии сохраняется.
389
+ * Если отрезок слишком короткий или угол вне зоны — возвращает p без изменений.
390
+ */
391
+ _snapEndpoint(anchor, p) {
392
+ const dx = p.x - anchor.x;
393
+ const dy = p.y - anchor.y;
394
+ const len = Math.hypot(dx, dy);
395
+ if (len < 1) return p;
396
+
397
+ const angle = Math.atan2(dy, dx);
398
+ const snapped = Math.round(angle / SNAP_STEP) * SNAP_STEP;
399
+
400
+ if (Math.abs(angle - snapped) <= SNAP_TOLERANCE) {
401
+ return {
402
+ x: anchor.x + Math.cos(snapped) * len,
403
+ y: anchor.y + Math.sin(snapped) * len
404
+ };
405
+ }
406
+ return p;
407
+ }
408
+
338
409
  _toWorld(x, y) {
339
410
  if (!this.world) return { x, y };
340
411
  const global = new PIXI.Point(x, y);
@@ -389,24 +460,29 @@ export class DrawingTool extends BaseTool {
389
460
  const props = meta.properties || {};
390
461
  const pts = Array.isArray(props.points) ? props.points : [];
391
462
  if (pts.length >= 2) {
392
- // Оценка масштабов
463
+ // Масштаб берём из локальных границ (не зависят от zoom) — как в _containsPoint DrawingObject
464
+ const lb = typeof pixi.getLocalBounds === 'function' ? pixi.getLocalBounds() : b;
393
465
  const baseW = props.baseWidth || 1;
394
466
  const baseH = props.baseHeight || 1;
395
- const scaleX = baseW ? (b.width / baseW) : 1;
396
- const scaleY = baseH ? (b.height / baseH) : 1;
467
+ const scaleX = baseW ? (lb.width / baseW) : 1;
468
+ const scaleY = baseH ? (lb.height / baseH) : 1;
397
469
  const eraserThresh = Math.max(6, (props.strokeWidth || 2) / 2 + radius);
398
470
  // трансформируем сегмент ластика в локальные координаты фигуры
399
471
  const localPrev = pixi.toLocal(new PIXI.Point(prevGlobal.x, prevGlobal.y));
400
472
  const localCurr = pixi.toLocal(new PIXI.Point(currGlobal.x, currGlobal.y));
401
- // Проверяем пересечение с каждым отрезком рисунка
473
+ // Проверяем пересечение отрезка ластика с каждым отрезком рисунка.
474
+ // Используем отрезок→отрезок, а не точка→отрезок: при быстром движении
475
+ // оба конца сегмента ластика могут быть дальше threshold, но сам отрезок
476
+ // пересекает линию — именно это давало «раз через раз» при быстром мазке.
402
477
  for (let i = 0; i < pts.length - 1 && !intersects; i++) {
403
478
  const ax = pts[i].x * scaleX;
404
479
  const ay = pts[i].y * scaleY;
405
480
  const bx = pts[i + 1].x * scaleX;
406
481
  const by = pts[i + 1].y * scaleY;
407
- const d = this._distancePointToSegment(localPrev.x, localPrev.y, ax, ay, bx, by);
408
- const d2 = this._distancePointToSegment(localCurr.x, localCurr.y, ax, ay, bx, by);
409
- if (Math.min(d, d2) <= eraserThresh) intersects = true;
482
+ if (this._distanceSegmentToSegment(
483
+ localPrev.x, localPrev.y, localCurr.x, localCurr.y,
484
+ ax, ay, bx, by
485
+ ) <= eraserThresh) intersects = true;
410
486
  }
411
487
  }
412
488
  }
@@ -445,6 +521,30 @@ export class DrawingTool extends BaseTool {
445
521
  const cy = ay + t * aby;
446
522
  return Math.hypot(px - cx, py - cy);
447
523
  }
524
+
525
+ // Минимальное расстояние между отрезком p1→p2 и отрезком q1→q2.
526
+ // Возвращает 0 если отрезки пересекаются — гарантирует стирание при
527
+ // быстром поперечном движении, когда ни одна из концевых точек не близка к линии.
528
+ _distanceSegmentToSegment(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y) {
529
+ if (this._segmentsIntersect(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y)) return 0;
530
+ return Math.min(
531
+ this._distancePointToSegment(p1x, p1y, q1x, q1y, q2x, q2y),
532
+ this._distancePointToSegment(p2x, p2y, q1x, q1y, q2x, q2y),
533
+ this._distancePointToSegment(q1x, q1y, p1x, p1y, p2x, p2y),
534
+ this._distancePointToSegment(q2x, q2y, p1x, p1y, p2x, p2y)
535
+ );
536
+ }
537
+
538
+ _segmentsIntersect(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y) {
539
+ const d1x = p2x - p1x, d1y = p2y - p1y;
540
+ const d2x = q2x - q1x, d2y = q2y - q1y;
541
+ const cross = d1x * d2y - d1y * d2x;
542
+ if (Math.abs(cross) < 1e-10) return false; // параллельны или коллинеарны
543
+ const dx = q1x - p1x, dy = q1y - p1y;
544
+ const t = (dx * d2y - dy * d2x) / cross;
545
+ const u = (dx * d1y - dy * d1x) / cross;
546
+ return t >= 0 && t <= 1 && u >= 0 && u <= 1;
547
+ }
448
548
  }
449
549
 
450
550
 
@@ -0,0 +1,121 @@
1
+ import { BaseTool } from '../BaseTool.js';
2
+ import * as PIXI from 'pixi.js';
3
+
4
+ const LASER_COLOR = 0xff2222;
5
+ const LASER_DOT_COLOR = 0xff4444;
6
+ const TRAIL_MAX_POINTS = 30;
7
+ const FADE_STEPS = 20;
8
+ const FADE_INTERVAL_MS = 40;
9
+
10
+ /**
11
+ * Инструмент «Лазерная указка» — оставляет эфемерный затухающий след, ничего не сохраняет.
12
+ */
13
+ export class LaserPointerTool extends BaseTool {
14
+ constructor(eventBus) {
15
+ super('laser', eventBus);
16
+ this.hotkey = null;
17
+ this.cursor = 'crosshair';
18
+
19
+ this.isDrawing = false;
20
+ this.points = [];
21
+ this.graphics = null;
22
+ this.app = null;
23
+ this._fadeTimer = null;
24
+ }
25
+
26
+ activate(app) {
27
+ super.activate();
28
+ this.app = app;
29
+ if (app && app.view) app.view.style.cursor = 'crosshair';
30
+ if (app && app.stage) {
31
+ app.stage.sortableChildren = true;
32
+ this.graphics = new PIXI.Graphics();
33
+ this.graphics.zIndex = 3000;
34
+ this.graphics.name = 'laser-trail';
35
+ app.stage.addChild(this.graphics);
36
+ }
37
+ }
38
+
39
+ deactivate() {
40
+ this._clearFadeTimer();
41
+ if (this.graphics) {
42
+ this.graphics.clear();
43
+ if (this.graphics.parent) this.graphics.parent.removeChild(this.graphics);
44
+ this.graphics.destroy();
45
+ this.graphics = null;
46
+ }
47
+ if (this.app && this.app.view) this.app.view.style.cursor = '';
48
+ this.app = null;
49
+ this.points = [];
50
+ this.isDrawing = false;
51
+ super.deactivate();
52
+ }
53
+
54
+ onMouseDown(event) {
55
+ this.isDrawing = true;
56
+ this._clearFadeTimer();
57
+ if (this.graphics) this.graphics.alpha = 1;
58
+ this.points = [{ x: event.x, y: event.y }];
59
+ this._redrawTrail();
60
+ }
61
+
62
+ onMouseMove(event) {
63
+ if (!this.isDrawing) return;
64
+ this.points.push({ x: event.x, y: event.y });
65
+ if (this.points.length > TRAIL_MAX_POINTS) this.points.shift();
66
+ this._redrawTrail();
67
+ }
68
+
69
+ onMouseUp() {
70
+ this.isDrawing = false;
71
+ this._scheduleFade();
72
+ }
73
+
74
+ _redrawTrail() {
75
+ if (!this.graphics) return;
76
+ this.graphics.clear();
77
+ const n = this.points.length;
78
+ if (n < 2) return;
79
+
80
+ for (let i = 1; i < n; i++) {
81
+ const t = i / n;
82
+ const width = Math.max(1, t * 3);
83
+ this.graphics.lineStyle(width, LASER_COLOR, t * 0.85);
84
+ this.graphics.moveTo(this.points[i - 1].x, this.points[i - 1].y);
85
+ this.graphics.lineTo(this.points[i].x, this.points[i].y);
86
+ }
87
+
88
+ const last = this.points[n - 1];
89
+ this.graphics.beginFill(LASER_DOT_COLOR, 0.95);
90
+ this.graphics.drawCircle(last.x, last.y, 5);
91
+ this.graphics.endFill();
92
+ }
93
+
94
+ _scheduleFade() {
95
+ this._clearFadeTimer();
96
+ let step = 0;
97
+ this._fadeTimer = setInterval(() => {
98
+ step++;
99
+ if (!this.graphics) { clearInterval(this._fadeTimer); this._fadeTimer = null; return; }
100
+ const alpha = Math.max(0, 1 - step / FADE_STEPS);
101
+ this.graphics.alpha = alpha;
102
+ if (step >= FADE_STEPS) {
103
+ clearInterval(this._fadeTimer);
104
+ this._fadeTimer = null;
105
+ if (this.graphics) {
106
+ this.graphics.clear();
107
+ this.graphics.alpha = 1;
108
+ }
109
+ this.points = [];
110
+ }
111
+ }, FADE_INTERVAL_MS);
112
+ }
113
+
114
+ _clearFadeTimer() {
115
+ if (this._fadeTimer) {
116
+ clearInterval(this._fadeTimer);
117
+ this._fadeTimer = null;
118
+ }
119
+ if (this.graphics) this.graphics.alpha = 1;
120
+ }
121
+ }
@@ -28,7 +28,9 @@ import {
28
28
  openFileNameEditor as openFileNameEditorViaController,
29
29
  closeFileNameEditor as closeFileNameEditorViaController,
30
30
  closeTextEditor as closeTextEditorViaController,
31
- closeMindmapEditor as closeMindmapEditorViaController
31
+ closeMindmapEditor as closeMindmapEditorViaController,
32
+ openFrameTitleEditor as openFrameTitleEditorViaController,
33
+ closeFrameTitleEditor as closeFrameTitleEditorViaController,
32
34
  } from './selection/InlineEditorController.js';
33
35
  import {
34
36
  hitTest as hitTestViaService,
@@ -206,6 +208,20 @@ export class SelectTool extends BaseTool {
206
208
  endBoxSelect() {
207
209
  return endBoxSelectViaController.call(this);
208
210
  }
211
+
212
+ startLassoSelect(event) {
213
+ this.isLassoSelect = true;
214
+ if (this._lassoSelect) this._lassoSelect.start({ x: event.x, y: event.y }, this.isMultiSelect);
215
+ }
216
+
217
+ updateLassoSelect(event) {
218
+ if (this._lassoSelect) this._lassoSelect.update({ x: event.x, y: event.y });
219
+ }
220
+
221
+ endLassoSelect() {
222
+ this.isLassoSelect = false;
223
+ if (this._lassoSelect) this._lassoSelect.end();
224
+ }
209
225
  rectIntersectsRect(a, b) {
210
226
  return !(
211
227
  b.x > a.x + a.width ||
@@ -346,6 +362,14 @@ export class SelectTool extends BaseTool {
346
362
  return closeMindmapEditorViaController.call(this, commit);
347
363
  }
348
364
 
365
+ _openFrameTitleEditor(object, create = false) {
366
+ return openFrameTitleEditorViaController.call(this, object, create);
367
+ }
368
+
369
+ _closeFrameTitleEditor(commit) {
370
+ return closeFrameTitleEditorViaController.call(this, commit);
371
+ }
372
+
349
373
  destroy() {
350
374
  return destroySelectTool.call(this, () => super.destroy());
351
375
  }
@@ -39,7 +39,12 @@ export class TextTool extends BaseTool {
39
39
  color: '#000000',
40
40
  textAlign: 'left',
41
41
  fontWeight: 'normal',
42
- fontStyle: 'normal'
42
+ fontStyle: 'normal',
43
+ bold: false,
44
+ italic: false,
45
+ underline: false,
46
+ strikethrough: false,
47
+ listType: 'none',
43
48
  };
44
49
  }
45
50
 
@@ -11,12 +11,21 @@ import {
11
11
  const DRAG_THRESHOLD = 4;
12
12
  /** Порог «у кромки» в CSS-пикселях. */
13
13
  const EDGE_THRESHOLD_CSS = 10;
14
+ /** Порог магнита к коннектору цели — строго больше EDGE_THRESHOLD_CSS, иначе грань перехватит. */
15
+ const ANCHOR_SNAP_CSS = 16;
14
16
  /** Радиус поиска ближайшего объекта при клике по якорю (world-px). */
15
17
  const CLICK_FIND_RADIUS = 400;
16
18
  /** Зазор между дубликатом и источником при автосоздании (world-px). */
17
19
  const CLONE_GAP = 40;
18
20
  /** Типы объектов, к которым можно привязать коннектор (из ConnectionAnchorsLayer). */
19
21
  const ALLOWED_BIND_TYPES = new Set(['shape', 'note', 'image', 'text', 'simple-text', 'file']);
22
+ /** Нормализованные якоря коннекторов: top, right, bottom, left. */
23
+ const TARGET_ANCHORS = [
24
+ { x: 0.5, y: 0 },
25
+ { x: 1, y: 0.5 },
26
+ { x: 0.5, y: 1 },
27
+ { x: 0, y: 0.5 },
28
+ ];
20
29
 
21
30
  /**
22
31
  * Обрабатывает жест «pointerdown на точке подключения → drag → drop»
@@ -92,6 +101,23 @@ export class ConnectorDragController {
92
101
  return { x: posData.position.x, y: posData.position.y, ...sizeData.size };
93
102
  }
94
103
 
104
+ /**
105
+ * Возвращает ближайший якорь из TARGET_ANCHORS в пределах ANCHOR_SNAP_CSS от worldPt,
106
+ * иначе null. Приоритет выше грани — вызывать в _resolveEnd первым.
107
+ */
108
+ _snapToAnchor(bounds, worldPt) {
109
+ const scale = this._world()?.scale?.x || 1;
110
+ const thr = ANCHOR_SNAP_CSS / scale;
111
+ let best = null, bestDist = thr;
112
+ for (const a of TARGET_ANCHORS) {
113
+ const ax = bounds.x + a.x * bounds.width;
114
+ const ay = bounds.y + a.y * bounds.height;
115
+ const d = Math.hypot(worldPt.x - ax, worldPt.y - ay);
116
+ if (d <= bestDist) { bestDist = d; best = a; }
117
+ }
118
+ return best;
119
+ }
120
+
95
121
  /** Возвращает true, если worldPt находится в пределах EDGE_THRESHOLD_CSS от кромки. */
96
122
  _nearEdge(bounds, worldPt) {
97
123
  const scale = this._world()?.scale?.x || 1;
@@ -116,6 +142,12 @@ export class ConnectorDragController {
116
142
  if (objectId && objectId !== sourceBoundId) {
117
143
  const bounds = this._objectBounds(objectId);
118
144
  if (bounds) {
145
+ // ПРИОРИТЕТ 1: магнит к коннектору (середина грани)
146
+ const snapAnchor = this._snapToAnchor(bounds, worldPt);
147
+ if (snapAnchor) {
148
+ return { boundId: objectId, anchor: snapAnchor, isPrecise: true, isExact: false };
149
+ }
150
+ // ПРИОРИТЕТ 2: произвольная точка грани
119
151
  if (this._nearEdge(bounds, worldPt)) {
120
152
  return {
121
153
  boundId: objectId,
@@ -124,6 +156,7 @@ export class ConnectorDragController {
124
156
  isExact: false,
125
157
  };
126
158
  }
159
+ // ПРИОРИТЕТ 3: центр объекта
127
160
  return { boundId: objectId, anchor: { x: 0.5, y: 0.5 }, isPrecise: false, isExact: false };
128
161
  }
129
162
  }
@@ -150,19 +183,33 @@ export class ConnectorDragController {
150
183
 
151
184
  if (!this._previewGraphics) return;
152
185
 
153
- const worldPt = this._toWorld(e.clientX, e.clientY);
154
- const fromPt = terminalWorldPoint(this.eventBus, this._sourceTerminal);
155
- drawPreview(this._previewGraphics, fromPt, worldPt);
186
+ const worldPt = this._toWorld(e.clientX, e.clientY);
187
+ const fromPt = terminalWorldPoint(this.eventBus, this._sourceTerminal);
156
188
 
157
189
  this._highlightGraphics.clear();
158
190
  const objectId = this._hitTest(e.clientX, e.clientY);
191
+ let previewEnd = worldPt;
159
192
  if (objectId && objectId !== this._sourceTerminal?.boundId) {
160
193
  const bounds = this._objectBounds(objectId);
161
194
  if (bounds) {
162
195
  this._highlightGraphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.85 });
163
196
  this._highlightGraphics.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
197
+ // Подводим превью к коннектору, если курсор в зоне магнита
198
+ const snapAnchor = this._snapToAnchor(bounds, worldPt);
199
+ if (snapAnchor) {
200
+ previewEnd = {
201
+ x: bounds.x + snapAnchor.x * bounds.width,
202
+ y: bounds.y + snapAnchor.y * bounds.height,
203
+ };
204
+ // Подсвечиваем конкретный коннектор
205
+ this._highlightGraphics.lineStyle(0);
206
+ this._highlightGraphics.beginFill(0x2563EB, 1);
207
+ this._highlightGraphics.drawCircle(previewEnd.x, previewEnd.y, 6);
208
+ this._highlightGraphics.endFill();
209
+ }
164
210
  }
165
211
  }
212
+ drawPreview(this._previewGraphics, fromPt, previewEnd);
166
213
  }
167
214
 
168
215
  _onUp(e) {
@@ -1,5 +1,6 @@
1
1
  import * as PIXI from 'pixi.js';
2
2
  import { Events } from '../../../core/events/Events.js';
3
+ import { buildPath } from '../../../services/ConnectorRouter.js';
3
4
 
4
5
  /**
5
6
  * Переиспользуемые хелперы жеста коннектора.
@@ -53,33 +54,46 @@ export function computeAnchor(eventBus, objectId, worldPt) {
53
54
  }
54
55
 
55
56
  /**
56
- * Рисует превью линии со стрелкой в PIXI-графику (PIXI 7 API).
57
+ * Рисует превью коннектора со стрелкой в PIXI-графику (PIXI 7 API).
58
+ * Маршрут совпадает с тем, что будет создан при отпускании (по умолчанию 'elbow'),
59
+ * поэтому «резинка» во время перетаскивания выглядит как итоговый коннектор.
57
60
  * graphics — PIXI.Graphics, уже добавленный в worldLayer.
61
+ *
62
+ * @param {string} route 'straight'|'elbow'|'bezier' — должен совпадать с дефолтом createConnectorFromTerminals
58
63
  */
59
- export function drawPreview(graphics, fromWorldPt, toWorldPt) {
64
+ export function drawPreview(graphics, fromWorldPt, toWorldPt, route = 'elbow') {
60
65
  graphics.clear();
61
66
 
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);
67
+ // Без привязанной грани dir-векторы неизвестны buildPath даёт H-V-H/V-H-V излом
68
+ const pts = buildPath(fromWorldPt, toWorldPt, route);
69
+ if (pts.length < 2) return;
65
70
 
66
- const dx = toWorldPt.x - fromWorldPt.x;
67
- const dy = toWorldPt.y - fromWorldPt.y;
71
+ graphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.7, cap: 'round', join: 'round' });
72
+ graphics.moveTo(pts[0].x, pts[0].y);
73
+ for (let i = 1; i < pts.length; i++) {
74
+ graphics.lineTo(pts[i].x, pts[i].y);
75
+ }
76
+
77
+ // Наконечник по направлению последнего сегмента — открытый chevron,
78
+ // как у финального коннектора (ConnectorLayer.drawHead, kind='arrow').
79
+ const tip = pts[pts.length - 1];
80
+ const prev = pts[pts.length - 2];
81
+ const dx = tip.x - prev.x;
82
+ const dy = tip.y - prev.y;
68
83
  const len = Math.hypot(dx, dy);
69
84
  if (len > 10) {
70
85
  const ux = dx / len;
71
86
  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();
87
+ const px = -uy;
88
+ const py = ux;
89
+ const aLen = 12;
90
+ const aHalf = 5;
91
+ const bx = tip.x - ux * aLen;
92
+ const by = tip.y - uy * aLen;
93
+ graphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.7, cap: 'round', join: 'round' });
94
+ graphics.moveTo(bx + px * aHalf, by + py * aHalf);
95
+ graphics.lineTo(tip.x, tip.y);
96
+ graphics.lineTo(bx - px * aHalf, by - py * aHalf);
83
97
  }
84
98
  }
85
99
 
@@ -102,7 +116,7 @@ export function createConnectorFromTerminals(core, eventBus, sourceTerminal, end
102
116
  width: 2,
103
117
  dash: false,
104
118
  head: { start: false, end: true },
105
- route: 'straight',
119
+ route: 'elbow',
106
120
  },
107
121
  });
108
122
  }