@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.
- package/package.json +5 -1
- package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/src/assets/icons/attachments.svg +3 -1
- package/src/assets/icons/comments.svg +2 -2
- package/src/assets/icons/connector.svg +6 -0
- package/src/assets/icons/emoji.svg +6 -1
- package/src/assets/icons/frame.svg +4 -1
- package/src/assets/icons/image.svg +5 -1
- package/src/assets/icons/laser.svg +1 -0
- package/src/assets/icons/lasso.svg +5 -0
- package/src/assets/icons/mindmap.svg +10 -2
- package/src/assets/icons/note.svg +4 -1
- package/src/assets/icons/pan.svg +5 -2
- package/src/assets/icons/pencil.svg +4 -1
- package/src/assets/icons/reactions.svg +5 -0
- package/src/assets/icons/redo.svg +3 -2
- package/src/assets/icons/select.svg +2 -8
- package/src/assets/icons/shapes.svg +5 -1
- package/src/assets/icons/text-add.svg +15 -1
- package/src/assets/icons/undo.svg +3 -2
- package/src/assets/reactions/1f44d.svg +20 -0
- package/src/assets/reactions/1f44e.svg +20 -0
- package/src/assets/reactions/2705.svg +20 -0
- package/src/assets/reactions/274c.svg +19 -0
- package/src/assets/reactions/2753.svg +20 -0
- package/src/assets/reactions/2764.svg +22 -0
- package/src/assets/reactions/2b50.svg +19 -0
- package/src/assets/reactions/plus-one.svg +25 -0
- package/src/core/PixiEngine.js +23 -0
- package/src/core/bootstrap/CoreInitializer.js +43 -0
- package/src/core/commands/GroupDeleteCommand.js +13 -1
- package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
- package/src/core/commands/UpdateTextStyleCommand.js +17 -6
- package/src/core/commands/index.js +3 -0
- package/src/core/events/Events.js +22 -0
- package/src/core/flows/LayerAndViewportFlow.js +1 -0
- package/src/core/flows/ObjectLifecycleFlow.js +155 -7
- package/src/core/index.js +28 -1
- package/src/grid/CrossGridZoomPhases.js +3 -3
- package/src/initNoBundler.js +1 -1
- package/src/moodboard/DataManager.js +28 -0
- package/src/moodboard/MoodBoard.js +27 -0
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
- package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
- package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
- package/src/objects/ConnectorObject.js +2 -2
- package/src/objects/FrameObject.js +119 -59
- package/src/objects/ShapeObject.js +49 -74
- package/src/objects/shape/ShapeDrawer.js +210 -0
- package/src/services/ConnectorBindingResolver.js +112 -0
- package/src/services/ConnectorRouter.js +210 -0
- package/src/services/comments/CommentService.js +344 -0
- package/src/tools/object-tools/CommentTool.js +85 -0
- package/src/tools/object-tools/DrawingTool.js +110 -10
- package/src/tools/object-tools/LaserPointerTool.js +121 -0
- package/src/tools/object-tools/SelectTool.js +25 -1
- package/src/tools/object-tools/TextTool.js +6 -1
- package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
- package/src/tools/object-tools/connector/connectorGesture.js +33 -19
- package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
- package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
- package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
- package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
- package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
- package/src/ui/CommentPopover.js +6 -0
- package/src/ui/CommentsBar.js +91 -0
- package/src/ui/ConnectorPropertiesPanel.js +150 -0
- package/src/ui/ContextMenu.js +25 -0
- package/src/ui/DrawingPropertiesPanel.js +362 -0
- package/src/ui/FilePropertiesPanel.js +5 -0
- package/src/ui/FramePropertiesPanel.js +5 -0
- package/src/ui/HtmlTextLayer.js +246 -66
- package/src/ui/NotePropertiesPanel.js +6 -0
- package/src/ui/ShapePropertiesPanel.js +307 -0
- package/src/ui/TextPropertiesPanel.js +100 -1
- package/src/ui/Toolbar.js +25 -2
- package/src/ui/Topbar.js +2 -2
- package/src/ui/animation/HoverLiftController.js +6 -7
- package/src/ui/chat/ChatComposer.js +58 -7
- package/src/ui/chat/ChatWindow.js +60 -143
- package/src/ui/comments/CommentListPanel.js +213 -0
- package/src/ui/comments/CommentPinLayer.js +448 -0
- package/src/ui/comments/CommentThreadPopover.js +539 -0
- package/src/ui/comments/commentFormat.js +32 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
- package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
- package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
- package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
- package/src/ui/connectors/ConnectorLayer.js +264 -57
- package/src/ui/handles/HandlesDomRenderer.js +5 -13
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
- package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
- package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
- package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
- package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
- package/src/ui/styles/chat.css +709 -19
- package/src/ui/styles/index.css +1 -0
- package/src/ui/styles/panels.css +112 -2
- package/src/ui/styles/shape-properties-panel.css +250 -0
- package/src/ui/styles/toolbar.css +7 -2
- package/src/ui/styles/topbar.css +1 -1
- package/src/ui/styles/workspace.css +257 -6
- package/src/ui/text-properties/TextFormatControls.js +88 -0
- package/src/ui/text-properties/TextListRenderer.js +137 -0
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
- package/src/ui/toolbar/ReactionsPopupController.js +88 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
- package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
- package/src/ui/toolbar/ToolbarRenderer.js +9 -1
- package/src/ui/toolbar/ToolbarStateController.js +4 -1
- package/src/utils/iconLoader.js +17 -16
- package/src/utils/markdown.js +14 -0
- package/src/utils/richText.js +125 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Чистая геометрия вычисления маршрутов коннекторов.
|
|
3
|
+
* Без зависимостей PIXI — только координатная математика.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const BEZIER_CP_RATIO = 0.4;
|
|
7
|
+
const BEZIER_SAMPLES = 20;
|
|
8
|
+
const ELBOW_STUB = 24; // минимальный выход перпендикулярно грани до первого поворота
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ортогональные точки излома для elbow-маршрута.
|
|
12
|
+
* Доминирующая ось определяет схему H-V-H или V-H-V.
|
|
13
|
+
*
|
|
14
|
+
* @param {{ x: number, y: number }} start
|
|
15
|
+
* @param {{ x: number, y: number }} end
|
|
16
|
+
* @returns {Array<{x:number,y:number}>}
|
|
17
|
+
*/
|
|
18
|
+
export function computeElbowWaypoints(start, end) {
|
|
19
|
+
const dx = end.x - start.x;
|
|
20
|
+
const dy = end.y - start.y;
|
|
21
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
22
|
+
const mx = Math.round((start.x + end.x) / 2);
|
|
23
|
+
return [start, { x: mx, y: start.y }, { x: mx, y: end.y }, end];
|
|
24
|
+
}
|
|
25
|
+
const my = Math.round((start.y + end.y) / 2);
|
|
26
|
+
return [start, { x: start.x, y: my }, { x: end.x, y: my }, end];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ортогональный маршрут с перпендикулярным выходом из грани и минимумом изгибов.
|
|
31
|
+
* S' = startPt + startDir*ELBOW_STUB; E' = endPt + endDir*ELBOW_STUB.
|
|
32
|
+
* Перпендикулярные грани → L-shape (1 изгиб); одна ось → Z-shape (навстречу)
|
|
33
|
+
* или U-shape (в одну сторону), оба по 2 изгиба.
|
|
34
|
+
*
|
|
35
|
+
* @param {{ x:number, y:number }} startPt
|
|
36
|
+
* @param {{ x:number, y:number }} startDir единичная внешняя нормаль грани старта
|
|
37
|
+
* @param {{ x:number, y:number }} endPt
|
|
38
|
+
* @param {{ x:number, y:number }} endDir единичная внешняя нормаль грани конца
|
|
39
|
+
* @returns {Array<{x:number,y:number}>}
|
|
40
|
+
*/
|
|
41
|
+
function computeElbowWithDirs(startPt, startDir, endPt, endDir) {
|
|
42
|
+
const S = {
|
|
43
|
+
x: Math.round(startPt.x + startDir.x * ELBOW_STUB),
|
|
44
|
+
y: Math.round(startPt.y + startDir.y * ELBOW_STUB),
|
|
45
|
+
};
|
|
46
|
+
const E = {
|
|
47
|
+
x: Math.round(endPt.x + endDir.x * ELBOW_STUB),
|
|
48
|
+
y: Math.round(endPt.y + endDir.y * ELBOW_STUB),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const startHoriz = Math.abs(startDir.x) >= Math.abs(startDir.y);
|
|
52
|
+
const endHoriz = Math.abs(endDir.x) >= Math.abs(endDir.y);
|
|
53
|
+
|
|
54
|
+
// Цель — минимум изгибов: L-shape = 1 изгиб, Z/U-shape = 2 изгиба.
|
|
55
|
+
let corners;
|
|
56
|
+
if (startHoriz === endHoriz) {
|
|
57
|
+
// Грани на одной оси
|
|
58
|
+
if (startHoriz) {
|
|
59
|
+
if (Math.sign(startDir.x) === Math.sign(endDir.x)) {
|
|
60
|
+
// Смотрят в одну сторону → U-shape: общий вынос по дальней кромке (2 изгиба)
|
|
61
|
+
const outerX = startDir.x >= 0 ? Math.max(S.x, E.x) : Math.min(S.x, E.x);
|
|
62
|
+
corners = [{ x: outerX, y: S.y }, { x: outerX, y: E.y }];
|
|
63
|
+
} else {
|
|
64
|
+
// Смотрят противоположно по горизонтали — различаем навстречу и врозь.
|
|
65
|
+
// Навстречу: стабы сходятся, признак: (E.x - S.x) * startDir.x > 0.
|
|
66
|
+
const facingEachOther = (E.x - S.x) * startDir.x > 0;
|
|
67
|
+
if (facingEachOther) {
|
|
68
|
+
// Z-shape: одно вертикальное пересечение посередине (2 изгиба)
|
|
69
|
+
const midX = Math.round((S.x + E.x) / 2);
|
|
70
|
+
corners = [{ x: midX, y: S.y }, { x: midX, y: E.y }];
|
|
71
|
+
} else {
|
|
72
|
+
// Грани смотрят врозь → C-shape: уходим вертикально (перпендикулярно нормали),
|
|
73
|
+
// огибаем сверху или снизу.
|
|
74
|
+
const outerY = S.y <= E.y
|
|
75
|
+
? Math.min(S.y, E.y) - ELBOW_STUB
|
|
76
|
+
: Math.max(S.y, E.y) + ELBOW_STUB;
|
|
77
|
+
corners = [{ x: S.x, y: outerY }, { x: E.x, y: outerY }];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else if (Math.sign(startDir.y) === Math.sign(endDir.y)) {
|
|
81
|
+
const outerY = startDir.y >= 0 ? Math.max(S.y, E.y) : Math.min(S.y, E.y);
|
|
82
|
+
corners = [{ x: S.x, y: outerY }, { x: E.x, y: outerY }];
|
|
83
|
+
} else {
|
|
84
|
+
// Смотрят противоположно по вертикали — различаем навстречу и врозь.
|
|
85
|
+
// Навстречу: (E.y - S.y) * startDir.y > 0.
|
|
86
|
+
const facingEachOther = (E.y - S.y) * startDir.y > 0;
|
|
87
|
+
if (facingEachOther) {
|
|
88
|
+
const midY = Math.round((S.y + E.y) / 2);
|
|
89
|
+
corners = [{ x: S.x, y: midY }, { x: E.x, y: midY }];
|
|
90
|
+
} else {
|
|
91
|
+
// Грани смотрят врозь → C-shape: уходим горизонтально (перпендикулярно нормали),
|
|
92
|
+
// огибаем слева или справа.
|
|
93
|
+
const outerX = S.x <= E.x
|
|
94
|
+
? Math.min(S.x, E.x) - ELBOW_STUB
|
|
95
|
+
: Math.max(S.x, E.x) + ELBOW_STUB;
|
|
96
|
+
corners = [{ x: outerX, y: S.y }, { x: outerX, y: E.y }];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Перпендикулярные оси → L-shape (1 изгиб).
|
|
101
|
+
// Угол стыкуем к стабу: первый сегмент продолжает выход, чтобы не плодить изломы.
|
|
102
|
+
if (startHoriz) {
|
|
103
|
+
corners = [{ x: E.x, y: S.y }];
|
|
104
|
+
} else {
|
|
105
|
+
corners = [{ x: S.x, y: E.y }];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Дедупликация соседних точек (вырожденные нулевые сегменты)
|
|
110
|
+
const raw = [startPt, S, ...corners, E, endPt];
|
|
111
|
+
const result = [{ x: Math.round(raw[0].x), y: Math.round(raw[0].y) }];
|
|
112
|
+
for (let i = 1; i < raw.length; i++) {
|
|
113
|
+
const p = raw[i];
|
|
114
|
+
const prev = result[result.length - 1];
|
|
115
|
+
if (Math.abs(p.x - prev.x) > 0.5 || Math.abs(p.y - prev.y) > 0.5) {
|
|
116
|
+
result.push({ x: Math.round(p.x), y: Math.round(p.y) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Сэмплирует кубическую кривую Безье при параметре t∈[0,1].
|
|
125
|
+
*
|
|
126
|
+
* @param {{ x: number, y: number }} s начало
|
|
127
|
+
* @param {{ x: number, y: number }} cp1 контрольная точка 1
|
|
128
|
+
* @param {{ x: number, y: number }} cp2 контрольная точка 2
|
|
129
|
+
* @param {{ x: number, y: number }} e конец
|
|
130
|
+
* @param {number} t параметр [0,1]
|
|
131
|
+
* @returns {{ x: number, y: number }}
|
|
132
|
+
*/
|
|
133
|
+
export function sampleBezier(s, cp1, cp2, e, t) {
|
|
134
|
+
const mt = 1 - t;
|
|
135
|
+
return {
|
|
136
|
+
x: mt * mt * mt * s.x + 3 * mt * mt * t * cp1.x + 3 * mt * t * t * cp2.x + t * t * t * e.x,
|
|
137
|
+
y: mt * mt * mt * s.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * e.y,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Контрольные точки кубической кривой Безье.
|
|
143
|
+
* Если переданы dir-векторы — отводятся вдоль нормали грани (перпендикулярный заход).
|
|
144
|
+
* Иначе — вдоль доминирующей оси bbox (старый fallback для straight).
|
|
145
|
+
*
|
|
146
|
+
* @param {{ x: number, y: number }} start
|
|
147
|
+
* @param {{ x: number, y: number }} end
|
|
148
|
+
* @param {{ x: number, y: number }|null} startDir
|
|
149
|
+
* @param {{ x: number, y: number }|null} endDir
|
|
150
|
+
* @returns {{ cp1: {x:number,y:number}, cp2: {x:number,y:number} }}
|
|
151
|
+
*/
|
|
152
|
+
export function bezierControlPoints(start, end, startDir = null, endDir = null) {
|
|
153
|
+
if (startDir && endDir) {
|
|
154
|
+
const dist = Math.hypot(end.x - start.x, end.y - start.y);
|
|
155
|
+
const offset = dist * BEZIER_CP_RATIO;
|
|
156
|
+
return {
|
|
157
|
+
cp1: { x: start.x + startDir.x * offset, y: start.y + startDir.y * offset },
|
|
158
|
+
cp2: { x: end.x + endDir.x * offset, y: end.y + endDir.y * offset },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const dx = end.x - start.x;
|
|
162
|
+
const dy = end.y - start.y;
|
|
163
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
164
|
+
const offset = Math.abs(dx) * BEZIER_CP_RATIO;
|
|
165
|
+
return {
|
|
166
|
+
cp1: { x: start.x + offset, y: start.y },
|
|
167
|
+
cp2: { x: end.x - offset, y: end.y },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const offset = Math.abs(dy) * BEZIER_CP_RATIO;
|
|
171
|
+
return {
|
|
172
|
+
cp1: { x: start.x, y: start.y + offset },
|
|
173
|
+
cp2: { x: end.x, y: end.y - offset },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Строит массив опорных точек пути по двум world-точкам и route.
|
|
179
|
+
*
|
|
180
|
+
* straight → [start, end]
|
|
181
|
+
* elbow → перпендикулярный выход через стаб + ортогональные изломы
|
|
182
|
+
* bezier → BEZIER_SAMPLES+1 сэмплов вдоль кривой (для hitTest и направления головы)
|
|
183
|
+
*
|
|
184
|
+
* @param {{ x: number, y: number }} start
|
|
185
|
+
* @param {{ x: number, y: number }} end
|
|
186
|
+
* @param {string} route 'straight'|'elbow'|'bezier'
|
|
187
|
+
* @param {{ x: number, y: number }|null} startDir внешняя нормаль грани старта
|
|
188
|
+
* @param {{ x: number, y: number }|null} endDir внешняя нормаль грани конца
|
|
189
|
+
* @returns {Array<{x:number,y:number}>}
|
|
190
|
+
*/
|
|
191
|
+
export function buildPath(start, end, route, startDir = null, endDir = null) {
|
|
192
|
+
if (route === 'elbow') {
|
|
193
|
+
if (startDir && endDir) {
|
|
194
|
+
return computeElbowWithDirs(start, startDir, end, endDir);
|
|
195
|
+
}
|
|
196
|
+
return computeElbowWaypoints(start, end);
|
|
197
|
+
}
|
|
198
|
+
if (route === 'bezier') {
|
|
199
|
+
const { cp1, cp2 } = bezierControlPoints(start, end, startDir, endDir);
|
|
200
|
+
const pts = [];
|
|
201
|
+
for (let i = 0; i <= BEZIER_SAMPLES; i++) {
|
|
202
|
+
const p = sampleBezier(start, cp1, cp2, end, i / BEZIER_SAMPLES);
|
|
203
|
+
pts.push({ x: Math.round(p.x), y: Math.round(p.y) });
|
|
204
|
+
}
|
|
205
|
+
return pts;
|
|
206
|
+
}
|
|
207
|
+
return [start, end];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { BEZIER_CP_RATIO, BEZIER_SAMPLES, ELBOW_STUB };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { Events } from '../../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Хранилище тредов/сообщений и вызовы инжектированного CommentsAdapter.
|
|
5
|
+
* Без HTTP — только adapter из options.
|
|
6
|
+
*/
|
|
7
|
+
export class CommentService {
|
|
8
|
+
constructor({ eventBus, boardId, adapter, currentUser }) {
|
|
9
|
+
this.eventBus = eventBus;
|
|
10
|
+
this.boardId = boardId;
|
|
11
|
+
this.adapter = adapter;
|
|
12
|
+
this.currentUser = currentUser || null;
|
|
13
|
+
/** @type {Map<number, object>} */
|
|
14
|
+
this.threads = new Map();
|
|
15
|
+
this._appliedMessageIds = new Set();
|
|
16
|
+
this._appliedThreadIds = new Set();
|
|
17
|
+
this._onObjectDeleted = this._onObjectDeleted.bind(this);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
attach() {
|
|
21
|
+
this.eventBus.on(Events.Object.Deleted, this._onObjectDeleted);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
detach() {
|
|
25
|
+
this.eventBus.off(Events.Object.Deleted, this._onObjectDeleted);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
destroy() {
|
|
29
|
+
this.detach();
|
|
30
|
+
this.threads.clear();
|
|
31
|
+
this._appliedMessageIds.clear();
|
|
32
|
+
this._appliedThreadIds.clear();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getThread(threadId) {
|
|
36
|
+
return this.threads.get(Number(threadId)) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getAllThreads() {
|
|
40
|
+
return Array.from(this.threads.values());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async loadInitial() {
|
|
44
|
+
if (!this.adapter?.loadThreads) return;
|
|
45
|
+
const data = await this.adapter.loadThreads(this.boardId, {
|
|
46
|
+
include_resolved: true,
|
|
47
|
+
});
|
|
48
|
+
const items = data?.items || [];
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
this._upsertThread(item);
|
|
51
|
+
if (item.id != null) this._appliedThreadIds.add(Number(item.id));
|
|
52
|
+
for (const msg of item.messages?.items || []) {
|
|
53
|
+
if (msg?.id != null) this._appliedMessageIds.add(Number(msg.id));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {{ x: number, y: number, anchor_object_id?: string, anchor_dx?: number, anchor_dy?: number, content: string }} payload
|
|
60
|
+
*/
|
|
61
|
+
async createThread(payload) {
|
|
62
|
+
if (!this.adapter?.createThread) {
|
|
63
|
+
throw new Error('CommentsAdapter.createThread is not configured');
|
|
64
|
+
}
|
|
65
|
+
const result = await this.adapter.createThread(this.boardId, payload);
|
|
66
|
+
const thread = result?.thread;
|
|
67
|
+
if (thread) {
|
|
68
|
+
this._upsertThread(thread);
|
|
69
|
+
if (thread.id != null) this._appliedThreadIds.add(Number(thread.id));
|
|
70
|
+
for (const msg of thread.messages?.items || []) {
|
|
71
|
+
if (msg?.id != null) this._appliedMessageIds.add(Number(msg.id));
|
|
72
|
+
}
|
|
73
|
+
this.eventBus.emit(Events.Comment.PinCreated, {
|
|
74
|
+
threadId: thread.id,
|
|
75
|
+
x: thread.x,
|
|
76
|
+
y: thread.y,
|
|
77
|
+
anchorObjectId: thread.anchor_object_id || undefined,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return thread;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async addReply(threadId, content) {
|
|
84
|
+
const message = await this.adapter.addReply(this.boardId, threadId, content);
|
|
85
|
+
this._applyMessageToThread(threadId, message);
|
|
86
|
+
this.eventBus.emit(Events.Comment.MessageAdded, { threadId, message });
|
|
87
|
+
if (message?.id != null) this._appliedMessageIds.add(Number(message.id));
|
|
88
|
+
return message;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async resolveThread(threadId, resolved) {
|
|
92
|
+
const thread = await this.adapter.resolveThread(this.boardId, threadId, resolved);
|
|
93
|
+
if (thread) {
|
|
94
|
+
this._upsertThread({ ...this.getThread(threadId), ...thread });
|
|
95
|
+
this.eventBus.emit(Events.Comment.Resolved, {
|
|
96
|
+
threadId,
|
|
97
|
+
resolved: !!thread.resolved,
|
|
98
|
+
resolvedBy: thread.resolved_by ?? undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return thread;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async moveThread(threadId, payload) {
|
|
105
|
+
if (!this.adapter?.moveThread) {
|
|
106
|
+
throw new Error('CommentsAdapter.moveThread is not configured');
|
|
107
|
+
}
|
|
108
|
+
const prev = this.getThread(threadId);
|
|
109
|
+
if (!prev) return null;
|
|
110
|
+
this._upsertThread({ ...prev, ...payload });
|
|
111
|
+
try {
|
|
112
|
+
const thread = await this.adapter.moveThread(this.boardId, threadId, payload);
|
|
113
|
+
if (thread) {
|
|
114
|
+
this._upsertThread({ ...this.getThread(threadId), ...thread });
|
|
115
|
+
this.eventBus.emit(Events.Comment.RemoteUpdated, {
|
|
116
|
+
action: 'thread.updated',
|
|
117
|
+
boardId: this.boardId,
|
|
118
|
+
thread,
|
|
119
|
+
message: null,
|
|
120
|
+
changes: payload,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return thread;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error('[CommentService] moveThread error', err);
|
|
126
|
+
this._upsertThread(prev);
|
|
127
|
+
this.eventBus.emit(Events.Comment.RemoteUpdated, {
|
|
128
|
+
action: 'thread.updated',
|
|
129
|
+
boardId: this.boardId,
|
|
130
|
+
thread: prev,
|
|
131
|
+
message: null,
|
|
132
|
+
changes: null,
|
|
133
|
+
});
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async setThreadColor(threadId, color) {
|
|
139
|
+
if (!this.adapter?.setThreadColor) {
|
|
140
|
+
throw new Error('CommentsAdapter.setThreadColor is not configured');
|
|
141
|
+
}
|
|
142
|
+
const prev = this.getThread(threadId);
|
|
143
|
+
if (!prev) return null;
|
|
144
|
+
this._upsertThread({ ...prev, color });
|
|
145
|
+
this.eventBus.emit(Events.Comment.ColorChanged, { threadId, color });
|
|
146
|
+
try {
|
|
147
|
+
const thread = await this.adapter.setThreadColor(this.boardId, threadId, color);
|
|
148
|
+
if (thread) this._upsertThread({ ...this.getThread(threadId), ...thread });
|
|
149
|
+
return thread;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('[CommentService] setThreadColor error', err);
|
|
152
|
+
this._upsertThread(prev);
|
|
153
|
+
this.eventBus.emit(Events.Comment.ColorChanged, { threadId, color: prev.color ?? null });
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async deleteThread(threadId) {
|
|
159
|
+
if (!this.adapter?.deleteThread) {
|
|
160
|
+
throw new Error('CommentsAdapter.deleteThread is not configured');
|
|
161
|
+
}
|
|
162
|
+
await this.adapter.deleteThread(this.boardId, threadId);
|
|
163
|
+
const id = Number(threadId);
|
|
164
|
+
const thread = this.threads.get(id);
|
|
165
|
+
this.threads.delete(id);
|
|
166
|
+
this._appliedThreadIds.delete(id);
|
|
167
|
+
if (thread?.messages?.items) {
|
|
168
|
+
for (const m of thread.messages.items) {
|
|
169
|
+
if (m?.id != null) this._appliedMessageIds.delete(Number(m.id));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
this.eventBus.emit(Events.Comment.ThreadDeleted, { threadId });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async updateMessage(messageId, content) {
|
|
176
|
+
return this.adapter.updateMessage(this.boardId, messageId, content);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async deleteMessage(messageId) {
|
|
180
|
+
const data = await this.adapter.deleteMessage(this.boardId, messageId);
|
|
181
|
+
const threadId = data?.thread_id;
|
|
182
|
+
const thread = threadId != null ? this.getThread(threadId) : null;
|
|
183
|
+
if (thread?.messages?.items) {
|
|
184
|
+
thread.messages.items = thread.messages.items.filter((m) => m.id !== messageId);
|
|
185
|
+
thread.messages.count = thread.messages.items.length;
|
|
186
|
+
}
|
|
187
|
+
this.eventBus.emit(Events.Comment.Deleted, { threadId, messageId });
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
openThread(threadId) {
|
|
192
|
+
this.eventBus.emit(Events.Comment.ThreadOpened, { threadId });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Payload из broadcastWith() §3.3
|
|
197
|
+
*/
|
|
198
|
+
applyRemote(event) {
|
|
199
|
+
if (!event) return;
|
|
200
|
+
if (event.board_id != null && String(event.board_id) !== String(this.boardId)) return;
|
|
201
|
+
const action = event.action;
|
|
202
|
+
switch (action) {
|
|
203
|
+
case 'thread.created': {
|
|
204
|
+
const thread = event.thread;
|
|
205
|
+
if (thread?.id != null && this._appliedThreadIds.has(Number(thread.id))) break;
|
|
206
|
+
if (thread) {
|
|
207
|
+
this._upsertThread(thread);
|
|
208
|
+
if (thread.id != null) this._appliedThreadIds.add(Number(thread.id));
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 'thread.updated': {
|
|
213
|
+
const thread = event.thread;
|
|
214
|
+
if (thread) {
|
|
215
|
+
const prev = this.getThread(thread.id);
|
|
216
|
+
this._upsertThread({ ...prev, ...thread, ...(event.changes || {}) });
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'thread.resolved': {
|
|
221
|
+
const thread = event.thread;
|
|
222
|
+
if (thread) {
|
|
223
|
+
const prev = this.getThread(thread.id);
|
|
224
|
+
this._upsertThread({ ...prev, ...thread });
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case 'comment.created': {
|
|
229
|
+
const message = event.message;
|
|
230
|
+
if (message?.id != null && this._appliedMessageIds.has(Number(message.id))) break;
|
|
231
|
+
if (message?.thread_id != null) {
|
|
232
|
+
this._applyMessageToThread(message.thread_id, message);
|
|
233
|
+
if (message.id != null) this._appliedMessageIds.add(Number(message.id));
|
|
234
|
+
}
|
|
235
|
+
if (event.thread) {
|
|
236
|
+
const prev = this.getThread(event.thread.id);
|
|
237
|
+
this._upsertThread({ ...prev, ...event.thread });
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'comment.updated': {
|
|
242
|
+
const message = event.message;
|
|
243
|
+
if (message?.thread_id != null) {
|
|
244
|
+
this._replaceMessageInThread(message.thread_id, message);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 'comment.deleted': {
|
|
249
|
+
const message = event.message;
|
|
250
|
+
if (message?.thread_id != null && message?.id != null) {
|
|
251
|
+
const thread = this.getThread(message.thread_id);
|
|
252
|
+
if (thread?.messages?.items) {
|
|
253
|
+
thread.messages.items = thread.messages.items.filter((m) => m.id !== message.id);
|
|
254
|
+
thread.messages.count = thread.messages.items.length;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case 'thread.deleted': {
|
|
260
|
+
const deletedId = event.thread?.id;
|
|
261
|
+
if (deletedId != null) {
|
|
262
|
+
const nid = Number(deletedId);
|
|
263
|
+
this.threads.delete(nid);
|
|
264
|
+
this._appliedThreadIds.delete(nid);
|
|
265
|
+
this.eventBus.emit(Events.Comment.ThreadDeleted, { threadId: deletedId });
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
this.eventBus.emit(Events.Comment.RemoteUpdated, {
|
|
273
|
+
action: event.action,
|
|
274
|
+
boardId: event.board_id,
|
|
275
|
+
thread: event.thread,
|
|
276
|
+
message: event.message,
|
|
277
|
+
changes: event.changes,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_upsertThread(thread) {
|
|
282
|
+
if (!thread || thread.id == null) return;
|
|
283
|
+
const id = Number(thread.id);
|
|
284
|
+
const prev = this.threads.get(id);
|
|
285
|
+
const messages = thread.messages || prev?.messages;
|
|
286
|
+
this.threads.set(id, { ...prev, ...thread, messages });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_applyMessageToThread(threadId, message) {
|
|
290
|
+
const thread = this.getThread(threadId);
|
|
291
|
+
if (!thread) return;
|
|
292
|
+
if (!thread.messages) {
|
|
293
|
+
thread.messages = { count: 0, has_more: false, items: [] };
|
|
294
|
+
}
|
|
295
|
+
const items = thread.messages.items || [];
|
|
296
|
+
const idx = items.findIndex((m) => m.id === message.id);
|
|
297
|
+
if (idx >= 0) items[idx] = message;
|
|
298
|
+
else items.push(message);
|
|
299
|
+
thread.messages.items = items;
|
|
300
|
+
thread.messages.count = items.length;
|
|
301
|
+
thread.messages_count = items.length;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
_replaceMessageInThread(threadId, message) {
|
|
305
|
+
const thread = this.getThread(threadId);
|
|
306
|
+
if (!thread?.messages?.items) return;
|
|
307
|
+
const idx = thread.messages.items.findIndex((m) => m.id === message.id);
|
|
308
|
+
if (idx >= 0) thread.messages.items[idx] = message;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
_onObjectDeleted({ objectId }) {
|
|
312
|
+
if (!objectId) return;
|
|
313
|
+
for (const thread of this.threads.values()) {
|
|
314
|
+
if (thread.detached || thread.anchor_object_id !== objectId) continue;
|
|
315
|
+
const wx = thread.x;
|
|
316
|
+
const wy = thread.y;
|
|
317
|
+
thread.detached = true;
|
|
318
|
+
thread.x = wx;
|
|
319
|
+
thread.y = wy;
|
|
320
|
+
thread.anchor_object_id = null;
|
|
321
|
+
thread.anchor_dx = null;
|
|
322
|
+
thread.anchor_dy = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* World-позиция пина для отрисовки
|
|
328
|
+
*/
|
|
329
|
+
getThreadWorldPosition(thread, core) {
|
|
330
|
+
if (!thread) return null;
|
|
331
|
+
if (thread.detached || !thread.anchor_object_id) {
|
|
332
|
+
return { x: thread.x, y: thread.y };
|
|
333
|
+
}
|
|
334
|
+
const pos = { objectId: thread.anchor_object_id, position: null };
|
|
335
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, pos);
|
|
336
|
+
if (!pos.position) {
|
|
337
|
+
return { x: thread.x, y: thread.y };
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
x: pos.position.x + (thread.anchor_dx || 0),
|
|
341
|
+
y: pos.position.y + (thread.anchor_dy || 0),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -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
|
+
}
|