@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
@@ -0,0 +1,204 @@
1
+ /**
2
+ * ConnectorBindingResolver — чистая логика преобразования терминала в world-точку.
3
+ *
4
+ * Реализует алгоритм из раздела 4 CONNECTORS.md:
5
+ * 1. isPrecise=false → центр объекта
6
+ * 2. isPrecise=true → worldAnchor = topLeft + { anchor.x·w, anchor.y·h }
7
+ * 3. isExact=false → проекция на кромку AABB через Liang–Barsky;
8
+ * для повёрнутого объекта: луч переводится в локальные координаты.
9
+ * 4. isExact=true → точная world-точка без отсечения
10
+ * 5. Свободный терминал { point } → возвращается как есть
11
+ *
12
+ * Нет зависимостей от PIXI; только чистые математические операции.
13
+ */
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Вспомогательные функции
17
+
18
+ /**
19
+ * Поворачивает вектор на угол (в радианах).
20
+ * @param {{ x: number, y: number }} pt
21
+ * @param {number} angle радианы, положительный = CCW
22
+ * @returns {{ x: number, y: number }}
23
+ */
24
+ function rotateVector(pt, angle) {
25
+ const cos = Math.cos(angle);
26
+ const sin = Math.sin(angle);
27
+ return {
28
+ x: pt.x * cos - pt.y * sin,
29
+ y: pt.x * sin + pt.y * cos,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Liang–Barsky для AABB с центром в начале координат (half-widths hw × hh).
35
+ *
36
+ * `from` — точка внутри (или на границе) прямоугольника [-hw..hw] × [-hh..hh].
37
+ * `to` — произвольная точка; возвращает точку выхода из прямоугольника по лучу from→to.
38
+ *
39
+ * Если from === to, возвращает from (на границе) через simple clamp.
40
+ *
41
+ * @param {{ x: number, y: number }} from
42
+ * @param {{ x: number, y: number }} to
43
+ * @param {number} hw half-width (> 0)
44
+ * @param {number} hh half-height (> 0)
45
+ * @returns {{ x: number, y: number }}
46
+ */
47
+ function clipRayToAABB(from, to, hw, hh) {
48
+ const dx = to.x - from.x;
49
+ const dy = to.y - from.y;
50
+
51
+ if (Math.abs(dx) < 1e-10 && Math.abs(dy) < 1e-10) {
52
+ // Вырожденный луч — clamp from на границу
53
+ const cx = Math.max(-hw, Math.min(hw, from.x));
54
+ const cy = Math.max(-hh, Math.min(hh, from.y));
55
+ return { x: cx, y: cy };
56
+ }
57
+
58
+ // Минимальный t такой, что p(t) = from + t*(to−from) выходит за AABB
59
+ let tExit = 1.0;
60
+
61
+ if (Math.abs(dx) > 1e-10) {
62
+ const tEdge = (dx > 0 ? hw - from.x : -hw - from.x) / dx;
63
+ if (tEdge >= 0) tExit = Math.min(tExit, tEdge);
64
+ }
65
+ if (Math.abs(dy) > 1e-10) {
66
+ const tEdge = (dy > 0 ? hh - from.y : -hh - from.y) / dy;
67
+ if (tEdge >= 0) tExit = Math.min(tExit, tEdge);
68
+ }
69
+
70
+ return {
71
+ x: from.x + tExit * dx,
72
+ y: from.y + tExit * dy,
73
+ };
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export class ConnectorBindingResolver {
79
+ /**
80
+ * Разрешает терминал в world-point.
81
+ *
82
+ * @param {Object} terminal
83
+ * Привязанный: { boundId, anchor:{x,y}, isPrecise, isExact }
84
+ * Свободный: { point:{x,y} }
85
+ * @param {Object|null} target
86
+ * Объект из state.objects с полями: position:{x,y}, width, height, rotation?
87
+ * @param {{ x: number, y: number }|null} otherTerminalWorld
88
+ * Уже разрешённая world-точка противоположного конца.
89
+ * Используется при isExact=false для проекции на кромку.
90
+ * Если null — отсечение не производится, возвращается precisePoint.
91
+ * @returns {{ x: number, y: number }}
92
+ */
93
+ static resolve(terminal, target, otherTerminalWorld = null) {
94
+ // --- Свободный терминал ---
95
+ if (!terminal?.boundId) {
96
+ return { x: terminal?.point?.x ?? 0, y: terminal?.point?.y ?? 0 };
97
+ }
98
+
99
+ if (!target) {
100
+ return { x: 0, y: 0 };
101
+ }
102
+
103
+ const left = target.position?.x ?? 0;
104
+ const top = target.position?.y ?? 0;
105
+ const width = target.width ?? target.properties?.width ?? 0;
106
+ const height = target.height ?? target.properties?.height ?? 0;
107
+ const angle = target.rotation ?? target.properties?.rotation ?? 0;
108
+
109
+ const cx = left + width / 2;
110
+ const cy = top + height / 2;
111
+ const hw = width / 2;
112
+ const hh = height / 2;
113
+
114
+ // --- Точка привязки в локальных координатах (начало в центре объекта) ---
115
+ // isPrecise=false → центр объекта (0, 0) в локальных
116
+ // isPrecise=true → anchor.x·w − w/2, anchor.y·h − h/2
117
+ let localAnchorX = 0;
118
+ let localAnchorY = 0;
119
+ if (terminal.isPrecise) {
120
+ const ax = terminal.anchor?.x ?? 0.5;
121
+ const ay = terminal.anchor?.y ?? 0.5;
122
+ localAnchorX = ax * width - hw;
123
+ localAnchorY = ay * height - hh;
124
+ }
125
+
126
+ // --- precisePoint в world-space ---
127
+ const worldPrecise = angle !== 0
128
+ ? {
129
+ x: cx + rotateVector({ x: localAnchorX, y: localAnchorY }, angle).x,
130
+ y: cy + rotateVector({ x: localAnchorX, y: localAnchorY }, angle).y,
131
+ }
132
+ : { x: cx + localAnchorX, y: cy + localAnchorY };
133
+
134
+ // --- isExact=true → возвращаем точную точку без отсечения ---
135
+ if (terminal.isExact) {
136
+ return worldPrecise;
137
+ }
138
+
139
+ // --- isExact=false → проекция на кромку AABB ---
140
+ if (!otherTerminalWorld) {
141
+ // Нет информации о другом конце — вернуть precisePoint
142
+ return worldPrecise;
143
+ }
144
+
145
+ if (hw <= 0 || hh <= 0) {
146
+ return worldPrecise;
147
+ }
148
+
149
+ // Переводим противоположный терминал в локальную систему координат цели
150
+ const ox = otherTerminalWorld.x - cx;
151
+ const oy = otherTerminalWorld.y - cy;
152
+ const localOther = angle !== 0
153
+ ? rotateVector({ x: ox, y: oy }, -angle)
154
+ : { x: ox, y: oy };
155
+
156
+ // Обрезаем луч localAnchor → localOther по AABB
157
+ const exitLocal = clipRayToAABB(
158
+ { x: localAnchorX, y: localAnchorY },
159
+ localOther,
160
+ hw,
161
+ hh
162
+ );
163
+
164
+ // Возвращаем в world-space
165
+ const worldExit = angle !== 0
166
+ ? {
167
+ x: cx + rotateVector(exitLocal, angle).x,
168
+ y: cy + rotateVector(exitLocal, angle).y,
169
+ }
170
+ : { x: cx + exitLocal.x, y: cy + exitLocal.y };
171
+
172
+ return worldExit;
173
+ }
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /**
179
+ * Расстояние от точки до отрезка [a, b].
180
+ * Используется в ConnectorLayer.hitTest (Фаза 2).
181
+ *
182
+ * @param {{ x: number, y: number }} point
183
+ * @param {{ x: number, y: number }} a
184
+ * @param {{ x: number, y: number }} b
185
+ * @returns {number}
186
+ */
187
+ export function distanceToSegment(point, a, b) {
188
+ const dx = b.x - a.x;
189
+ const dy = b.y - a.y;
190
+ const lenSq = dx * dx + dy * dy;
191
+
192
+ if (lenSq < 1e-10) {
193
+ return Math.hypot(point.x - a.x, point.y - a.y);
194
+ }
195
+
196
+ const t = Math.max(0, Math.min(1,
197
+ ((point.x - a.x) * dx + (point.y - a.y) * dy) / lenSq
198
+ ));
199
+
200
+ return Math.hypot(
201
+ point.x - (a.x + t * dx),
202
+ point.y - (a.y + t * dy)
203
+ );
204
+ }
@@ -115,17 +115,20 @@ export class AiClient {
115
115
  * @param {number} [args.seed]
116
116
  * @param {string} [args.mimeType]
117
117
  * @param {string} [args.model]
118
+ * @param {File[]} [args.referenceImages]
118
119
  * @param {AbortSignal} [args.signal]
119
120
  * @returns {Promise<{operationId: string, imageBase64: string, mimeType: string}>}
120
121
  */
121
- async generateImage({ signal, ...payload }) {
122
+ async generateImage({ signal, referenceImages: files, ...payload }) {
123
+ const referenceImages = await filesToBase64(files);
124
+ const body = referenceImages ? { ...payload, referenceImages } : payload;
122
125
  const res = await this._fetch(`${this._baseUrl}/yandex-art/image`, {
123
126
  method: 'POST',
124
127
  headers: {
125
128
  'Content-Type': 'application/json',
126
129
  'Accept': 'application/json'
127
130
  },
128
- body: JSON.stringify(payload),
131
+ body: JSON.stringify(body),
129
132
  signal
130
133
  });
131
134
  if (!res.ok) {
@@ -222,3 +225,28 @@ async function safeReadError(res) {
222
225
  return res.statusText;
223
226
  }
224
227
  }
228
+
229
+ /**
230
+ * Конвертирует массив File в [{mimeType, data}] с base64-encoded данными.
231
+ * Возвращает undefined, если массив пустой или не передан.
232
+ *
233
+ * @param {File[]|undefined} files
234
+ * @returns {Promise<Array<{mimeType: string, data: string}>|undefined>}
235
+ */
236
+ async function filesToBase64(files) {
237
+ if (!Array.isArray(files) || files.length === 0) return undefined;
238
+ return Promise.all(
239
+ files.map(async (file) => {
240
+ const buffer = await file.arrayBuffer();
241
+ const bytes = new Uint8Array(buffer);
242
+ let binary = '';
243
+ for (let i = 0; i < bytes.length; i++) {
244
+ binary += String.fromCharCode(bytes[i]);
245
+ }
246
+ return {
247
+ mimeType: file.type || 'image/png',
248
+ data: btoa(binary)
249
+ };
250
+ })
251
+ );
252
+ }
@@ -154,6 +154,7 @@ export class ChatSessionController {
154
154
  widthRatio: options.widthRatio,
155
155
  heightRatio: options.heightRatio,
156
156
  model: options.model,
157
+ referenceImages: options.referenceImages,
157
158
  signal: abort.signal
158
159
  })
159
160
  .then((result) => {
@@ -1,5 +1,6 @@
1
1
  import { Events } from '../core/events/Events.js';
2
2
  import cursorDefaultSvg from '../assets/icons/cursor-default.svg?raw';
3
+ import { PointerGestureController } from './manager/PointerGestureController.js';
3
4
  import { ToolActivationController } from './manager/ToolActivationController.js';
4
5
  import { ToolEventRouter } from './manager/ToolEventRouter.js';
5
6
  import { ToolManagerGuards } from './manager/ToolManagerGuards.js';
@@ -42,12 +43,14 @@ export class ToolManager {
42
43
  this.lastMousePos = null;
43
44
  this.isMouseOverContainer = false;
44
45
  this._originalPixiCursorStyles = null;
46
+ this._lastPointerType = null;
45
47
 
46
48
  // Устанавливаем курсор по умолчанию на контейнер, если инструмент ещё не активирован
47
49
  if (this.container) {
48
50
  this.container.style.cursor = DEFAULT_CURSOR; // пусто → берётся глобальный CSS-курсор
49
51
  }
50
52
 
53
+ this.gestures = new PointerGestureController(this);
51
54
  this.initEventListeners();
52
55
  }
53
56
 
@@ -0,0 +1,206 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+
3
+ const PINCH_SCALE_MIN = 0.02;
4
+ const PINCH_SCALE_MAX = 5;
5
+ const LONG_PRESS_MS = 500;
6
+ const LONG_PRESS_MOVE_THRESHOLD = 10;
7
+ const DOUBLE_TAP_MS = 300;
8
+ const DOUBLE_TAP_DIST = 24;
9
+
10
+ /**
11
+ * Единый роутер ввода для мыши / пера / тача через Pointer Events API.
12
+ * Добавляет pinch-zoom, двухпальцевый pan и long-press для тача,
13
+ * не меняя контракты существующих тул-методов.
14
+ */
15
+ export class PointerGestureController {
16
+ constructor(manager) {
17
+ this.manager = manager;
18
+ /** @type {Map<number, {x: number, y: number}>} — активные нажатые указатели */
19
+ this.pointers = new Map();
20
+ /** Подавлять одиночный указатель во время мультитач-жеста */
21
+ this.suppressSingle = false;
22
+
23
+ this._pinchPrevDist = null;
24
+ this._pinchPrevMid = null;
25
+
26
+ this._longPressTimer = null;
27
+ this._longPressDownPos = null;
28
+
29
+ this._lastTapTime = 0;
30
+ this._lastTapPos = null;
31
+ }
32
+
33
+ _dist(p1, p2) {
34
+ const dx = p2.x - p1.x;
35
+ const dy = p2.y - p1.y;
36
+ return Math.sqrt(dx * dx + dy * dy);
37
+ }
38
+
39
+ _mid(p1, p2) {
40
+ return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
41
+ }
42
+
43
+ _clearLongPress() {
44
+ if (this._longPressTimer !== null) {
45
+ clearTimeout(this._longPressTimer);
46
+ this._longPressTimer = null;
47
+ }
48
+ this._longPressDownPos = null;
49
+ }
50
+
51
+ _screenPos(e) {
52
+ const rect = this.manager.container.getBoundingClientRect();
53
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
54
+ }
55
+
56
+ onPointerDown(e) {
57
+ const manager = this.manager;
58
+ manager._lastPointerType = e.pointerType;
59
+
60
+ const pos = this._screenPos(e);
61
+ this.pointers.set(e.pointerId, pos);
62
+
63
+ if (this.pointers.size === 2) {
64
+ this._clearLongPress();
65
+ // Безопасно завершить одиночное взаимодействие до начала жеста
66
+ if (manager.activeTool && typeof manager.activeTool.onMouseUp === 'function') {
67
+ manager.activeTool.onMouseUp({ x: pos.x, y: pos.y, button: 0, originalEvent: e });
68
+ }
69
+ this.suppressSingle = true;
70
+ const pts = Array.from(this.pointers.values());
71
+ this._pinchPrevDist = this._dist(pts[0], pts[1]);
72
+ this._pinchPrevMid = this._mid(pts[0], pts[1]);
73
+ return;
74
+ }
75
+
76
+ if (this.suppressSingle) return;
77
+
78
+ // Long-press: таймер только для тача
79
+ if (e.pointerType === 'touch') {
80
+ this._longPressDownPos = pos;
81
+ this._longPressTimer = setTimeout(() => {
82
+ this._longPressTimer = null;
83
+ this._longPressDownPos = null;
84
+ if (manager.activeTool && typeof manager.activeTool.onContextMenu === 'function') {
85
+ manager.activeTool.onContextMenu({ x: pos.x, y: pos.y, originalEvent: e });
86
+ }
87
+ }, LONG_PRESS_MS);
88
+ }
89
+
90
+ manager.handleMouseDown(e);
91
+ }
92
+
93
+ onPointerMove(e) {
94
+ const manager = this.manager;
95
+ const isTracked = this.pointers.has(e.pointerId);
96
+
97
+ // Тач без зарегистрированного pointerdown — пропускаем
98
+ if (e.pointerType === 'touch' && !isTracked) return;
99
+
100
+ const pos = this._screenPos(e);
101
+ if (isTracked) {
102
+ this.pointers.set(e.pointerId, pos);
103
+ }
104
+
105
+ if (this.pointers.size >= 2) {
106
+ if (!isTracked) return;
107
+ const pts = Array.from(this.pointers.values());
108
+ const curDist = this._dist(pts[0], pts[1]);
109
+ const curMid = this._mid(pts[0], pts[1]);
110
+
111
+ if (this._pinchPrevDist !== null) {
112
+ const world = manager.core?.pixi?.worldLayer || manager.core?.pixi?.app?.stage;
113
+ if (world) {
114
+ const oldScale = world.scale.x || 1;
115
+ const factor = curDist / this._pinchPrevDist;
116
+ const newScale = Math.max(PINCH_SCALE_MIN, Math.min(PINCH_SCALE_MAX, oldScale * factor));
117
+
118
+ // Мировая точка под серединой пальцев — она должна остаться на месте
119
+ const worldX = (curMid.x - world.x) / oldScale;
120
+ const worldY = (curMid.y - world.y) / oldScale;
121
+ // Дельта двухпальцевого pan
122
+ const panDx = this._pinchPrevMid ? curMid.x - this._pinchPrevMid.x : 0;
123
+ const panDy = this._pinchPrevMid ? curMid.y - this._pinchPrevMid.y : 0;
124
+
125
+ world.scale.set(newScale);
126
+ // integer-контракт: round screen-space координаты
127
+ world.x = Math.round(curMid.x - worldX * newScale + panDx);
128
+ world.y = Math.round(curMid.y - worldY * newScale + panDy);
129
+
130
+ manager.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
131
+ manager.eventBus.emit(Events.Viewport.Changed);
132
+ }
133
+ }
134
+
135
+ this._pinchPrevDist = curDist;
136
+ this._pinchPrevMid = curMid;
137
+ return;
138
+ }
139
+
140
+ if (this.suppressSingle) return;
141
+
142
+ // Отменить long-press если палец сдвинулся более чем на порог
143
+ if (isTracked && e.pointerType === 'touch' && this._longPressDownPos) {
144
+ const dx = pos.x - this._longPressDownPos.x;
145
+ const dy = pos.y - this._longPressDownPos.y;
146
+ if (Math.sqrt(dx * dx + dy * dy) > LONG_PRESS_MOVE_THRESHOLD) {
147
+ this._clearLongPress();
148
+ }
149
+ }
150
+
151
+ manager.handleMouseMove(e);
152
+ }
153
+
154
+ onPointerUp(e) {
155
+ const manager = this.manager;
156
+ this._clearLongPress();
157
+
158
+ const hadPointer = this.pointers.has(e.pointerId);
159
+ this.pointers.delete(e.pointerId);
160
+
161
+ const wasSuppressed = this.suppressSingle;
162
+ if (this.pointers.size === 0) {
163
+ this.suppressSingle = false;
164
+ this._pinchPrevDist = null;
165
+ this._pinchPrevMid = null;
166
+ }
167
+
168
+ if (wasSuppressed || !hadPointer) return;
169
+
170
+ manager.handleMouseUp(e);
171
+
172
+ // Double-tap для тача: синтетический doubleClick после обычного up
173
+ if (e.pointerType === 'touch') {
174
+ const pos = this._screenPos(e);
175
+ const now = performance.now();
176
+ const elapsed = now - this._lastTapTime;
177
+ if (
178
+ elapsed < DOUBLE_TAP_MS &&
179
+ this._lastTapPos &&
180
+ this._dist(pos, this._lastTapPos) < DOUBLE_TAP_DIST
181
+ ) {
182
+ if (manager.activeTool && typeof manager.activeTool.onDoubleClick === 'function') {
183
+ // Если вторым тапом был случайно запущен resize через PIXI hitTest — отменяем до открытия редактора
184
+ if (manager.activeTool.isResizing || manager.activeTool.isGroupResizing) {
185
+ manager.activeTool.onMouseUp({ x: pos.x, y: pos.y, button: 0, originalEvent: e });
186
+ }
187
+ manager.activeTool.onDoubleClick({ x: pos.x, y: pos.y, originalEvent: e, target: e.target });
188
+ }
189
+ this._lastTapTime = 0;
190
+ this._lastTapPos = null;
191
+ return;
192
+ }
193
+ this._lastTapTime = now;
194
+ this._lastTapPos = pos;
195
+ }
196
+ }
197
+
198
+ destroy() {
199
+ this._clearLongPress();
200
+ this.pointers.clear();
201
+ this._pinchPrevDist = null;
202
+ this._pinchPrevMid = null;
203
+ this._lastTapTime = 0;
204
+ this._lastTapPos = null;
205
+ }
206
+ }
@@ -188,6 +188,8 @@ export class ToolEventRouter {
188
188
  }
189
189
 
190
190
  static handleDoubleClick(manager, event) {
191
+ // Нативный dblclick на тач-устройствах гасим — его заменяет синтетический double-tap из PointerGestureController
192
+ if (manager._lastPointerType === 'touch') return;
191
193
  if (!manager.activeTool) return;
192
194
 
193
195
  const toolEvent = createPointerEvent(manager, event, {
@@ -559,6 +561,10 @@ export class ToolEventRouter {
559
561
  static handleKeyDown(manager, event) {
560
562
  this.handleHotkeys(manager, event);
561
563
 
564
+ if (ToolManagerGuards.shouldIgnoreHotkeys(event)) {
565
+ return;
566
+ }
567
+
562
568
  if (!manager.activeTool) return;
563
569
 
564
570
  const toolEvent = {
@@ -578,6 +584,10 @@ export class ToolEventRouter {
578
584
  }
579
585
 
580
586
  static handleKeyUp(manager, event) {
587
+ if (ToolManagerGuards.shouldIgnoreHotkeys(event)) {
588
+ return;
589
+ }
590
+
581
591
  if (!manager.activeTool) return;
582
592
 
583
593
  const toolEvent = {
@@ -1,3 +1,5 @@
1
+ import { isInputElement } from '../../core/keyboard/KeyboardContextGuards.js';
2
+
1
3
  export class ToolManagerGuards {
2
4
  static isCursorLockedToActiveTool(manager) {
3
5
  return !!manager.activeTool && manager.activeTool.name !== 'select';
@@ -18,7 +20,7 @@ export class ToolManagerGuards {
18
20
  }
19
21
 
20
22
  static shouldIgnoreHotkeys(event) {
21
- return event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA';
23
+ return isInputElement(event.target);
22
24
  }
23
25
 
24
26
  static isAuxPanStart(manager, event) {