@sequent-org/moodboard 1.4.29 → 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 +7 -5
- package/src/ui/chat/ChatWindow.js +652 -112
- 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 +40 -3
- package/src/ui/styles/toolbar.css +6 -0
- package/src/ui/styles/workspace.css +83 -21
- package/src/ui/toolbar/ToolbarPopupsController.js +1 -1
package/src/ui/chat/icons.js
CHANGED
|
@@ -45,6 +45,18 @@ const ENHANCE_PROMPT_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16"
|
|
|
45
45
|
/** public/icons/extend-promt-field.svg */
|
|
46
46
|
const EXTEND_PROMPT_FIELD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M9.696 7.151a.599.599 0 1 1-.847-.847L12.55 2.6H9.272a.6.6 0 0 1 0-1.2H14a.6.6 0 0 1 .6.599v4.728a.6.6 0 1 1-1.2 0V3.449zM1.4 9.272a.6.6 0 1 1 1.2 0v3.28l3.704-3.703a.599.599 0 1 1 .847.847L3.45 13.4h3.279a.6.6 0 0 1 0 1.2H2A.6.6 0 0 1 1.4 14z"/></svg>`;
|
|
47
47
|
|
|
48
|
+
/** public/icons/google.svg — цветной логотип Google 36×36 */
|
|
49
|
+
const MODEL_GOOGLE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36"><path fill="#4285F4" d="M29.251 18.49c0-.813-.073-1.596-.209-2.347H18.23v4.445h6.179c-.272 1.43-1.086 2.64-2.307 3.455v2.89h3.726c2.17-2.003 3.423-4.946 3.423-8.442"/><path fill="#34A853" d="M18.229 29.71c3.1 0 5.698-1.023 7.597-2.776l-3.725-2.89c-1.023.688-2.328 1.105-3.872 1.105-2.985 0-5.52-2.014-6.429-4.727H7.98v2.964c1.89 3.746 5.761 6.324 10.249 6.324"/><path fill="#FBBC05" d="M11.801 20.412a6.9 6.9 0 0 1-.365-2.181c0-.762.136-1.492.365-2.181v-2.964h-3.82A11.34 11.34 0 0 0 6.75 18.23c0 1.858.449 3.6 1.231 5.145l2.975-2.317z"/><path fill="#EA4335" d="M18.229 11.321c1.69 0 3.193.585 4.393 1.712l3.288-3.288c-1.994-1.857-4.582-2.995-7.681-2.995-4.488 0-8.36 2.578-10.249 6.335l3.82 2.964c.908-2.714 3.444-4.728 6.429-4.728"/></svg>`;
|
|
50
|
+
|
|
51
|
+
/** Placeholder OpenAI GPT — буква G в круге, 36×36 */
|
|
52
|
+
const MODEL_GPT_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true"><circle cx="18" cy="18" r="16" fill="#10a37f"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">G</text></svg>`;
|
|
53
|
+
|
|
54
|
+
/** Placeholder Alibaba Qwen — буква Q в круге, 36×36 */
|
|
55
|
+
const MODEL_QWEN_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true"><circle cx="18" cy="18" r="16" fill="#6e42ca"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">Q</text></svg>`;
|
|
56
|
+
|
|
57
|
+
/** Placeholder Yandex Alice — буква А в круге, 36×36 */
|
|
58
|
+
const MODEL_ALICE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true"><circle cx="18" cy="18" r="16" fill="#fc3f1d"/><text x="18" y="23" text-anchor="middle" font-size="16" font-family="Arial,sans-serif" fill="#fff" font-weight="bold">А</text></svg>`;
|
|
59
|
+
|
|
48
60
|
export const ICONS = {
|
|
49
61
|
image: IMAGE_ICON,
|
|
50
62
|
video: VIDEO_ICON,
|
|
@@ -63,7 +75,11 @@ export const ICONS = {
|
|
|
63
75
|
chevronDown: svg('<path d="M6 9l6 6 6-6"/>'),
|
|
64
76
|
trash: svg('<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M6 6l1 14a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-14"/>'),
|
|
65
77
|
close: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M13.575 12.726a.6.6 0 0 1-.849.849zm-.849-10.3a.6.6 0 0 1 .849.848L8.848 7.999l4.727 4.727-.425.424-.424.425-4.727-4.727-4.725 4.727a.6.6 0 0 1-.848-.849l4.726-4.727-4.726-4.725a.599.599 0 1 1 .848-.848l4.725 4.726z"></path></svg>`,
|
|
66
|
-
sparkles: svg('<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>')
|
|
78
|
+
sparkles: svg('<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>'),
|
|
79
|
+
modelGoogle: MODEL_GOOGLE_ICON,
|
|
80
|
+
modelGpt: MODEL_GPT_ICON,
|
|
81
|
+
modelQwen: MODEL_QWEN_ICON,
|
|
82
|
+
modelAlice: MODEL_ALICE_ICON,
|
|
67
83
|
};
|
|
68
84
|
|
|
69
85
|
/**
|
|
@@ -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
|
|