@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.
- package/package.json +3 -1
- package/src/core/PixiEngine.js +34 -5
- package/src/core/bootstrap/CoreInitializer.js +4 -0
- package/src/core/commands/CreateConnectorCommand.js +25 -0
- package/src/core/commands/GroupMoveCommand.js +2 -2
- package/src/core/commands/MoveObjectCommand.js +1 -1
- package/src/core/commands/UpdateConnectorCommand.js +38 -0
- package/src/core/events/Events.js +1 -0
- package/src/mindmap/MindmapCompoundContract.js +1 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +14 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +18 -0
- package/src/objects/ConnectorObject.js +85 -0
- package/src/objects/DrawingObject.js +47 -0
- package/src/objects/MindmapObject.js +21 -3
- package/src/objects/NoteObject.js +16 -8
- package/src/objects/ObjectFactory.js +3 -1
- package/src/objects/ShapeObject.js +1 -1
- package/src/services/ConnectorBindingResolver.js +204 -0
- package/src/services/ai/AiClient.js +30 -2
- package/src/services/ai/ChatSessionController.js +1 -0
- package/src/tools/ToolManager.js +3 -0
- package/src/tools/manager/PointerGestureController.js +206 -0
- package/src/tools/manager/ToolEventRouter.js +10 -0
- package/src/tools/manager/ToolManagerGuards.js +3 -1
- package/src/tools/manager/ToolManagerLifecycle.js +70 -58
- package/src/tools/object-tools/ConnectorTool.js +147 -0
- package/src/tools/object-tools/PlacementTool.js +2 -2
- package/src/tools/object-tools/connector/ConnectorDragController.js +296 -0
- package/src/tools/object-tools/connector/connectorGesture.js +108 -0
- package/src/tools/object-tools/placement/GhostController.js +4 -4
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -2
- package/src/tools/object-tools/placement/PlacementInputRouter.js +5 -5
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +11 -2
- package/src/tools/object-tools/selection/SelectInputRouter.js +33 -4
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +12 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +3 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +1 -2
- package/src/tools/object-tools/selection/TextEditorSyncService.js +4 -4
- package/src/tools/object-tools/selection/TextInlineEditorController.js +21 -3
- package/src/tools/object-tools/selection/TransformInteractionController.js +4 -6
- package/src/ui/HtmlTextLayer.js +212 -5
- package/src/ui/animation/HoverLiftController.js +395 -0
- package/src/ui/chat/ChatComposer.js +0 -5
- package/src/ui/chat/ChatWindow.js +167 -35
- package/src/ui/chat/icons.js +17 -1
- package/src/ui/connectors/ConnectionAnchorsLayer.js +231 -0
- package/src/ui/connectors/ConnectorLayer.js +251 -0
- package/src/ui/handles/HandlesDomRenderer.js +11 -7
- package/src/ui/handles/HandlesInteractionController.js +65 -34
- package/src/ui/handles/HandlesPositioningService.js +41 -6
- package/src/ui/mindmap/MindmapCollapseGraph.js +169 -0
- package/src/ui/mindmap/MindmapCollapseLayer.js +380 -0
- package/src/ui/mindmap/MindmapConnectionLayer.js +50 -25
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +223 -2
- package/src/ui/mindmap/MindmapLayoutConfig.js +12 -0
- package/src/ui/styles/chat.css +1 -0
- package/src/ui/styles/toolbar.css +6 -0
- package/src/ui/styles/workspace.css +83 -21
- package/src/ui/toolbar/ToolbarPopupsController.js +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { Events } from '../../core/events/Events.js';
|
|
3
|
+
import { ConnectorBindingResolver, distanceToSegment } from '../../services/ConnectorBindingResolver.js';
|
|
4
|
+
|
|
5
|
+
const HIT_TEST_SCREEN_PX = 8;
|
|
6
|
+
const ARROW_LEN = 12;
|
|
7
|
+
const ARROW_HALF = 4;
|
|
8
|
+
const DASH_LEN = 8;
|
|
9
|
+
const GAP_LEN = 5;
|
|
10
|
+
|
|
11
|
+
function asArray(value) {
|
|
12
|
+
return Array.isArray(value) ? value : [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Рисует пунктирную линию через последовательность moveTo/lineTo.
|
|
17
|
+
* @param {PIXI.Graphics} g
|
|
18
|
+
*/
|
|
19
|
+
function drawDashedLine(g, x1, y1, x2, y2) {
|
|
20
|
+
const dx = x2 - x1;
|
|
21
|
+
const dy = y2 - y1;
|
|
22
|
+
const len = Math.hypot(dx, dy);
|
|
23
|
+
if (len < 1e-6) return;
|
|
24
|
+
const ux = dx / len;
|
|
25
|
+
const uy = dy / len;
|
|
26
|
+
let dist = 0;
|
|
27
|
+
let drawing = true;
|
|
28
|
+
g.moveTo(Math.round(x1), Math.round(y1));
|
|
29
|
+
while (dist < len) {
|
|
30
|
+
const step = drawing ? DASH_LEN : GAP_LEN;
|
|
31
|
+
const next = Math.min(dist + step, len);
|
|
32
|
+
const px = x1 + ux * next;
|
|
33
|
+
const py = y1 + uy * next;
|
|
34
|
+
if (drawing) {
|
|
35
|
+
g.lineTo(Math.round(px), Math.round(py));
|
|
36
|
+
} else {
|
|
37
|
+
g.moveTo(Math.round(px), Math.round(py));
|
|
38
|
+
}
|
|
39
|
+
dist = next;
|
|
40
|
+
drawing = !drawing;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Рисует треугольный наконечник стрелки в точке `to`, направление from→to.
|
|
46
|
+
* Вызывать после g.lineStyle(0) не нужно — сбрасывает линию сам.
|
|
47
|
+
* @param {PIXI.Graphics} g
|
|
48
|
+
* @param {{ x: number, y: number }} from
|
|
49
|
+
* @param {{ x: number, y: number }} to
|
|
50
|
+
* @param {number} color PIXI-цвет
|
|
51
|
+
*/
|
|
52
|
+
function drawArrow(g, from, to, color) {
|
|
53
|
+
const dx = to.x - from.x;
|
|
54
|
+
const dy = to.y - from.y;
|
|
55
|
+
const len = Math.hypot(dx, dy);
|
|
56
|
+
if (len < 1e-6) return;
|
|
57
|
+
const ux = dx / len;
|
|
58
|
+
const uy = dy / len;
|
|
59
|
+
// перпендикуляр
|
|
60
|
+
const px = -uy;
|
|
61
|
+
const py = ux;
|
|
62
|
+
const bx = to.x - ux * ARROW_LEN;
|
|
63
|
+
const by = to.y - uy * ARROW_LEN;
|
|
64
|
+
g.lineStyle(0);
|
|
65
|
+
g.beginFill(color, 1);
|
|
66
|
+
g.drawPolygon([
|
|
67
|
+
Math.round(to.x), Math.round(to.y),
|
|
68
|
+
Math.round(bx + px * ARROW_HALF), Math.round(by + py * ARROW_HALF),
|
|
69
|
+
Math.round(bx - px * ARROW_HALF), Math.round(by - py * ARROW_HALF),
|
|
70
|
+
]);
|
|
71
|
+
g.endFill();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ConnectorLayer — слой рендера универсальных коннекторов.
|
|
76
|
+
*
|
|
77
|
+
* Паттерн: MindmapConnectionLayer (один PIXI.Graphics, полная перерисовка на события).
|
|
78
|
+
* Рисует connector-объекты из state.objects в worldLayer.
|
|
79
|
+
* Резолвинг end-point: ConnectorBindingResolver.resolve() двумя проходами
|
|
80
|
+
* (грубый → точный) для корректной проекции на кромку при isExact=false.
|
|
81
|
+
*/
|
|
82
|
+
export class ConnectorLayer {
|
|
83
|
+
/**
|
|
84
|
+
* @param {Object} eventBus Экземпляр EventBus
|
|
85
|
+
* @param {Object} core Экземпляр CoreMoodBoard
|
|
86
|
+
*/
|
|
87
|
+
constructor(eventBus, core) {
|
|
88
|
+
this.eventBus = eventBus;
|
|
89
|
+
this.core = core;
|
|
90
|
+
this.graphics = null;
|
|
91
|
+
this.subscriptions = [];
|
|
92
|
+
this._eventsAttached = false;
|
|
93
|
+
/** @type {Array<{ id: string, start: {x,y}, end: {x,y} }>} */
|
|
94
|
+
this._lastSegments = [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Инициализирует слой: подписки на события и первый рендер. */
|
|
98
|
+
attach() {
|
|
99
|
+
if (!this.core?.pixi) return;
|
|
100
|
+
if (!this._eventsAttached) {
|
|
101
|
+
this._attachEvents();
|
|
102
|
+
}
|
|
103
|
+
this.updateAll();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Уничтожает слой: отписка от событий и очистка PIXI-объектов. */
|
|
107
|
+
destroy() {
|
|
108
|
+
this._detachEvents();
|
|
109
|
+
if (this.graphics) {
|
|
110
|
+
this.graphics.clear();
|
|
111
|
+
this.graphics.removeFromParent();
|
|
112
|
+
this.graphics.destroy();
|
|
113
|
+
this.graphics = null;
|
|
114
|
+
}
|
|
115
|
+
this._lastSegments = [];
|
|
116
|
+
this.eventBus = null;
|
|
117
|
+
this.core = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_attachEvents() {
|
|
121
|
+
if (this._eventsAttached) return;
|
|
122
|
+
const bindings = [
|
|
123
|
+
[Events.Object.Created, () => this.updateAll()],
|
|
124
|
+
[Events.Object.Deleted, () => this.updateAll()],
|
|
125
|
+
[Events.Object.Updated, () => this.updateAll()],
|
|
126
|
+
[Events.Object.StateChanged, () => this.updateAll()],
|
|
127
|
+
[Events.Tool.DragUpdate, () => this.updateAll()],
|
|
128
|
+
[Events.Tool.DragEnd, () => this.updateAll()],
|
|
129
|
+
[Events.Tool.ResizeUpdate, () => this.updateAll()],
|
|
130
|
+
[Events.Tool.ResizeEnd, () => this.updateAll()],
|
|
131
|
+
[Events.Tool.GroupDragUpdate, () => this.updateAll()],
|
|
132
|
+
[Events.Tool.GroupResizeUpdate, () => this.updateAll()],
|
|
133
|
+
[Events.Tool.RotateUpdate, () => this.updateAll()],
|
|
134
|
+
[Events.Tool.PanUpdate, () => this.updateAll()],
|
|
135
|
+
[Events.UI.ZoomPercent, () => this.updateAll()],
|
|
136
|
+
[Events.History.Changed, () => this.updateAll()],
|
|
137
|
+
[Events.Board.Loaded, () => this.updateAll()],
|
|
138
|
+
];
|
|
139
|
+
bindings.forEach(([event, handler]) => {
|
|
140
|
+
this.eventBus.on(event, handler);
|
|
141
|
+
this.subscriptions.push([event, handler]);
|
|
142
|
+
});
|
|
143
|
+
this._eventsAttached = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_detachEvents() {
|
|
147
|
+
if (typeof this.eventBus?.off !== 'function') {
|
|
148
|
+
this.subscriptions = [];
|
|
149
|
+
this._eventsAttached = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.subscriptions.forEach(([event, handler]) => this.eventBus.off(event, handler));
|
|
153
|
+
this.subscriptions = [];
|
|
154
|
+
this._eventsAttached = false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Перерисовывает все коннекторы из state. */
|
|
158
|
+
updateAll() {
|
|
159
|
+
const objects = asArray(this.core?.state?.state?.objects);
|
|
160
|
+
const connectors = objects.filter((o) => o?.type === 'connector');
|
|
161
|
+
|
|
162
|
+
if (connectors.length === 0) {
|
|
163
|
+
if (this.graphics) this.graphics.clear();
|
|
164
|
+
this._lastSegments = [];
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!this.graphics) {
|
|
169
|
+
this.graphics = new PIXI.Graphics();
|
|
170
|
+
this.graphics.name = 'connector-layer';
|
|
171
|
+
this.graphics.zIndex = 3;
|
|
172
|
+
const world = this.core?.pixi?.worldLayer || this.core?.pixi?.app?.stage;
|
|
173
|
+
world?.addChild?.(this.graphics);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const byId = new Map(objects.map((o) => [o.id, o]));
|
|
177
|
+
const g = this.graphics;
|
|
178
|
+
g.clear();
|
|
179
|
+
this._lastSegments = [];
|
|
180
|
+
|
|
181
|
+
connectors.forEach((connector) => {
|
|
182
|
+
const style = connector?.properties?.style ?? {};
|
|
183
|
+
const startTerm = connector?.properties?.start;
|
|
184
|
+
const endTerm = connector?.properties?.end;
|
|
185
|
+
if (!startTerm || !endTerm) return;
|
|
186
|
+
|
|
187
|
+
const startTarget = startTerm.boundId ? (byId.get(startTerm.boundId) ?? null) : null;
|
|
188
|
+
const endTarget = endTerm.boundId ? (byId.get(endTerm.boundId) ?? null) : null;
|
|
189
|
+
|
|
190
|
+
// Двухпроходное резолвание для корректной проекции isExact=false:
|
|
191
|
+
// проход 1 — грубые точки (без взаимной информации)
|
|
192
|
+
const roughStart = ConnectorBindingResolver.resolve(startTerm, startTarget, null);
|
|
193
|
+
const roughEnd = ConnectorBindingResolver.resolve(endTerm, endTarget, null);
|
|
194
|
+
// проход 2 — уточнение с кромочной проекцией
|
|
195
|
+
const start = ConnectorBindingResolver.resolve(startTerm, startTarget, roughEnd);
|
|
196
|
+
const end = ConnectorBindingResolver.resolve(endTerm, endTarget, start);
|
|
197
|
+
|
|
198
|
+
const sx = Math.round(start.x);
|
|
199
|
+
const sy = Math.round(start.y);
|
|
200
|
+
const ex = Math.round(end.x);
|
|
201
|
+
const ey = Math.round(end.y);
|
|
202
|
+
|
|
203
|
+
const color = style.stroke ?? 0x2563EB;
|
|
204
|
+
const width = style.width ?? 2;
|
|
205
|
+
const isDash = !!style.dash;
|
|
206
|
+
const head = style.head ?? { start: false, end: true };
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
g.lineStyle({ width, color, alpha: 1, alignment: 0, cap: 'round', join: 'round' });
|
|
210
|
+
} catch (_) {
|
|
211
|
+
g.lineStyle(width, color, 1, 0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (isDash) {
|
|
215
|
+
drawDashedLine(g, sx, sy, ex, ey);
|
|
216
|
+
} else {
|
|
217
|
+
g.moveTo(sx, sy);
|
|
218
|
+
g.lineTo(ex, ey);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (head?.end) drawArrow(g, { x: sx, y: sy }, { x: ex, y: ey }, color);
|
|
222
|
+
if (head?.start) drawArrow(g, { x: ex, y: ey }, { x: sx, y: sy }, color);
|
|
223
|
+
|
|
224
|
+
this._lastSegments.push({ id: connector.id, start: { x: sx, y: sy }, end: { x: ex, y: ey } });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Возвращает id ближайшего коннектора, если worldPoint в пределах порога.
|
|
230
|
+
* Порог задан в экранных пикселях, пересчитывается в world через текущий scale.
|
|
231
|
+
*
|
|
232
|
+
* @param {{ x: number, y: number }} worldPoint
|
|
233
|
+
* @returns {string|null}
|
|
234
|
+
*/
|
|
235
|
+
hitTest(worldPoint) {
|
|
236
|
+
if (this._lastSegments.length === 0) return null;
|
|
237
|
+
// worldLayer.scale.x = zoom; 1 screen px = 1/scale world units
|
|
238
|
+
const scale = this.core?.pixi?.worldLayer?.scale?.x ?? 1;
|
|
239
|
+
const worldThreshold = HIT_TEST_SCREEN_PX / scale;
|
|
240
|
+
let closest = null;
|
|
241
|
+
let minDist = worldThreshold;
|
|
242
|
+
for (const seg of this._lastSegments) {
|
|
243
|
+
const d = distanceToSegment(worldPoint, seg.start, seg.end);
|
|
244
|
+
if (d < minDist) {
|
|
245
|
+
minDist = d;
|
|
246
|
+
closest = seg.id;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return closest;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -1223,6 +1223,8 @@ export class HandlesDomRenderer {
|
|
|
1223
1223
|
transformOrigin: 'center center',
|
|
1224
1224
|
transform: `rotate(${rotation}deg)`,
|
|
1225
1225
|
});
|
|
1226
|
+
box.style.setProperty('--box-w', `${width}px`);
|
|
1227
|
+
box.style.setProperty('--box-h', `${height}px`);
|
|
1226
1228
|
this.host.layer.appendChild(box);
|
|
1227
1229
|
if (this.host._handlesSuppressed) {
|
|
1228
1230
|
this.host.visible = true;
|
|
@@ -1252,12 +1254,12 @@ export class HandlesDomRenderer {
|
|
|
1252
1254
|
h.style.cursor = cursor;
|
|
1253
1255
|
});
|
|
1254
1256
|
h.addEventListener('mouseleave', () => {
|
|
1255
|
-
h.style.background =
|
|
1257
|
+
h.style.background = '#ffffff';
|
|
1256
1258
|
h.style.borderColor = HANDLES_ACCENT_COLOR;
|
|
1257
1259
|
});
|
|
1258
1260
|
|
|
1259
1261
|
if (!isNonResizableTarget) {
|
|
1260
|
-
h.addEventListener('
|
|
1262
|
+
h.addEventListener('pointerdown', (e) => this.host._onHandleDown(e, box));
|
|
1261
1263
|
}
|
|
1262
1264
|
|
|
1263
1265
|
box.appendChild(h);
|
|
@@ -1286,7 +1288,7 @@ export class HandlesDomRenderer {
|
|
|
1286
1288
|
});
|
|
1287
1289
|
if (isNonResizableTarget) e.dataset.lockedHidden = '1';
|
|
1288
1290
|
if (!isNonResizableTarget) {
|
|
1289
|
-
e.addEventListener('
|
|
1291
|
+
e.addEventListener('pointerdown', (evt) => this.host._onEdgeResizeDown(evt));
|
|
1290
1292
|
}
|
|
1291
1293
|
box.appendChild(e);
|
|
1292
1294
|
};
|
|
@@ -1341,7 +1343,7 @@ export class HandlesDomRenderer {
|
|
|
1341
1343
|
svgEl.style.height = '100%';
|
|
1342
1344
|
svgEl.style.display = 'block';
|
|
1343
1345
|
}
|
|
1344
|
-
rotateHandle.addEventListener('
|
|
1346
|
+
rotateHandle.addEventListener('pointerdown', (e) => this.host._onRotateHandleDown(e, box));
|
|
1345
1347
|
}
|
|
1346
1348
|
box.appendChild(rotateHandle);
|
|
1347
1349
|
|
|
@@ -1626,7 +1628,7 @@ export class HandlesDomRenderer {
|
|
|
1626
1628
|
btn.style.left = `${Math.round(left + width + centerOffset)}px`;
|
|
1627
1629
|
}
|
|
1628
1630
|
btn.style.top = `${centerY}px`;
|
|
1629
|
-
btn.addEventListener('
|
|
1631
|
+
btn.addEventListener('pointerdown', (evt) => {
|
|
1630
1632
|
evt.preventDefault();
|
|
1631
1633
|
evt.stopPropagation();
|
|
1632
1634
|
});
|
|
@@ -1651,7 +1653,7 @@ export class HandlesDomRenderer {
|
|
|
1651
1653
|
const centerOffset = edgeGap + buttonRadius;
|
|
1652
1654
|
btn.style.left = `${centerX}px`;
|
|
1653
1655
|
btn.style.top = `${Math.round(top + height + centerOffset)}px`;
|
|
1654
|
-
btn.addEventListener('
|
|
1656
|
+
btn.addEventListener('pointerdown', (evt) => {
|
|
1655
1657
|
evt.preventDefault();
|
|
1656
1658
|
evt.stopPropagation();
|
|
1657
1659
|
});
|
|
@@ -1680,7 +1682,7 @@ export class HandlesDomRenderer {
|
|
|
1680
1682
|
showInModelButton.innerHTML = `${REVIT_SHOW_IN_MODEL_ICON_SVG}<span>Показать в модели</span>`;
|
|
1681
1683
|
showInModelButton.style.left = `${Math.round(left + width / 2)}px`;
|
|
1682
1684
|
showInModelButton.style.top = `${Math.round(top - 34)}px`;
|
|
1683
|
-
showInModelButton.addEventListener('
|
|
1685
|
+
showInModelButton.addEventListener('pointerdown', (evt) => {
|
|
1684
1686
|
evt.preventDefault();
|
|
1685
1687
|
evt.stopPropagation();
|
|
1686
1688
|
});
|
|
@@ -1702,6 +1704,8 @@ export class HandlesDomRenderer {
|
|
|
1702
1704
|
repositionBoxChildren(box) {
|
|
1703
1705
|
const width = parseFloat(box.style.width);
|
|
1704
1706
|
const height = parseFloat(box.style.height);
|
|
1707
|
+
box.style.setProperty('--box-w', `${width}px`);
|
|
1708
|
+
box.style.setProperty('--box-h', `${height}px`);
|
|
1705
1709
|
const cx = width / 2;
|
|
1706
1710
|
const cy = height / 2;
|
|
1707
1711
|
|
|
@@ -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: (
|
|
46
|
-
y: (
|
|
47
|
-
width:
|
|
48
|
-
height:
|
|
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 = (
|
|
332
|
-
const worldY = (
|
|
333
|
-
const worldW =
|
|
334
|
-
const worldH =
|
|
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('
|
|
363
|
-
document.removeEventListener('
|
|
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 = (
|
|
375
|
-
const worldY = (
|
|
376
|
-
const worldW =
|
|
377
|
-
const worldH =
|
|
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 =
|
|
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('
|
|
423
|
-
document.addEventListener('
|
|
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 = (
|
|
604
|
-
const worldY = (
|
|
605
|
-
const worldW =
|
|
606
|
-
const worldH =
|
|
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('
|
|
628
|
-
document.removeEventListener('
|
|
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 = (
|
|
640
|
-
const worldY = (
|
|
641
|
-
const worldW =
|
|
642
|
-
const worldH =
|
|
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 =
|
|
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('
|
|
674
|
-
document.addEventListener('
|
|
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('
|
|
745
|
-
document.removeEventListener('
|
|
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('
|
|
770
|
-
document.addEventListener('
|
|
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: (
|
|
106
|
-
y: (
|
|
107
|
-
width:
|
|
108
|
-
height:
|
|
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: (
|
|
203
|
-
y: (
|
|
237
|
+
x: (screenX - tx) / s,
|
|
238
|
+
y: (screenY - ty) / s,
|
|
204
239
|
};
|
|
205
240
|
}
|
|
206
241
|
}
|