@sequent-org/moodboard 1.4.30 → 1.4.32

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 (61) 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 +1 -10
  44. package/src/ui/chat/ChatExtendedPromptModal.js +1 -12
  45. package/src/ui/chat/ChatWindow.js +167 -36
  46. package/src/ui/chat/ChatWindowRenderer.js +1 -8
  47. package/src/ui/chat/icons.js +17 -5
  48. package/src/ui/connectors/ConnectionAnchorsLayer.js +231 -0
  49. package/src/ui/connectors/ConnectorLayer.js +251 -0
  50. package/src/ui/handles/HandlesDomRenderer.js +11 -7
  51. package/src/ui/handles/HandlesInteractionController.js +65 -34
  52. package/src/ui/handles/HandlesPositioningService.js +41 -6
  53. package/src/ui/mindmap/MindmapCollapseGraph.js +169 -0
  54. package/src/ui/mindmap/MindmapCollapseLayer.js +380 -0
  55. package/src/ui/mindmap/MindmapConnectionLayer.js +50 -25
  56. package/src/ui/mindmap/MindmapHtmlTextLayer.js +223 -2
  57. package/src/ui/mindmap/MindmapLayoutConfig.js +12 -0
  58. package/src/ui/styles/chat.css +2 -37
  59. package/src/ui/styles/toolbar.css +6 -0
  60. package/src/ui/styles/workspace.css +83 -21
  61. package/src/ui/toolbar/ToolbarPopupsController.js +1 -1
@@ -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
  }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Чистые функции для работы с деревом майндмапа (collapse/expand).
