@sequent-org/moodboard 1.4.32 → 1.4.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/package.json +5 -1
  2. package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
  3. package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
  4. package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  5. package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  6. package/src/assets/icons/attachments.svg +3 -1
  7. package/src/assets/icons/comments.svg +2 -2
  8. package/src/assets/icons/connector.svg +6 -0
  9. package/src/assets/icons/emoji.svg +6 -1
  10. package/src/assets/icons/frame.svg +4 -1
  11. package/src/assets/icons/image.svg +5 -1
  12. package/src/assets/icons/laser.svg +1 -0
  13. package/src/assets/icons/lasso.svg +5 -0
  14. package/src/assets/icons/mindmap.svg +10 -2
  15. package/src/assets/icons/note.svg +4 -1
  16. package/src/assets/icons/pan.svg +5 -2
  17. package/src/assets/icons/pencil.svg +4 -1
  18. package/src/assets/icons/reactions.svg +5 -0
  19. package/src/assets/icons/redo.svg +3 -2
  20. package/src/assets/icons/select.svg +2 -8
  21. package/src/assets/icons/shapes.svg +5 -1
  22. package/src/assets/icons/text-add.svg +15 -1
  23. package/src/assets/icons/undo.svg +3 -2
  24. package/src/assets/reactions/1f44d.svg +20 -0
  25. package/src/assets/reactions/1f44e.svg +20 -0
  26. package/src/assets/reactions/2705.svg +20 -0
  27. package/src/assets/reactions/274c.svg +19 -0
  28. package/src/assets/reactions/2753.svg +20 -0
  29. package/src/assets/reactions/2764.svg +22 -0
  30. package/src/assets/reactions/2b50.svg +19 -0
  31. package/src/assets/reactions/plus-one.svg +25 -0
  32. package/src/core/PixiEngine.js +23 -0
  33. package/src/core/bootstrap/CoreInitializer.js +43 -0
  34. package/src/core/commands/GroupDeleteCommand.js +13 -1
  35. package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
  36. package/src/core/commands/UpdateTextStyleCommand.js +17 -6
  37. package/src/core/commands/index.js +3 -0
  38. package/src/core/events/Events.js +22 -0
  39. package/src/core/flows/LayerAndViewportFlow.js +1 -0
  40. package/src/core/flows/ObjectLifecycleFlow.js +155 -7
  41. package/src/core/index.js +28 -1
  42. package/src/grid/CrossGridZoomPhases.js +3 -3
  43. package/src/initNoBundler.js +1 -1
  44. package/src/moodboard/DataManager.js +28 -0
  45. package/src/moodboard/MoodBoard.js +27 -0
  46. package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
  47. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
  48. package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
  49. package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
  50. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
  51. package/src/objects/ConnectorObject.js +2 -2
  52. package/src/objects/FrameObject.js +119 -59
  53. package/src/objects/ShapeObject.js +49 -74
  54. package/src/objects/shape/ShapeDrawer.js +210 -0
  55. package/src/services/ConnectorBindingResolver.js +112 -0
  56. package/src/services/ConnectorRouter.js +210 -0
  57. package/src/services/comments/CommentService.js +344 -0
  58. package/src/tools/object-tools/CommentTool.js +85 -0
  59. package/src/tools/object-tools/DrawingTool.js +110 -10
  60. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  61. package/src/tools/object-tools/SelectTool.js +25 -1
  62. package/src/tools/object-tools/TextTool.js +6 -1
  63. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  64. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  65. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  66. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  67. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  68. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  69. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  70. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  71. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  72. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  73. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  74. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  75. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  76. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  77. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  78. package/src/ui/CommentPopover.js +6 -0
  79. package/src/ui/CommentsBar.js +91 -0
  80. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  81. package/src/ui/ContextMenu.js +25 -0
  82. package/src/ui/DrawingPropertiesPanel.js +362 -0
  83. package/src/ui/FilePropertiesPanel.js +5 -0
  84. package/src/ui/FramePropertiesPanel.js +5 -0
  85. package/src/ui/HtmlTextLayer.js +246 -66
  86. package/src/ui/NotePropertiesPanel.js +6 -0
  87. package/src/ui/ShapePropertiesPanel.js +307 -0
  88. package/src/ui/TextPropertiesPanel.js +100 -1
  89. package/src/ui/Toolbar.js +25 -2
  90. package/src/ui/Topbar.js +2 -2
  91. package/src/ui/animation/HoverLiftController.js +6 -7
  92. package/src/ui/chat/ChatComposer.js +58 -7
  93. package/src/ui/chat/ChatWindow.js +60 -143
  94. package/src/ui/comments/CommentListPanel.js +213 -0
  95. package/src/ui/comments/CommentPinLayer.js +448 -0
  96. package/src/ui/comments/CommentThreadPopover.js +539 -0
  97. package/src/ui/comments/commentFormat.js +32 -0
  98. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  99. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  100. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  103. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  104. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  105. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  106. package/src/ui/connectors/ConnectorLayer.js +264 -57
  107. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  108. package/src/ui/handles/HandlesEventBridge.js +1 -0
  109. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  110. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  111. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  112. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  113. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  114. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  115. package/src/ui/styles/chat.css +709 -19
  116. package/src/ui/styles/index.css +1 -0
  117. package/src/ui/styles/panels.css +112 -2
  118. package/src/ui/styles/shape-properties-panel.css +250 -0
  119. package/src/ui/styles/toolbar.css +7 -2
  120. package/src/ui/styles/topbar.css +1 -1
  121. package/src/ui/styles/workspace.css +257 -6
  122. package/src/ui/text-properties/TextFormatControls.js +88 -0
  123. package/src/ui/text-properties/TextListRenderer.js +137 -0
  124. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  125. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  126. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  127. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  128. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  129. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  130. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  131. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  132. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  133. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  134. package/src/utils/iconLoader.js +17 -16
  135. package/src/utils/markdown.js +14 -0
  136. package/src/utils/richText.js +125 -0
