@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.
- 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 +1 -10
- package/src/ui/chat/ChatExtendedPromptModal.js +1 -12
- package/src/ui/chat/ChatWindow.js +167 -36
- package/src/ui/chat/ChatWindowRenderer.js +1 -8
- package/src/ui/chat/icons.js +17 -5
- 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 +2 -37
- 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,231 @@
|
|
|
1
|
+
import { Events } from '../../core/events/Events.js';
|
|
2
|
+
import { HandlesPositioningService } from '../handles/HandlesPositioningService.js';
|
|
3
|
+
import { ConnectorDragController } from '../../tools/object-tools/connector/ConnectorDragController.js';
|
|
4
|
+
|
|
5
|
+
const ALLOWED_TYPES = new Set(['shape', 'note', 'image', 'text', 'simple-text', 'file']);
|
|
6
|
+
|
|
7
|
+
export class ConnectionAnchorsLayer {
|
|
8
|
+
constructor(container, eventBus, core) {
|
|
9
|
+
this.container = container;
|
|
10
|
+
this.eventBus = eventBus;
|
|
11
|
+
this.core = core;
|
|
12
|
+
this.layer = null;
|
|
13
|
+
this.positioningService = new HandlesPositioningService(this);
|
|
14
|
+
|
|
15
|
+
this.subscriptions = [];
|
|
16
|
+
this._eventsAttached = false;
|
|
17
|
+
|
|
18
|
+
this.hoveredObjectId = null;
|
|
19
|
+
this._dragController = null;
|
|
20
|
+
this._onAnchorPointerDown = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
attach() {
|
|
24
|
+
if (!this.layer) {
|
|
25
|
+
this.layer = document.createElement('div');
|
|
26
|
+
this.layer.className = 'mb-connection-anchors-layer';
|
|
27
|
+
Object.assign(this.layer.style, {
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
left: '0',
|
|
30
|
+
top: '0',
|
|
31
|
+
width: '100%',
|
|
32
|
+
height: '100%',
|
|
33
|
+
pointerEvents: 'none',
|
|
34
|
+
zIndex: '35'
|
|
35
|
+
});
|
|
36
|
+
this.container.appendChild(this.layer);
|
|
37
|
+
|
|
38
|
+
this._dragController = new ConnectorDragController(this.core, this.eventBus);
|
|
39
|
+
this._onAnchorPointerDown = (e) => {
|
|
40
|
+
if (!e.target.dataset.connectorAnchor) return;
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
this._dragController.startFromAnchor(e);
|
|
44
|
+
};
|
|
45
|
+
this.layer.addEventListener('pointerdown', this._onAnchorPointerDown);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this._attachEvents();
|
|
49
|
+
this.update();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
destroy() {
|
|
53
|
+
this._detachEvents();
|
|
54
|
+
if (this._onAnchorPointerDown && this.layer) {
|
|
55
|
+
this.layer.removeEventListener('pointerdown', this._onAnchorPointerDown);
|
|
56
|
+
this._onAnchorPointerDown = null;
|
|
57
|
+
}
|
|
58
|
+
if (this._dragController) {
|
|
59
|
+
this._dragController.destroy();
|
|
60
|
+
this._dragController = null;
|
|
61
|
+
}
|
|
62
|
+
if (this.layer && this.layer.parentNode) {
|
|
63
|
+
this.layer.parentNode.removeChild(this.layer);
|
|
64
|
+
}
|
|
65
|
+
this.layer = null;
|
|
66
|
+
this.eventBus = null;
|
|
67
|
+
this.core = null;
|
|
68
|
+
this.container = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_attachEvents() {
|
|
72
|
+
if (this._eventsAttached) return;
|
|
73
|
+
|
|
74
|
+
const bindings = [
|
|
75
|
+
[Events.Object.Hover, (e) => {
|
|
76
|
+
this.hoveredObjectId = e.objectId || null;
|
|
77
|
+
this.update();
|
|
78
|
+
}],
|
|
79
|
+
[Events.Tool.SelectionAdd, () => this.update()],
|
|
80
|
+
[Events.Tool.SelectionRemove, () => this.update()],
|
|
81
|
+
[Events.Tool.SelectionClear, () => this.update()],
|
|
82
|
+
[Events.Object.Created, () => this.update()],
|
|
83
|
+
[Events.Object.Deleted, () => this.update()],
|
|
84
|
+
[Events.Object.Updated, () => this.update()],
|
|
85
|
+
[Events.Object.StateChanged, () => this.update()],
|
|
86
|
+
[Events.Tool.DragUpdate, () => this.update()],
|
|
87
|
+
[Events.Tool.DragEnd, () => this.update()],
|
|
88
|
+
[Events.Tool.ResizeUpdate, () => this.update()],
|
|
89
|
+
[Events.Tool.ResizeEnd, () => this.update()],
|
|
90
|
+
[Events.Tool.GroupDragUpdate, () => this.update()],
|
|
91
|
+
[Events.Tool.GroupResizeUpdate, () => this.update()],
|
|
92
|
+
[Events.Tool.RotateUpdate, () => this.update()],
|
|
93
|
+
[Events.Tool.PanUpdate, () => this.update()],
|
|
94
|
+
[Events.UI.ZoomPercent, () => this.update()],
|
|
95
|
+
[Events.History.Changed, () => this.update()],
|
|
96
|
+
[Events.Board.Loaded, () => this.update()]
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
bindings.forEach(([event, handler]) => {
|
|
100
|
+
this.eventBus.on(event, handler);
|
|
101
|
+
this.subscriptions.push([event, handler]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this._eventsAttached = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_detachEvents() {
|
|
108
|
+
if (typeof this.eventBus?.off !== 'function') {
|
|
109
|
+
this.subscriptions = [];
|
|
110
|
+
this._eventsAttached = false;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.subscriptions.forEach(([event, handler]) => {
|
|
114
|
+
this.eventBus.off(event, handler);
|
|
115
|
+
});
|
|
116
|
+
this.subscriptions = [];
|
|
117
|
+
this._eventsAttached = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_getSingleSelectionWorldBounds(id) {
|
|
121
|
+
const positionData = { objectId: id, position: null };
|
|
122
|
+
const sizeData = { objectId: id, size: null };
|
|
123
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, positionData);
|
|
124
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
125
|
+
|
|
126
|
+
if (positionData.position && sizeData.size) {
|
|
127
|
+
return {
|
|
128
|
+
x: positionData.position.x,
|
|
129
|
+
y: positionData.position.y,
|
|
130
|
+
width: sizeData.size.width,
|
|
131
|
+
height: sizeData.size.height,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
update() {
|
|
138
|
+
if (!this.layer) return;
|
|
139
|
+
this.layer.innerHTML = '';
|
|
140
|
+
|
|
141
|
+
const selection = Array.from(this.core?.selectTool?.selectedObjects || []);
|
|
142
|
+
let selectedId = null;
|
|
143
|
+
if (selection.length === 1) {
|
|
144
|
+
selectedId = selection[0];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const targets = new Set();
|
|
148
|
+
if (this.hoveredObjectId) targets.add(this.hoveredObjectId);
|
|
149
|
+
if (selectedId) targets.add(selectedId);
|
|
150
|
+
|
|
151
|
+
targets.forEach(id => {
|
|
152
|
+
this._renderAnchorsFor(id);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_renderAnchorsFor(id) {
|
|
157
|
+
const req = { objectId: id, pixiObject: null };
|
|
158
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, req);
|
|
159
|
+
const mbType = req.pixiObject?._mb?.type;
|
|
160
|
+
|
|
161
|
+
if (!mbType || !ALLOWED_TYPES.has(mbType)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const worldBounds = this._getSingleSelectionWorldBounds(id);
|
|
166
|
+
if (!worldBounds) return;
|
|
167
|
+
|
|
168
|
+
const cssRect = this.positioningService.worldBoundsToCssRect(worldBounds);
|
|
169
|
+
|
|
170
|
+
const left = Math.round(cssRect.left);
|
|
171
|
+
const top = Math.round(cssRect.top);
|
|
172
|
+
const width = Math.max(1, Math.round(cssRect.width));
|
|
173
|
+
const height = Math.max(1, Math.round(cssRect.height));
|
|
174
|
+
|
|
175
|
+
const rotationData = { objectId: id, rotation: 0 };
|
|
176
|
+
this.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
|
|
177
|
+
const rotation = rotationData.rotation || 0;
|
|
178
|
+
|
|
179
|
+
const wrapper = document.createElement('div');
|
|
180
|
+
Object.assign(wrapper.style, {
|
|
181
|
+
position: 'absolute',
|
|
182
|
+
left: `${left}px`,
|
|
183
|
+
top: `${top}px`,
|
|
184
|
+
width: `${width}px`,
|
|
185
|
+
height: `${height}px`,
|
|
186
|
+
pointerEvents: 'none',
|
|
187
|
+
transformOrigin: 'center center',
|
|
188
|
+
transform: `rotate(${rotation}deg)`,
|
|
189
|
+
boxSizing: 'border-box'
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const offset = 12;
|
|
193
|
+
const radius = 5;
|
|
194
|
+
const dotSize = radius * 2;
|
|
195
|
+
|
|
196
|
+
const createDot = (side, x, y, ax, ay) => {
|
|
197
|
+
const dot = document.createElement('div');
|
|
198
|
+
dot.className = 'mb-connection-anchor';
|
|
199
|
+
Object.assign(dot.style, {
|
|
200
|
+
position: 'absolute',
|
|
201
|
+
left: `${Math.round(x - radius)}px`,
|
|
202
|
+
top: `${Math.round(y - radius)}px`,
|
|
203
|
+
width: `${dotSize}px`,
|
|
204
|
+
height: `${dotSize}px`,
|
|
205
|
+
backgroundColor: '#2563EB',
|
|
206
|
+
borderRadius: '50%',
|
|
207
|
+
pointerEvents: 'auto',
|
|
208
|
+
boxSizing: 'border-box',
|
|
209
|
+
border: '2px solid #ffffff'
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
dot.dataset.connectorAnchor = "1";
|
|
213
|
+
dot.dataset.id = id;
|
|
214
|
+
dot.dataset.side = side;
|
|
215
|
+
dot.dataset.anchorX = ax;
|
|
216
|
+
dot.dataset.anchorY = ay;
|
|
217
|
+
|
|
218
|
+
wrapper.appendChild(dot);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const cx = Math.round(width / 2);
|
|
222
|
+
const cy = Math.round(height / 2);
|
|
223
|
+
|
|
224
|
+
createDot('top', cx, -offset, 0.5, 0);
|
|
225
|
+
createDot('right', width + offset, cy, 1, 0.5);
|
|
226
|
+
createDot('bottom', cx, height + offset, 0.5, 1);
|
|
227
|
+
createDot('left', -offset, cy, 0, 0.5);
|
|
228
|
+
|
|
229
|
+
this.layer.appendChild(wrapper);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -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
|
|