@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.30",
3
+ "version": "1.4.31",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -40,7 +40,9 @@
40
40
  "deploy:prod": "NODE_ENV=production npm run build && NODE_ENV=production npm run start"
41
41
  },
42
42
  "dependencies": {
43
+ "@pixi/filter-drop-shadow": "^5.2.0",
43
44
  "axios": "^1.0.0",
45
+ "gsap": "^3.15.0",
44
46
  "lucide-static": "^1.16.0",
45
47
  "pixi.js": "^7.0.0"
46
48
  },
@@ -2,6 +2,7 @@ import * as PIXI from 'pixi.js';
2
2
  import { ObjectFactory } from '../objects/ObjectFactory.js';
3
3
  import { ObjectRenderer } from './rendering/ObjectRenderer.js';
4
4
  import { Events } from './events/Events.js';
5
+ import { HoverLiftController } from '../ui/animation/HoverLiftController.js';
5
6
 
6
7
  export class PixiEngine {
7
8
  constructor(container, eventBus, options) {
@@ -41,6 +42,9 @@ export class PixiEngine {
41
42
  // Инициализируем ObjectRenderer
42
43
  this.renderer = new ObjectRenderer(this.objects, this.eventBus);
43
44
 
45
+ // Hover-lift анимация для всех объектов
46
+ this.hoverLift = new HoverLiftController(this.eventBus, this.app);
47
+
44
48
  // Поддержка чёткости текстов записок при зуме: подписка на событие зума
45
49
  if (this.eventBus) {
46
50
  const onZoom = ({ percentage }) => {
@@ -53,6 +57,9 @@ export class PixiEngine {
53
57
  if (mb && mb.type === 'note' && mb.instance && typeof mb.instance.updateCrispnessForZoom === 'function') {
54
58
  mb.instance.updateCrispnessForZoom(s, res);
55
59
  }
60
+ if (mb && mb.type === 'mindmap' && mb.instance && typeof mb.instance.redrawForZoom === 'function') {
61
+ mb.instance.redrawForZoom(s);
62
+ }
56
63
  }
57
64
  } catch (e) {
58
65
  console.warn('PixiEngine: zoom crispness update failed', e);
@@ -75,14 +82,17 @@ export class PixiEngine {
75
82
  type: objectData.type,
76
83
  instance: instance // Сохраняем ссылку на сам объект
77
84
  };
78
- // Первичная установка чёткости для записок по текущему масштабу/резолюции
85
+ // Первичная установка чёткости/масштаба по текущему зуму
79
86
  try {
80
- if (pixiObject && pixiObject._mb && pixiObject._mb.type === 'note' && pixiObject._mb.instance && typeof pixiObject._mb.instance.updateCrispnessForZoom === 'function') {
81
- const world = this.worldLayer || this.app.stage;
82
- const s = world?.scale?.x || 1;
83
- const res = this.app?.renderer?.resolution || 1;
87
+ const world = this.worldLayer || this.app.stage;
88
+ const s = world?.scale?.x || 1;
89
+ const res = this.app?.renderer?.resolution || 1;
90
+ if (pixiObject._mb.type === 'note' && pixiObject._mb.instance && typeof pixiObject._mb.instance.updateCrispnessForZoom === 'function') {
84
91
  pixiObject._mb.instance.updateCrispnessForZoom(s, res);
85
92
  }
93
+ if (pixiObject._mb.type === 'mindmap' && pixiObject._mb.instance && typeof pixiObject._mb.instance.redrawForZoom === 'function') {
94
+ pixiObject._mb.instance.redrawForZoom(s);
95
+ }
86
96
  } catch (_) {}
87
97
  } else {
88
98
  console.warn(`Unknown object type: ${objectData.type}`);
@@ -159,6 +169,17 @@ export class PixiEngine {
159
169
  this.worldLayer.addChild(pixiObject);
160
170
  this.objects.set(objectData.id, pixiObject);
161
171
 
172
+ // Hover-lift: подключаем упругую анимацию
173
+ if (this.hoverLift) {
174
+ this.hoverLift.attach(pixiObject, objectData);
175
+ // Если текстура ещё не загружена (ImageObject, EmojiObject),
176
+ // baseScale в attach зафиксирован как 1. Синхронизируем после загрузки.
177
+ const bt = pixiObject.texture?.baseTexture;
178
+ if (bt && !bt.valid) {
179
+ bt.once('loaded', () => { this.hoverLift?.syncBase(pixiObject); });
180
+ }
181
+ }
182
+
162
183
 
163
184
  }
164
185
  }
@@ -222,6 +243,11 @@ export class PixiEngine {
222
243
  }
223
244
  }
224
245
 
246
+ // Отключаем hover-lift перед очисткой listeners
247
+ if (this.hoverLift) {
248
+ this.hoverLift.detach(pixiObject);
249
+ }
250
+
225
251
  // Очищаем все события
226
252
  pixiObject.removeAllListeners();
227
253
 
@@ -229,6 +255,9 @@ export class PixiEngine {
229
255
  pixiObject.destroy({ children: true, texture: false, baseTexture: false });
230
256
  } else {
231
257
  // Для других типов объектов - стандартная очистка
258
+ if (this.hoverLift) {
259
+ this.hoverLift.detach(pixiObject);
260
+ }
232
261
  if (pixiObject.destroy) {
233
262
  pixiObject.destroy({ children: true });
234
263
  }
@@ -55,6 +55,10 @@ export async function initializeCoreTools(core) {
55
55
  const textTool = new textToolModule.TextTool(core.eventBus);
56
56
  core.toolManager.registerTool(textTool);
57
57
 
58
+ const connectorToolModule = await import('../../tools/object-tools/ConnectorTool.js');
59
+ const connectorTool = new connectorToolModule.ConnectorTool(core.eventBus, core);
60
+ core.toolManager.registerTool(connectorTool);
61
+
58
62
  core.selectTool = selectTool;
59
63
  core.toolManager.activateTool('select');
60
64
 
@@ -0,0 +1,25 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+
3
+ /**
4
+ * Команда создания коннектора.
5
+ * execute() добавляет connector-объект через ядро (аналог создания других объектов).
6
+ */
7
+ export class CreateConnectorCommand extends BaseCommand {
8
+ /**
9
+ * @param {Object} core Экземпляр CoreMoodBoard
10
+ * @param {Object} connectorData Полные данные объекта типа 'connector' (включая id)
11
+ */
12
+ constructor(core, connectorData) {
13
+ super('create_connector', 'Создать коннектор');
14
+ this.core = core;
15
+ this.connectorData = connectorData;
16
+ }
17
+
18
+ execute() {
19
+ this.core.createObjectFromData(this.connectorData);
20
+ }
21
+
22
+ undo() {
23
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
24
+ }
25
+ }
@@ -22,7 +22,7 @@ export class GroupMoveCommand extends BaseCommand {
22
22
  for (const item of this.moves) {
23
23
  if (this.coordinatesAreTopLeft) {
24
24
  // Координаты уже левый-верх (Frame перемещение)
25
- this.core.updateObjectPositionDirect(item.id, item.to);
25
+ this.core.updateObjectPositionDirect(item.id, item.to, { snap: false });
26
26
  this.emit(Events.Object.TransformUpdated, {
27
27
  objectId: item.id,
28
28
  type: 'position',
@@ -35,7 +35,7 @@ export class GroupMoveCommand extends BaseCommand {
35
35
  const halfW = (pixiObject.width || 0) / 2;
36
36
  const halfH = (pixiObject.height || 0) / 2;
37
37
  const topLeft = { x: item.to.x - halfW, y: item.to.y - halfH };
38
- this.core.updateObjectPositionDirect(item.id, topLeft);
38
+ this.core.updateObjectPositionDirect(item.id, topLeft, { snap: false });
39
39
  this.emit(Events.Object.TransformUpdated, {
40
40
  objectId: item.id,
41
41
  type: 'position',
@@ -29,7 +29,7 @@ export class MoveObjectCommand extends BaseCommand {
29
29
  _setPosition(position) {
30
30
  // Используем готовую функцию из ядра - она правильно обрабатывает все типы объектов
31
31
  // position уже является координатами левого-верхнего угла
32
- this.coreMoodboard.updateObjectPositionDirect(this.objectId, position);
32
+ this.coreMoodboard.updateObjectPositionDirect(this.objectId, position, { snap: false });
33
33
 
34
34
  // Уведомляем о том, что объект был изменен (для обновления ручек)
35
35
  if (this.eventBus) {
@@ -0,0 +1,38 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+
3
+ /**
4
+ * Команда обновления терминалов и/или стиля коннектора.
5
+ * execute() применяет изменения к существующему connector-объекту в state.
6
+ */
7
+ export class UpdateConnectorCommand extends BaseCommand {
8
+ /**
9
+ * @param {Object} core Экземпляр CoreMoodBoard
10
+ * @param {string} connectorId id коннектора в state.objects
11
+ * @param {Object} updates { start?, end?, style? }
12
+ */
13
+ constructor(core, connectorId, updates) {
14
+ super('update_connector', 'Обновить коннектор');
15
+ this.core = core;
16
+ this.connectorId = connectorId;
17
+ this.updates = updates;
18
+ }
19
+
20
+ execute() {
21
+ const objects = this.core?.state?.state?.objects;
22
+ if (!Array.isArray(objects)) return;
23
+
24
+ const obj = objects.find(o => o.id === this.connectorId);
25
+ if (!obj) return;
26
+
27
+ if (!obj.properties) obj.properties = {};
28
+ if (this.updates.start !== undefined) obj.properties.start = this.updates.start;
29
+ if (this.updates.end !== undefined) obj.properties.end = this.updates.end;
30
+ if (this.updates.style !== undefined) {
31
+ obj.properties.style = { ...(obj.properties.style || {}), ...this.updates.style };
32
+ }
33
+ }
34
+
35
+ undo() {
36
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
37
+ }
38
+ }
@@ -109,6 +109,7 @@ export const Events = {
109
109
  StateChanged: 'state:changed',
110
110
  FileNameChange: 'object:filename:change',
111
111
  ContentChange: 'object:content:change',
112
+ Hover: 'object:hover',
112
113
  },
113
114
 
114
115
  History: {
@@ -215,6 +215,7 @@ export function normalizeMindmapPropertiesForCreate({
215
215
  ? (branchRootIdRaw || asNonEmptyString(objectId) || null)
216
216
  : null,
217
217
  branchColor: role === CHILD_ROLE ? branchColor : null,
218
+ collapsed: meta.collapsed === true,
218
219
  },
219
220
  };
220
221
  }
@@ -8,6 +8,9 @@ import { ContextMenu } from '../../ui/ContextMenu.js';
8
8
  import { HtmlTextLayer } from '../../ui/HtmlTextLayer.js';
9
9
  import { MindmapHtmlTextLayer } from '../../ui/mindmap/MindmapHtmlTextLayer.js';
10
10
  import { MindmapConnectionLayer } from '../../ui/mindmap/MindmapConnectionLayer.js';
11
+ import { MindmapCollapseLayer } from '../../ui/mindmap/MindmapCollapseLayer.js';
12
+ import { ConnectorLayer } from '../../ui/connectors/ConnectorLayer.js';
13
+ import { ConnectionAnchorsLayer } from '../../ui/connectors/ConnectionAnchorsLayer.js';
11
14
  import { HtmlHandlesLayer } from '../../ui/HtmlHandlesLayer.js';
12
15
  import { CommentPopover } from '../../ui/CommentPopover.js';
13
16
  import { TextPropertiesPanel } from '../../ui/TextPropertiesPanel.js';
@@ -102,6 +105,14 @@ function initHtmlLayersAndPanels(board) {
102
105
  board.mindmapHtmlTextLayer.attach();
103
106
  board.mindmapConnectionLayer = new MindmapConnectionLayer(board.coreMoodboard.eventBus, board.coreMoodboard);
104
107
  board.mindmapConnectionLayer.attach();
108
+ board.mindmapCollapseLayer = new MindmapCollapseLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
109
+ board.mindmapCollapseLayer.attach();
110
+
111
+ board.connectorLayer = new ConnectorLayer(board.coreMoodboard.eventBus, board.coreMoodboard);
112
+ board.connectorLayer.attach();
113
+
114
+ board.connectionAnchorsLayer = new ConnectionAnchorsLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
115
+ board.connectionAnchorsLayer.attach();
105
116
 
106
117
  board.htmlHandlesLayer = new HtmlHandlesLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
107
118
  board.htmlHandlesLayer.attach();
@@ -110,6 +121,9 @@ function initHtmlLayersAndPanels(board) {
110
121
  window.moodboardHtmlTextLayer = board.htmlTextLayer;
111
122
  window.moodboardMindmapHtmlTextLayer = board.mindmapHtmlTextLayer;
112
123
  window.moodboardMindmapConnectionLayer = board.mindmapConnectionLayer;
124
+ window.moodboardMindmapCollapseLayer = board.mindmapCollapseLayer;
125
+ window.moodboardConnectorLayer = board.connectorLayer;
126
+ window.moodboardConnectionAnchorsLayer = board.connectionAnchorsLayer;
113
127
  window.moodboardHtmlHandlesLayer = board.htmlHandlesLayer;
114
128
  }
115
129
 
@@ -55,6 +55,15 @@ export function destroyMoodBoard(board) {
55
55
  safeDestroy(board.mindmapConnectionLayer, 'mindmapConnectionLayer');
56
56
  board.mindmapConnectionLayer = null;
57
57
 
58
+ safeDestroy(board.mindmapCollapseLayer, 'mindmapCollapseLayer');
59
+ board.mindmapCollapseLayer = null;
60
+
61
+ safeDestroy(board.connectorLayer, 'connectorLayer');
62
+ board.connectorLayer = null;
63
+
64
+ safeDestroy(board.connectionAnchorsLayer, 'connectionAnchorsLayer');
65
+ board.connectionAnchorsLayer = null;
66
+
58
67
  safeDestroy(board.htmlHandlesLayer, 'htmlHandlesLayer');
59
68
  board.htmlHandlesLayer = null;
60
69
 
@@ -100,6 +109,15 @@ export function destroyMoodBoard(board) {
100
109
  if (window.moodboardMindmapConnectionLayer === board.mindmapConnectionLayer) {
101
110
  window.moodboardMindmapConnectionLayer = null;
102
111
  }
112
+ if (window.moodboardMindmapCollapseLayer === board.mindmapCollapseLayer) {
113
+ window.moodboardMindmapCollapseLayer = null;
114
+ }
115
+ if (window.moodboardConnectorLayer === board.connectorLayer) {
116
+ window.moodboardConnectorLayer = null;
117
+ }
118
+ if (window.moodboardConnectionAnchorsLayer === board.connectionAnchorsLayer) {
119
+ window.moodboardConnectionAnchorsLayer = null;
120
+ }
103
121
  if (window.moodboardHtmlHandlesLayer === board.htmlHandlesLayer) {
104
122
  window.moodboardHtmlHandlesLayer = null;
105
123
  }
@@ -0,0 +1,85 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * ConnectorObject — универсальный коннектор (стрелка/линия с привязкой к объектам).
5
+ *
6
+ * Визуальный рендер делегирован ConnectorLayer; здесь только тип, дефолты и сериализация.
7
+ *
8
+ * properties:
9
+ * start — { boundId, anchor:{x,y}, isPrecise, isExact } | { point:{x,y} }
10
+ * end — то же
11
+ * style — { stroke, width, dash, head:{start,end}, route }
12
+ *
13
+ * position/size номинальны (bounding box линии); реальная геометрия — из терминалов.
14
+ */
15
+ export class ConnectorObject {
16
+ constructor(objectData = {}) {
17
+ this.objectData = objectData;
18
+
19
+ // Номинальные размеры (bbox); реальная геометрия — из терминалов
20
+ this.width = objectData.width ?? 0;
21
+ this.height = objectData.height ?? 0;
22
+
23
+ const props = objectData.properties || {};
24
+
25
+ this.start = props.start || { point: { x: 0, y: 0 } };
26
+ this.end = props.end || { point: { x: 100, y: 0 } };
27
+
28
+ this.style = {
29
+ stroke: 0x2563EB,
30
+ width: 2,
31
+ dash: false,
32
+ head: { start: false, end: true },
33
+ route: 'straight',
34
+ ...(props.style || {}),
35
+ };
36
+
37
+ // Невидимый контейнер-заглушка: реальный рендер в ConnectorLayer
38
+ this._container = new PIXI.Container();
39
+ this._container.visible = false;
40
+ this._container.eventMode = 'none';
41
+
42
+ this._container._mb = {
43
+ type: 'connector',
44
+ instance: this,
45
+ properties: this._buildProperties(),
46
+ };
47
+ }
48
+
49
+ _buildProperties() {
50
+ return {
51
+ start: this.start,
52
+ end: this.end,
53
+ style: { ...this.style, head: { ...this.style.head } },
54
+ };
55
+ }
56
+
57
+ getPixi() {
58
+ return this._container;
59
+ }
60
+
61
+ /** Обновляет терминалы и/или стиль; синхронизирует _mb.properties */
62
+ setProperties({ start, end, style } = {}) {
63
+ if (start !== undefined) this.start = start;
64
+ if (end !== undefined) this.end = end;
65
+ if (style !== undefined) this.style = { ...this.style, ...style };
66
+
67
+ if (this._container?._mb) {
68
+ this._container._mb.properties = this._buildProperties();
69
+ }
70
+ }
71
+
72
+ /** Обновляет номинальные размеры (используется ядром при resize) */
73
+ updateSize(size) {
74
+ if (!size) return;
75
+ if (size.width != null) this.width = size.width;
76
+ if (size.height != null) this.height = size.height;
77
+ }
78
+
79
+ destroy() {
80
+ if (this._container) {
81
+ try { this._container.destroy(); } catch (_) {}
82
+ this._container = null;
83
+ }
84
+ }
85
+ }
@@ -1,4 +1,5 @@
1
1
  import * as PIXI from 'pixi.js';
2
+ import { GeometryUtils } from '../core/rendering/GeometryUtils.js';
2
3
 
3
4
  /**
4
5
  * Класс объекта «Рисунок» (карандаш/маркер)
@@ -27,6 +28,13 @@ export class DrawingObject {
27
28
  this.graphics = new PIXI.Graphics();
28
29
  this._draw(this.points, this.color, this.strokeWidth, this.mode);
29
30
 
31
+ // PIXI v7 Graphics.containsPoint учитывает только заливку (fillStyle),
32
+ // а рисунок состоит лишь из обводки (lineStyle) — поэтому штатный hit-test
33
+ // линии всегда «мимо», и событийная система не шлёт pointerover/pointerout
34
+ // (от них зависит hover-подсветка). Переопределяем containsPoint на проверку
35
+ // расстояния до сегментов, чтобы наведение ловилось на любом участке линии.
36
+ this.graphics.containsPoint = (globalPoint) => this._containsPoint(globalPoint);
37
+
30
38
  // Сохраняем мета для hit-test/resize
31
39
  this.graphics._mb = {
32
40
  ...(this.graphics._mb || {}),
@@ -46,6 +54,45 @@ export class DrawingObject {
46
54
  return this.graphics;
47
55
  }
48
56
 
57
+ /**
58
+ * Hit-test линии по расстоянию до её сегментов.
59
+ * Вызывается событийной системой PIXI с ГЛОБАЛЬНОЙ точкой.
60
+ * @param {PIXI.IPointData} globalPoint
61
+ * @returns {boolean}
62
+ */
63
+ _containsPoint(globalPoint) {
64
+ const g = this.graphics;
65
+ const pts = this.points;
66
+ if (!pts || pts.length < 2 || !g.toLocal) return false;
67
+
68
+ // Локальные координаты курсора в системе геометрии линии.
69
+ const local = g.toLocal(globalPoint);
70
+
71
+ // Геометрия могла быть перерисована под новый размер (updateSize),
72
+ // тогда как this.points хранит базовые точки. Масштаб берём из
73
+ // ЛОКАЛЬНЫХ границ (не зависят от zoom), чтобы совпасть с local.
74
+ const lb = g.getLocalBounds();
75
+ const baseW = this.baseWidth || 1;
76
+ const baseH = this.baseHeight || 1;
77
+ const scaleX = baseW ? (lb.width / baseW) : 1;
78
+ const scaleY = baseH ? (lb.height / baseH) : 1;
79
+
80
+ // Полоса попадания: половина толщины линии + запас на «любой участок».
81
+ const lineWidth = this.mode === 'marker' ? this.strokeWidth * 2 : this.strokeWidth;
82
+ const threshold = Math.max(8, lineWidth / 2 + 6);
83
+
84
+ for (let j = 0; j < pts.length - 1; j++) {
85
+ const ax = pts[j].x * scaleX;
86
+ const ay = pts[j].y * scaleY;
87
+ const bx = pts[j + 1].x * scaleX;
88
+ const by = pts[j + 1].y * scaleY;
89
+ if (GeometryUtils.distancePointToSegment(local.x, local.y, ax, ay, bx, by) <= threshold) {
90
+ return true;
91
+ }
92
+ }
93
+ return false;
94
+ }
95
+
49
96
  /** Обновить визуал без изменения точек */
50
97
  setStyle({ mode, strokeColor, strokeWidth } = {}) {
51
98
  if (mode) this.mode = mode;
@@ -1,6 +1,9 @@
1
1
  import * as PIXI from 'pixi.js';
2
2
  import { MINDMAP_LAYOUT } from '../ui/mindmap/MindmapLayoutConfig.js';
3
3
 
4
+ // Толщина обводки в экранных пикселях — совпадает с толщиной веток MindmapConnectionLayer.
5
+ const STROKE_SCREEN_PX = 2;
6
+
4
7
  /**
5
8
  * Простой объект mindmap: прямоугольник с синей обводкой и полупрозрачной синей заливкой.
6
9
  */
@@ -18,8 +21,10 @@ export class MindmapObject {
18
21
  ? Math.max(1, Math.round(props.capsuleBaseHeight))
19
22
  : Math.max(1, Math.round(Math.min(this.height, MINDMAP_LAYOUT.height)));
20
23
 
24
+ // Текущий масштаб мирового слоя; обновляется через redrawForZoom при каждом зуме.
25
+ this._worldScale = 1;
26
+
21
27
  this.graphics = new PIXI.Graphics();
22
- this.graphics.roundPixels = true;
23
28
  this._draw();
24
29
  }
25
30
 
@@ -34,6 +39,16 @@ export class MindmapObject {
34
39
  this._redrawPreserveTransform();
35
40
  }
36
41
 
42
+ /**
43
+ * Перерисовать пилюлю под текущий масштаб мира.
44
+ * Вызывается из PixiEngine на каждый зум — пересчитывает толщину обводки и
45
+ * число сегментов дуги, чтобы фаски оставались гладкими без зазубрин.
46
+ */
47
+ redrawForZoom(worldScale) {
48
+ this._worldScale = Math.max(0.01, worldScale || 1);
49
+ this._redrawPreserveTransform();
50
+ }
51
+
37
52
  _redrawPreserveTransform() {
38
53
  const g = this.graphics;
39
54
  const centerX = g.x;
@@ -54,13 +69,16 @@ export class MindmapObject {
54
69
  const fixedBaseRadius = Math.max(0, Math.floor(this.capsuleBaseHeight / 2));
55
70
  const capsuleRadius = Math.min(dynamicRadius, fixedBaseRadius);
56
71
 
72
+ // Толщина в мировых единицах: 3 экранных пикселя ÷ масштаб — совпадает с ветками.
73
+ const strokeW = STROKE_SCREEN_PX / this._worldScale;
74
+
57
75
  g.beginFill(this.fillColor, this.fillAlpha);
58
76
  g.drawRoundedRect(0, 0, this.width, this.height, capsuleRadius);
59
77
  g.endFill();
60
78
 
61
79
  try {
62
80
  g.lineStyle({
63
- width: this.strokeWidth,
81
+ width: strokeW,
64
82
  color: this.strokeColor,
65
83
  alpha: 1,
66
84
  alignment: 0,
@@ -69,7 +87,7 @@ export class MindmapObject {
69
87
  miterLimit: 2,
70
88
  });
71
89
  } catch (_) {
72
- g.lineStyle(this.strokeWidth, this.strokeColor, 1, 0);
90
+ g.lineStyle(strokeW, this.strokeColor, 1, 0);
73
91
  }
74
92
  g.drawRoundedRect(0, 0, this.width, this.height, capsuleRadius);
75
93
  }
@@ -83,24 +83,26 @@ export class NoteObject {
83
83
  this.textField.mask = this.textMask;
84
84
 
85
85
  this._redraw(); // Сначала рисуем фон
86
- // Прячем текст до загрузки шрифта Caveat, чтобы не показывать системный
87
86
  this.textField.visible = false;
88
87
  this.container.addChild(this.textField); // Затем добавляем текст поверх
89
- this._updateTextPosition();
90
- // Если шрифт уже загружен показываем сразу, иначе подождём загрузки
88
+ // Если шрифт уже загружен — позиционируем и показываем сразу.
89
+ // Если нет НЕ вызываем _updateTextPosition(), чтобы не отравить кэш
90
+ // PIXI.TextMetrics метриками fallback-шрифта (Arial). Позиционирование
91
+ // произойдёт в _ensureWebFontApplied после реальной загрузки шрифта.
91
92
  if (this._isFontLoaded(fontFamily, this.fontSize)) {
93
+ this._updateTextPosition();
92
94
  this.textField.visible = true;
93
95
  } else {
94
- this._ensureWebFontApplied(fontFamily, this.fontSize);
95
- // Фолбэк на случай отсутствия Font Loading API — короткая задержка
96
+ // Фолбэк на случай отсутствия Font Loading API — показываем без позиционирования
96
97
  try {
97
98
  if (!(typeof document !== 'undefined' && document.fonts && typeof document.fonts.load === 'function')) {
99
+ this._updateTextPosition();
98
100
  setTimeout(() => { try { this.textField.visible = true; } catch (_) {} }, 300);
99
101
  }
100
102
  } catch (_) {}
101
103
  }
102
104
 
103
- // Гарантируем применение web-font (например, Caveat) при первом создании
105
+ // Загружаем web-font и применяем корректные метрики
104
106
  this._ensureWebFontApplied(fontFamily, this.fontSize);
105
107
 
106
108
  // Отладочная информация
@@ -290,7 +292,9 @@ export class NoteObject {
290
292
  }
291
293
 
292
294
  /**
293
- * Дожидается загрузки веб-шрифта и обновляет PIXI.Text, чтобы применились корректные метрики
295
+ * Дожидается загрузки веб-шрифта и обновляет PIXI.Text с правильными метриками.
296
+ * Сбрасывает кэш PIXI.TextMetrics перед позиционированием — это гарантирует,
297
+ * что PIXI не будет использовать ранее закэшированные метрики fallback-шрифта.
294
298
  */
295
299
  _ensureWebFontApplied(fontFamily, fontSizePx) {
296
300
  try {
@@ -299,9 +303,13 @@ export class NoteObject {
299
303
  const size = Math.max(1, Number(fontSizePx) || 32);
300
304
  const spec = `normal ${size}px ${primary}`;
301
305
  document.fonts.load(spec).then(() => {
302
- // Обновляем текст после загрузки шрифта и сразу подгоняем без мерцания
303
306
  try {
304
307
  if (this.textField) this.textField.visible = false;
308
+ // Сбрасываем кэш метрик шрифтов PIXI, чтобы Caveat переизмерился
309
+ // с реальными метриками, а не с ранее закэшированными от Arial
310
+ if (PIXI.TextMetrics?.clearMetrics) {
311
+ PIXI.TextMetrics.clearMetrics();
312
+ }
305
313
  this.textField.style.fontFamily = fontFamily;
306
314
  this._updateTextPosition();
307
315
  if (this.textField) this.textField.visible = true;
@@ -9,6 +9,7 @@ import { CommentObject } from './CommentObject.js';
9
9
  import { NoteObject } from './NoteObject.js';
10
10
  import { FileObject } from './FileObject.js';
11
11
  import { MindmapObject } from './MindmapObject.js';
12
+ import { ConnectorObject } from './ConnectorObject.js';
12
13
 
13
14
  /**
14
15
  * Фабрика объектов холста
@@ -27,7 +28,8 @@ export class ObjectFactory {
27
28
  ['comment', CommentObject],
28
29
  ['note', NoteObject],
29
30
  ['file', FileObject],
30
- ['mindmap', MindmapObject]
31
+ ['mindmap', MindmapObject],
32
+ ['connector', ConnectorObject],
31
33
  ]);
32
34
 
33
35
  /**
@@ -16,7 +16,7 @@ export class ShapeObject {
16
16
  this.objectData = objectData;
17
17
  this.width = objectData.width || 100;
18
18
  this.height = objectData.height || 100;
19
- this.fillColor = objectData.color ?? 0x3b82f6;
19
+ this.fillColor = objectData.color ?? 0xffffff;
20
20
  const props = objectData.properties || {};
21
21
  this.kind = props.kind || 'square';
22
22
  this.cornerRadius = props.cornerRadius || 10;