@@ -1,19 +1,60 @@
1
1
  import * as PIXI from 'pixi.js';
2
2
  import { Events } from '../../core/events/Events.js';
3
3
  import { ConnectorBindingResolver, distanceToSegment } from '../../services/ConnectorBindingResolver.js';
4
+ import { buildPath, bezierControlPoints, sampleBezier, BEZIER_SAMPLES } from '../../services/ConnectorRouter.js';
4
5
 
5
6
  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;
7
+ const ARROW_LEN = 12;
8
+ const ARROW_HALF = 5;
9
+ const DASH_LEN = 8;
10
+ const GAP_LEN = 5;
11
+ const ELBOW_RADIUS = 8;
12
+ const CIRCLE_R = 4;
13
+ const DIAMOND_HALF = 5;
14
+
15
+ /** Сколько пикселей отступить от кончика маркера, чтобы линия не заходила внутрь него. */
16
+ function getHeadSetback(kind) {
17
+ if (kind === 'arrow') return ARROW_LEN;
18
+ if (kind === 'triangle') return ARROW_LEN;
19
+ if (kind === 'circle') return CIRCLE_R * 2;
20
+ if (kind === 'diamond') return DIAMOND_HALF * 2;
21
+ return 0;
22
+ }
10
23
 
11
24
  function asArray(value) {
12
25
  return Array.isArray(value) ? value : [];
13
26
  }
14
27
 