3
+ * Без PIXI, без побочных эффектов, без импортов UI.
4
+ */
5
+
6
+ const MINDMAP_TYPE = 'mindmap';
7
+ const CHILD_ROLE = 'child';
8
+ const VALID_SIDES = new Set(['left', 'right', 'bottom']);
9
+
10
+ function isMindmapNode(obj) {
11
+ return obj?.type === MINDMAP_TYPE;
12
+ }
13
+
14
+ function asMeta(obj) {
15
+ return obj?.properties?.mindmap || {};
16
+ }
17
+
18
+ function asNonEmptyString(value) {
19
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
20
+ }
21
+
22
+ /**
23
+ * Резолв родителя — та же логика, что в MindmapConnectionLayer.resolveLegacyLink.
24
+ * Возвращает parentId или null.
25
+ */
26
+ function resolveParentId(child, byId, rootByCompoundId) {
27
+ const meta = asMeta(child);
28
+ const compoundId = meta.compoundId || null;
29
+ let parentId = meta.parentId || null;
30
+ const childId = child?.id || null;
31
+
32
+ if (!parentId || parentId === childId) {
33
+ parentId = asNonEmptyString(meta.branchRootId);
34
+ }
35
+
36
+ const parent = parentId ? byId.get(parentId) : null;
37
+ if (!parent && compoundId) {
38
+ const rootId = rootByCompoundId.get(compoundId) || null;
39
+ if (rootId && rootId !== childId) parentId = rootId;
40
+ }
41
+
42
+ if (parentId === childId && compoundId) {
43
+ const rootId = rootByCompoundId.get(compoundId) || null;
44
+ if (rootId && rootId !== childId) parentId = rootId;
45
+ }
46
+
47
+ return parentId || null;
48
+ }
49
+
50
+ function buildIndexMaps(objects) {
51
+ const arr = Array.isArray(objects) ? objects : [];
52
+ const mindmaps = arr.filter(isMindmapNode);
53
+ const byId = new Map(mindmaps.map((o) => [o.id, o]));
54
+ const rootByCompoundId = new Map();
55
+ mindmaps.forEach((o) => {
56
+ const meta = asMeta(o);
57
+ const cid = asNonEmptyString(meta.compoundId);
58
+ if (meta.role === 'root' && cid) {
59
+ rootByCompoundId.set(cid, o.id);
60
+ }
61
+ });
62
+ return { byId, rootByCompoundId };
63
+ }
64
+
65
+ /**
66
+ * Строит индекс parent → [children] для всех майндмап-нод.
67
+ * @param {object[]} objects — массив всех объектов доски
68
+ * @returns {Map<string, object[]>}
69
+ */
70
+ export function buildChildrenIndex(objects) {
71
+ const arr = Array.isArray(objects) ? objects : [];
72
+ const { byId, rootByCompoundId } = buildIndexMaps(arr);
73
+
74
+ const index = new Map();
75
+ arr.filter(isMindmapNode).forEach((obj) => {
76
+ const meta = asMeta(obj);
77
+ if (meta.role !== CHILD_ROLE) return;
78
+ const parentId = resolveParentId(obj, byId, rootByCompoundId);
79
+ if (!parentId) return;
80
+ if (!index.has(parentId)) index.set(parentId, []);
81
+ index.get(parentId).push(obj);
82
+ });
83
+
84
+ return index;
85
+ }
86
+
87
+ /**
88
+ * Возвращает id всех потомков nodeId (рекурсивно, все уровни).
89
+ * @param {object[]} objects
90
+ * @param {string} nodeId
91
+ * @returns {string[]}
92
+ */
93
+ export function getDescendantIds(objects, nodeId) {
94
+ const childrenIndex = buildChildrenIndex(objects);
95
+ const result = [];
96
+ const queue = [nodeId];
97
+ const visited = new Set();
98
+
99
+ while (queue.length > 0) {
100
+ const id = queue.shift();
101
+ if (visited.has(id)) continue;
102
+ visited.add(id);
103
+ const children = childrenIndex.get(id) || [];
104
+ for (const child of children) {
105
+ result.push(child.id);
106
+ queue.push(child.id);
107
+ }
108
+ }
109
+
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Возвращает true, если у любого предка nodeId стоит collapsed === true.
115
+ * @param {object[]} objects
116
+ * @param {string} nodeId
117
+ * @returns {boolean}
118
+ */
119
+ export function isHiddenByCollapsedAncestor(objects, nodeId) {
120
+ const { byId, rootByCompoundId } = buildIndexMaps(objects);
121
+ const node = byId.get(nodeId);
122
+ if (!node) return false;
123
+
124
+ let currentId = resolveParentId(node, byId, rootByCompoundId);
125
+ const visited = new Set();
126
+
127
+ while (currentId) {
128
+ if (visited.has(currentId)) break;
129
+ visited.add(currentId);
130
+ const ancestor = byId.get(currentId);
131
+ if (!ancestor) break;
132
+ if (asMeta(ancestor).collapsed === true) return true;
133
+ currentId = resolveParentId(ancestor, byId, rootByCompoundId);
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Число всех потомков ноды — для отображения в бейдже.
141
+ * @param {object[]} objects
142
+ * @param {string} nodeId
143
+ * @returns {number}
144
+ */
145
+ export function countVisibleDescendantsForBadge(objects, nodeId) {
146
+ return getDescendantIds(objects, nodeId).length;
147
+ }
148
+
149
+ /**
150
+ * Возвращает стороны ('left'|'right'|'bottom'), по которым у ноды есть дети.
151
+ * Используется для размещения кнопки collapse со стороны детей.
152
+ * @param {object[]} objects
153
+ * @param {string} nodeId
154
+ * @returns {Set<string>}
155
+ */
156
+ export function getChildrenSidesWithChildren(objects, nodeId) {
157
+ const { byId, rootByCompoundId } = buildIndexMaps(objects);
158
+ const sides = new Set();
159
+
160
+ (Array.isArray(objects) ? objects : []).filter(isMindmapNode).forEach((obj) => {
161
+ const meta = asMeta(obj);
162
+ if (meta.role !== CHILD_ROLE) return;
163
+ const parentId = resolveParentId(obj, byId, rootByCompoundId);
164
+ if (parentId !== nodeId) return;
165
+ if (VALID_SIDES.has(meta.side)) sides.add(meta.side);
166
+ });
167
+
168
+ return sides;
169
+ }