28
+ /** Возвращает центр объекта (world) или свободную точку терминала. */
29
+ function getObjectCenter(terminal, target) {
30
+ if (!terminal?.boundId) {
31
+ return { x: terminal?.point?.x ?? 0, y: terminal?.point?.y ?? 0 };
32
+ }
33
+ if (!target) return { x: 0, y: 0 };
34
+ const left = target.position?.x ?? 0;
35
+ const top = target.position?.y ?? 0;
36
+ const w = target.width ?? target.properties?.width ?? 0;
37
+ const h = target.height ?? target.properties?.height ?? 0;
38
+ return { x: left + w / 2, y: top + h / 2 };
39
+ }
40
+
41
+ /** Нормализует HeadKind: boolean (обратная совместимость) → строка. */
42
+ function normalizeHeadKind(value) {
43
+ if (value === true) return 'arrow';
44
+ if (value === false) return 'none';
45
+ return typeof value === 'string' ? value : 'none';
46
+ }
47
+
48
+ function normalizeHead(head) {
49
+ if (!head) return { start: 'none', end: 'arrow' };
50
+ return {
51
+ start: normalizeHeadKind(head.start),
52
+ end: normalizeHeadKind(head.end),
53
+ };
54
+ }
55
+
15
56
  /**
16
- * Рисует пунктирную линию через последовательность moveTo/lineTo.
57
+ * Пунктирная линия по одному отрезку.
17
58
  * @param {PIXI.Graphics} g
18
59
  */
19
60
  function drawDashedLine(g, x1, y1, x2, y2) {
@@ -42,39 +83,141 @@ function drawDashedLine(g, x1, y1, x2, y2) {
42
83
  }
43
84
 
44
85
  /**
45
- * Рисует треугольный наконечник стрелки в точке `to`, направление fromto.
46
- * Вызывать после g.lineStyle(0) не нужно — сбрасывает линию сам.
86
+ * Рисует наконечник стрелки в tipPt, направление fromPttipPt.
87
+ *
47
88
  * @param {PIXI.Graphics} g
48
- * @param {{ x: number, y: number }} from
49
- * @param {{ x: number, y: number }} to
50
- * @param {number} color PIXI-цвет
89
+ * @param {{ x:number, y:number }} fromPt предпоследняя точка
90
+ * @param {{ x:number, y:number }} tipPt кончик
91
+ * @param {number} color PIXI-цвет
92
+ * @param {string} kind HeadKind: 'none'|'arrow'|'triangle'|'circle'|'diamond'
93
+ * @param {number} lineWidth толщина линии коннектора (для согласованной толщины наконечника)
51
94
  */
52
- function drawArrow(g, from, to, color) {
53
- const dx = to.x - from.x;
54
- const dy = to.y - from.y;
95
+ function drawHead(g, fromPt, tipPt, color, kind, lineWidth = 2) {
96
+ if (kind === 'none') return;
97
+ const dx = tipPt.x - fromPt.x;
98
+ const dy = tipPt.y - fromPt.y;
55
99
  const len = Math.hypot(dx, dy);
56
100
  if (len < 1e-6) return;
57
101
  const ux = dx / len;
58
102
  const uy = dy / len;
59
- // перпендикуляр
60
103
  const px = -uy;
61
104
  const py = ux;
62
- const bx = to.x - ux * ARROW_LEN;
63
- const by = to.y - uy * ARROW_LEN;
105
+
64
106
  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();
107
+
108
+ if (kind === 'arrow') {
109
+ // Единый штрих крыло→кончик→крыло: round-join даёт чистый острый кончик,
110
+ // round-cap аккуратные концы крыльев. Толщина = толщине линии.
111
+ const bx = tipPt.x - ux * ARROW_LEN;
112
+ const by = tipPt.y - uy * ARROW_LEN;
113
+ const w = Math.max(2, lineWidth + 0.5);
114
+ try {
115
+ g.lineStyle({ width: w, color, alpha: 1, cap: 'round', join: 'round' });
116
+ } catch (_) {
117
+ g.lineStyle(w, color, 1);
118
+ }
119
+ g.moveTo(Math.round(bx + px * ARROW_HALF), Math.round(by + py * ARROW_HALF));
120
+ g.lineTo(Math.round(tipPt.x), Math.round(tipPt.y));
121
+ g.lineTo(Math.round(bx - px * ARROW_HALF), Math.round(by - py * ARROW_HALF));
122
+ g.lineStyle(0);
123
+ } else if (kind === 'triangle') {
124
+ const bx = tipPt.x - ux * ARROW_LEN;
125
+ const by = tipPt.y - uy * ARROW_LEN;
126
+ g.beginFill(color, 1);
127
+ g.drawPolygon([
128
+ Math.round(tipPt.x), Math.round(tipPt.y),
129
+ Math.round(bx + px * ARROW_HALF), Math.round(by + py * ARROW_HALF),
130
+ Math.round(bx - px * ARROW_HALF), Math.round(by - py * ARROW_HALF),
131
+ ]);
132
+ g.endFill();
133
+ } else if (kind === 'circle') {
134
+ const cx = Math.round(tipPt.x - ux * CIRCLE_R);
135
+ const cy = Math.round(tipPt.y - uy * CIRCLE_R);
136
+ g.beginFill(color, 1);
137
+ g.drawCircle(cx, cy, CIRCLE_R);
138
+ g.endFill();
139
+ } else if (kind === 'diamond') {
140
+ // Ромб: вершина в tipPt, тыл на расстоянии 2×DIAMOND_HALF
141
+ const mx = tipPt.x - ux * DIAMOND_HALF;
142
+ const my = tipPt.y - uy * DIAMOND_HALF;
143
+ g.beginFill(color, 1);
144
+ g.drawPolygon([
145
+ Math.round(tipPt.x), Math.round(tipPt.y),
146
+ Math.round(mx + px * DIAMOND_HALF), Math.round(my + py * DIAMOND_HALF),
147
+ Math.round(tipPt.x - ux * 2 * DIAMOND_HALF), Math.round(tipPt.y - uy * 2 * DIAMOND_HALF),
148
+ Math.round(mx - px * DIAMOND_HALF), Math.round(my - py * DIAMOND_HALF),
149
+ ]);
150
+ g.endFill();
151
+ }
152
+ }
153
+
154
+
155
+ /**
156
+ * Сплошная ломаная; для elbow скругляет углы дугой quadraticCurveTo.
157
+ *
158
+ * @param {PIXI.Graphics} g
159
+ * @param {Array<{x:number,y:number}>} pts
160
+ * @param {boolean} isElbow включить скругление углов
161
+ */
162
+ function drawPolylineSolid(g, pts, isElbow) {
163
+ if (pts.length < 2) return;
164
+ g.moveTo(pts[0].x, pts[0].y);
165
+ for (let i = 1; i < pts.length - 1; i++) {
166
+ const prev = pts[i - 1];
167
+ const curr = pts[i];
168
+ const next = pts[i + 1];
169
+ if (!isElbow) {
170
+ g.lineTo(curr.x, curr.y);
171
+ continue;
172
+ }
173
+ const dxIn = curr.x - prev.x;
174
+ const dyIn = curr.y - prev.y;
175
+ const lenIn = Math.hypot(dxIn, dyIn);
176
+ const dxOut = next.x - curr.x;
177
+ const dyOut = next.y - curr.y;
178
+ const lenOut = Math.hypot(dxOut, dyOut);
179
+ if (lenIn < 1e-6 || lenOut < 1e-6) {
180
+ g.lineTo(curr.x, curr.y);
181
+ continue;
182
+ }
183
+ const r = Math.min(ELBOW_RADIUS, lenIn / 2, lenOut / 2);
184
+ const iux = dxIn / lenIn;
185
+ const iuy = dyIn / lenIn;
186
+ const oux = dxOut / lenOut;
187
+ const ouy = dyOut / lenOut;
188
+ g.lineTo(Math.round(curr.x - iux * r), Math.round(curr.y - iuy * r));
189
+ g.quadraticCurveTo(curr.x, curr.y,
190
+ Math.round(curr.x + oux * r), Math.round(curr.y + ouy * r));
191
+ }
192
+ g.lineTo(pts[pts.length - 1].x, pts[pts.length - 1].y);
193
+ }
194
+
195
+ /** Пунктирная ломаная по массиву точек. */
196
+ function drawPolylineDash(g, pts) {
197
+ for (let i = 1; i < pts.length; i++) {
198
+ drawDashedLine(g, pts[i - 1].x, pts[i - 1].y, pts[i].x, pts[i].y);
199
+ }
200
+ }
201
+
202
+ /** Рисует кубическую кривую Безье через bezierCurveTo (или пунктирную аппроксимацию). */
203
+ function drawBezierCurve(g, start, end, isDash, startDir = null, endDir = null) {
204
+ const { cp1, cp2 } = bezierControlPoints(start, end, startDir, endDir);
205
+ if (isDash) {
206
+ const pts = [];
207
+ for (let i = 0; i <= BEZIER_SAMPLES; i++) {
208
+ pts.push(sampleBezier(start, cp1, cp2, end, i / BEZIER_SAMPLES));
209
+ }
210
+ drawPolylineDash(g, pts);
211
+ return;
212
+ }
213
+ g.moveTo(start.x, start.y);
214
+ g.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
72
215
  }
73
216
 
74
217
  /**
75
218
  * ConnectorLayer — слой рендера универсальных коннекторов.
76
219
  *
77
- * Паттерн: MindmapConnectionLayer (один PIXI.Graphics, полная перерисовка на события).
220
+ * Паттерн: один PIXI.Graphics, полная перерисовка на события.
78
221
  * Рисует connector-объекты из state.objects в worldLayer.
79
222
  * Резолвинг end-point: ConnectorBindingResolver.resolve() двумя проходами
80
223
  * (грубый → точный) для корректной проекции на кромку при isExact=false.
@@ -90,7 +233,7 @@ export class ConnectorLayer {
90
233
  this.graphics = null;
91
234
  this.subscriptions = [];
92
235
  this._eventsAttached = false;
93
- /** @type {Array<{ id: string, start: {x,y}, end: {x,y} }>} */
236
+ /** @type {Array<{ id: string, points: Array<{x:number,y:number}> }>} */
94
237
  this._lastSegments = [];
95
238
  }
96
239
 
@@ -132,6 +275,7 @@ export class ConnectorLayer {
132
275
  [Events.Tool.GroupResizeUpdate, () => this.updateAll()],
133
276
  [Events.Tool.RotateUpdate, () => this.updateAll()],
134
277
  [Events.Tool.PanUpdate, () => this.updateAll()],
278
+ [Events.Viewport.Changed, () => this.updateAll()],
135
279
  [Events.UI.ZoomPercent, () => this.updateAll()],
136
280
  [Events.History.Changed, () => this.updateAll()],
137
281
  [Events.Board.Loaded, () => this.updateAll()],
@@ -166,7 +310,7 @@ export class ConnectorLayer {
166
310
  }
167
311
 
168
312
  if (!this.graphics) {
169
- this.graphics = new PIXI.Graphics();
313
+ this.graphics = new PIXI.Graphics();
170
314
  this.graphics.name = 'connector-layer';
171
315
  this.graphics.zIndex = 3;
172
316
  const world = this.core?.pixi?.worldLayer || this.core?.pixi?.app?.stage;
@@ -179,71 +323,134 @@ export class ConnectorLayer {
179
323
  this._lastSegments = [];
180
324
 
181
325
  connectors.forEach((connector) => {
182
- const style = connector?.properties?.style ?? {};
183
- const startTerm = connector?.properties?.start;
184
- const endTerm = connector?.properties?.end;
326
+ const style = connector?.properties?.style ?? {};
327
+ const startTerm = connector?.properties?.start;
328
+ const endTerm = connector?.properties?.end;
185
329
  if (!startTerm || !endTerm) return;
186
330
 
187
331
  const startTarget = startTerm.boundId ? (byId.get(startTerm.boundId) ?? null) : null;
188
332
  const endTarget = endTerm.boundId ? (byId.get(endTerm.boundId) ?? null) : null;
189
333
 
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
334
  const color = style.stroke ?? 0x2563EB;
204
335
  const width = style.width ?? 2;
205
336
  const isDash = !!style.dash;
206
- const head = style.head ?? { start: false, end: true };
337
+ const route = style.route ?? 'straight';
338
+ const head = normalizeHead(style.head);
339
+
340
+ let sx, sy, ex, ey, startDir = null, endDir = null;
341
+
342
+ if (route === 'elbow' || route === 'bezier') {
343
+ // Для elbow/bezier — резолв через грань с перпендикулярным выходом
344
+ const startCenter = getObjectCenter(startTerm, startTarget);
345
+ const endCenter = getObjectCenter(endTerm, endTarget);
346
+ const startDesc = ConnectorBindingResolver.resolveWithSide(startTerm, startTarget, endCenter);
347
+ const endDesc = ConnectorBindingResolver.resolveWithSide(endTerm, endTarget, startCenter);
348
+ sx = Math.round(startDesc.point.x);
349
+ sy = Math.round(startDesc.point.y);
350
+ ex = Math.round(endDesc.point.x);
351
+ ey = Math.round(endDesc.point.y);
352
+ startDir = startDesc.dir;
353
+ endDir = endDesc.dir;
354
+ } else {
355
+ // straight: двухпроходный резолв (кромочная проекция по лучу центр-центр)
356
+ const roughStart = ConnectorBindingResolver.resolve(startTerm, startTarget, null);
357
+ const roughEnd = ConnectorBindingResolver.resolve(endTerm, endTarget, null);
358
+ const start = ConnectorBindingResolver.resolve(startTerm, startTarget, roughEnd);
359
+ const end = ConnectorBindingResolver.resolve(endTerm, endTarget, start);
360
+ sx = Math.round(start.x);
361
+ sy = Math.round(start.y);
362
+ ex = Math.round(end.x);
363
+ ey = Math.round(end.y);
364
+ }
207
365
 
208
366
  try {
209
- g.lineStyle({ width, color, alpha: 1, alignment: 0, cap: 'round', join: 'round' });
367
+ g.lineStyle({ width, color, alpha: 1, alignment: 0.5, cap: 'round', join: 'round' });
210
368
  } catch (_) {
211
- g.lineStyle(width, color, 1, 0);
369
+ g.lineStyle(width, color, 1, 0.5);
370
+ }
371
+
372
+ const pts = buildPath({ x: sx, y: sy }, { x: ex, y: ey }, route, startDir, endDir);
373
+
374
+ // Для рисования линии: укорачиваем концы ровно до основания маркера,
375
+ // чтобы толстый stroke не заходил внутрь наконечника.
376
+ // Оригинальный pts используется только для drawHead (кончик остаётся точным).
377
+ const drawPts = pts.slice();
378
+ if (drawPts.length >= 2) {
379
+ if (head.end !== 'none') {
380
+ const n = drawPts.length;
381
+ const tp = drawPts[n - 1];
382
+ const fp = drawPts[n - 2];
383
+ const sb = getHeadSetback(head.end);
384
+ const dx = tp.x - fp.x;
385
+ const dy = tp.y - fp.y;
386
+ const len = Math.hypot(dx, dy);
387
+ if (sb > 0 && len > sb) {
388
+ drawPts[n - 1] = {
389
+ x: Math.round(tp.x - (dx / len) * sb),
390
+ y: Math.round(tp.y - (dy / len) * sb),
391
+ };
392
+ }
393
+ }
394
+ if (head.start !== 'none') {
395
+ const tp = drawPts[0];
396
+ const fp = drawPts[1];
397
+ const sb = getHeadSetback(head.start);
398
+ const dx = tp.x - fp.x;
399
+ const dy = tp.y - fp.y;
400
+ const len = Math.hypot(dx, dy);
401
+ if (sb > 0 && len > sb) {
402
+ drawPts[0] = {
403
+ x: Math.round(tp.x - (dx / len) * sb),
404
+ y: Math.round(tp.y - (dy / len) * sb),
405
+ };
406
+ }
407
+ }
212
408
  }
213
409
 
214
- if (isDash) {
215
- drawDashedLine(g, sx, sy, ex, ey);
410
+ const drawStart = drawPts[0];
411
+ const drawEnd = drawPts[drawPts.length - 1];
412
+
413
+ if (route === 'bezier') {
414
+ drawBezierCurve(g, drawStart, drawEnd, isDash, startDir, endDir);
415
+ } else if (isDash) {
416
+ drawPolylineDash(g, drawPts);
216
417
  } else {
217
- g.moveTo(sx, sy);
218
- g.lineTo(ex, ey);
418
+ drawPolylineSolid(g, drawPts, route === 'elbow');
219
419
  }
220
420
 
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);
421
+ if (head.end !== 'none' && pts.length >= 2) {
422
+ drawHead(g, pts[pts.length - 2], pts[pts.length - 1], color, head.end, width);
423
+ }
424
+ if (head.start !== 'none' && pts.length >= 2) {
425
+ drawHead(g, pts[1], pts[0], color, head.start, width);
426
+ }
223
427
 
224
- this._lastSegments.push({ id: connector.id, start: { x: sx, y: sy }, end: { x: ex, y: ey } });
428
+ this._lastSegments.push({ id: connector.id, points: pts });
225
429
  });
226
430
  }
227
431
 
228
432
  /**
229
433
  * Возвращает id ближайшего коннектора, если worldPoint в пределах порога.
230
434
  * Порог задан в экранных пикселях, пересчитывается в world через текущий scale.
435
+ * Проверяет каждую пару соседних точек сохранённого пути (ломаная/аппроксимация).
231
436
  *
232
437
  * @param {{ x: number, y: number }} worldPoint
233
438
  * @returns {string|null}
234
439
  */
235
440
  hitTest(worldPoint) {
236
441
  if (this._lastSegments.length === 0) return null;
237
- // worldLayer.scale.x = zoom; 1 screen px = 1/scale world units
238
442
  const scale = this.core?.pixi?.worldLayer?.scale?.x ?? 1;
239
443
  const worldThreshold = HIT_TEST_SCREEN_PX / scale;
240
444
  let closest = null;
241
445
  let minDist = worldThreshold;
242
446
  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;
447
+ const pts = seg.points;
448
+ for (let i = 1; i < pts.length; i++) {
449
+ const d = distanceToSegment(worldPoint, pts[i - 1], pts[i]);
450
+ if (d < minDist) {
451
+ minDist = d;
452
+ closest = seg.id;
453
+ }
247
454
  }
248
455
  }
249
456
  return closest;
@@ -68,13 +68,9 @@ function relayoutMindmapBranchLevel({ core, eventBus, parentId, side }) {
68
68
  });
69
69
  if (siblings.length === 0) return;
70
70
 
71
- const app = core?.pixi?.app;
72
- const worldLayer = core?.pixi?.worldLayer || app?.stage;
73
- const rendererRes = app?.renderer?.resolution || 1;
74
- const worldScale = worldLayer?.scale?.x || 1;
75
- const baseGapWorld = Math.max(1, Math.round((10 * rendererRes) / worldScale));
76
- const gapWorld = Math.max(1, Math.round(baseGapWorld * MINDMAP_CHILD_GAP_MULTIPLIER));
77
- const verticalGapWorld = Math.max(1, Math.round(baseGapWorld * MINDMAP_CHILD_VERTICAL_GAP_MULTIPLIER));
71
+ const baseGapWorld = 10;
72
+ const gapWorld = baseGapWorld * MINDMAP_CHILD_GAP_MULTIPLIER;
73
+ const verticalGapWorld = Math.max(1, baseGapWorld * MINDMAP_CHILD_VERTICAL_GAP_MULTIPLIER);
78
74
 
79
75
  const byParentBySide = new Map();
80
76
  const childrenByParent = new Map();
@@ -1349,12 +1345,8 @@ export class HandlesDomRenderer {
1349
1345
 
1350
1346
  if (isMindmapTarget) {
1351
1347
  const emitChildMindmapFromSource = (direction) => {
1352
- const app = this.host.core?.pixi?.app;
1353
- const worldLayer = this.host.core?.pixi?.worldLayer || app?.stage;
1354
- const rendererRes = app?.renderer?.resolution || 1;
1355
- const worldScale = worldLayer?.scale?.x || 1;
1356
- const baseGapWorld = Math.max(1, Math.round((10 * rendererRes) / worldScale));
1357
- const gapWorld = Math.max(1, Math.round(baseGapWorld * MINDMAP_CHILD_GAP_MULTIPLIER));
1348
+ const baseGapWorld = 10;
1349
+ const gapWorld = baseGapWorld * MINDMAP_CHILD_GAP_MULTIPLIER;
1358
1350
  const childWidth = Math.max(1, Math.round(MINDMAP_LAYOUT.width * MINDMAP_CHILD_WIDTH_FACTOR));
1359
1351
  const childHeight = Math.max(1, Math.round(MINDMAP_LAYOUT.height * MINDMAP_CHILD_HEIGHT_FACTOR));
1360
1352
  const childPaddingX = Math.max(1, Math.round(MINDMAP_LAYOUT.paddingX * MINDMAP_CHILD_PADDING_FACTOR));
@@ -113,6 +113,7 @@ export class HandlesEventBridge {
113
113
  }],
114
114
  [Events.UI.ZoomPercent, () => this.host.update()],
115
115
  [Events.Tool.PanUpdate, () => this.host.update()],
116
+ [Events.Viewport.Changed, () => this.host.update()],
116
117
  [Events.History.Changed, (data) => {
117
118
  if (data?.lastUndone || data?.lastRedone) {
118
119
  this.host._endGroupRotationPreview();
@@ -15,6 +15,10 @@ export class SingleSelectionHandlesController {
15
15
  this.host.hide();
16
16
  return;
17
17
  }
18
+ if (mb.type === 'connector') {
19
+ this.host.hide();
20
+ return;
21
+ }
18
22
 
19
23
  const worldBounds = this.host.positioningService.getSingleSelectionWorldBounds(id, pixi);
20
24
  this.host._showBounds(worldBounds, id);
@@ -161,6 +161,7 @@ export class MindmapCollapseLayer {
161
161
  [Events.Tool.GroupDragUpdate, reposition],
162
162
  [Events.Tool.GroupResizeUpdate, reposition],
163
163
  [Events.Tool.PanUpdate, reposition],
164
+ [Events.Viewport.Changed, reposition],
164
165
  [Events.UI.ZoomPercent, reposition],
165
166
  [Events.Object.TransformUpdated, reposition],
166
167
  ];
@@ -182,6 +182,7 @@ export class MindmapConnectionLayer {
182
182
  [Events.Tool.GroupResizeUpdate, () => this.updateAll()],
183
183
  [Events.Tool.RotateUpdate, () => this.updateAll()],
184
184
  [Events.Tool.PanUpdate, () => this.updateAll()],
185
+ [Events.Viewport.Changed, () => this.updateAll()],
185
186
  [Events.UI.ZoomPercent, () => this.updateAll()],
186
187
  [Events.History.Changed, () => this.updateAll()],
187
188
  [Events.Board.Loaded, () => this.updateAll()],
@@ -114,6 +114,8 @@ export class MindmapHtmlTextLayer {
114
114
 
115
115
  this.eventBus.on(Events.UI.ZoomPercent, () => this.updateAll());
116
116
  this.eventBus.on(Events.Tool.PanUpdate, () => this.updateAll());
117
+ this._onViewportChanged = () => this.updateAll();
118
+ this.eventBus.on(Events.Viewport.Changed, this._onViewportChanged);
117
119
  this.eventBus.on(Events.UI.TextEditEnd, ({ objectId }) => {
118
120
  if (objectId && this.idToEl.has(objectId)) this._scheduleAutoFit(objectId);
119
121
  });
@@ -178,6 +180,10 @@ export class MindmapHtmlTextLayer {
178
180
  }
179
181
 
180
182
  destroy() {
183
+ if (this._onViewportChanged && this.eventBus) {
184
+ this.eventBus.off(Events.Viewport.Changed, this._onViewportChanged);
185
+ this._onViewportChanged = null;
186
+ }
181
187
  for (const [id, state] of this._hoverStates) {
182
188
  gsap.killTweensOf(state);
183
189
  this._detachPixiHover(id);