@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequent-org/moodboard",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Interactive moodboard",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"deploy:prod": "NODE_ENV=production npm run build && NODE_ENV=production npm run start"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@pixi/filter-drop-shadow": "^5.2.0",
|
|
43
44
|
"axios": "^1.0.0",
|
|
45
|
+
"gsap": "^3.15.0",
|
|
44
46
|
"lucide-static": "^1.16.0",
|
|
45
47
|
"pixi.js": "^7.0.0"
|
|
46
48
|
},
|
package/src/core/PixiEngine.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as PIXI from 'pixi.js';
|
|
|
2
2
|
import { ObjectFactory } from '../objects/ObjectFactory.js';
|
|
3
3
|
import { ObjectRenderer } from './rendering/ObjectRenderer.js';
|
|
4
4
|
import { Events } from './events/Events.js';
|
|
5
|
+
import { HoverLiftController } from '../ui/animation/HoverLiftController.js';
|
|
5
6
|
|
|
6
7
|
export class PixiEngine {
|
|
7
8
|
constructor(container, eventBus, options) {
|
|
@@ -41,6 +42,9 @@ export class PixiEngine {
|
|
|
41
42
|
// Инициализируем ObjectRenderer
|
|
42
43
|
this.renderer = new ObjectRenderer(this.objects, this.eventBus);
|
|
43
44
|
|
|
45
|
+
// Hover-lift анимация для всех объектов
|
|
46
|
+
this.hoverLift = new HoverLiftController(this.eventBus, this.app);
|
|
47
|
+
|
|
44
48
|
// Поддержка чёткости текстов записок при зуме: подписка на событие зума
|
|
45
49
|
if (this.eventBus) {
|
|
46
50
|
const onZoom = ({ percentage }) => {
|
|
@@ -53,6 +57,9 @@ export class PixiEngine {
|
|
|
53
57
|
if (mb && mb.type === 'note' && mb.instance && typeof mb.instance.updateCrispnessForZoom === 'function') {
|
|
54
58
|
mb.instance.updateCrispnessForZoom(s, res);
|
|
55
59
|
}
|
|
60
|
+
if (mb && mb.type === 'mindmap' && mb.instance && typeof mb.instance.redrawForZoom === 'function') {
|
|
61
|
+
mb.instance.redrawForZoom(s);
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
} catch (e) {
|
|
58
65
|
console.warn('PixiEngine: zoom crispness update failed', e);
|
|
@@ -75,14 +82,17 @@ export class PixiEngine {
|
|
|
75
82
|
type: objectData.type,
|
|
76
83
|
instance: instance // Сохраняем ссылку на сам объект
|
|
77
84
|
};
|
|
78
|
-
// Первичная установка
|
|
85
|
+
// Первичная установка чёткости/масштаба по текущему зуму
|
|
79
86
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const world = this.worldLayer || this.app.stage;
|
|
88
|
+
const s = world?.scale?.x || 1;
|
|
89
|
+
const res = this.app?.renderer?.resolution || 1;
|
|
90
|
+
if (pixiObject._mb.type === 'note' && pixiObject._mb.instance && typeof pixiObject._mb.instance.updateCrispnessForZoom === 'function') {
|
|
84
91
|
pixiObject._mb.instance.updateCrispnessForZoom(s, res);
|
|
85
92
|
}
|
|
93
|
+
if (pixiObject._mb.type === 'mindmap' && pixiObject._mb.instance && typeof pixiObject._mb.instance.redrawForZoom === 'function') {
|
|
94
|
+
pixiObject._mb.instance.redrawForZoom(s);
|
|
95
|
+
}
|
|
86
96
|
} catch (_) {}
|
|
87
97
|
} else {
|
|
88
98
|
console.warn(`Unknown object type: ${objectData.type}`);
|
|
@@ -159,6 +169,17 @@ export class PixiEngine {
|
|
|
159
169
|
this.worldLayer.addChild(pixiObject);
|
|
160
170
|
this.objects.set(objectData.id, pixiObject);
|
|
161
171
|
|
|
172
|
+
// Hover-lift: подключаем упругую анимацию
|
|
173
|
+
if (this.hoverLift) {
|
|
174
|
+
this.hoverLift.attach(pixiObject, objectData);
|
|
175
|
+
// Если текстура ещё не загружена (ImageObject, EmojiObject),
|
|
176
|
+
// baseScale в attach зафиксирован как 1. Синхронизируем после загрузки.
|
|
177
|
+
const bt = pixiObject.texture?.baseTexture;
|
|
178
|
+
if (bt && !bt.valid) {
|
|
179
|
+
bt.once('loaded', () => { this.hoverLift?.syncBase(pixiObject); });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
162
183
|
|
|
163
184
|
}
|
|
164
185
|
}
|
|
@@ -222,6 +243,11 @@ export class PixiEngine {
|
|
|
222
243
|
}
|
|
223
244
|
}
|
|
224
245
|
|
|
246
|
+
// Отключаем hover-lift перед очисткой listeners
|
|
247
|
+
if (this.hoverLift) {
|
|
248
|
+
this.hoverLift.detach(pixiObject);
|
|
249
|
+
}
|
|
250
|
+
|
|
225
251
|
// Очищаем все события
|
|
226
252
|
pixiObject.removeAllListeners();
|
|
227
253
|
|
|
@@ -229,6 +255,9 @@ export class PixiEngine {
|
|
|
229
255
|
pixiObject.destroy({ children: true, texture: false, baseTexture: false });
|
|
230
256
|
} else {
|
|
231
257
|
// Для других типов объектов - стандартная очистка
|
|
258
|
+
if (this.hoverLift) {
|
|
259
|
+
this.hoverLift.detach(pixiObject);
|
|
260
|
+
}
|
|
232
261
|
if (pixiObject.destroy) {
|
|
233
262
|
pixiObject.destroy({ children: true });
|
|
234
263
|
}
|
|
@@ -55,6 +55,10 @@ export async function initializeCoreTools(core) {
|
|
|
55
55
|
const textTool = new textToolModule.TextTool(core.eventBus);
|
|
56
56
|
core.toolManager.registerTool(textTool);
|
|
57
57
|
|
|
58
|
+
const connectorToolModule = await import('../../tools/object-tools/ConnectorTool.js');
|
|
59
|
+
const connectorTool = new connectorToolModule.ConnectorTool(core.eventBus, core);
|
|
60
|
+
core.toolManager.registerTool(connectorTool);
|
|
61
|
+
|
|
58
62
|
core.selectTool = selectTool;
|
|
59
63
|
core.toolManager.activateTool('select');
|
|
60
64
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BaseCommand } from './BaseCommand.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Команда создания коннектора.
|
|
5
|
+
* execute() добавляет connector-объект через ядро (аналог создания других объектов).
|
|
6
|
+
*/
|
|
7
|
+
export class CreateConnectorCommand extends BaseCommand {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} core Экземпляр CoreMoodBoard
|
|
10
|
+
* @param {Object} connectorData Полные данные объекта типа 'connector' (включая id)
|
|
11
|
+
*/
|
|
12
|
+
constructor(core, connectorData) {
|
|
13
|
+
super('create_connector', 'Создать коннектор');
|
|
14
|
+
this.core = core;
|
|
15
|
+
this.connectorData = connectorData;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
execute() {
|
|
19
|
+
this.core.createObjectFromData(this.connectorData);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
undo() {
|
|
23
|
+
// Локальный undo отключен: история состояния загружается с сервера по версиям.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -22,7 +22,7 @@ export class GroupMoveCommand extends BaseCommand {
|
|
|
22
22
|
for (const item of this.moves) {
|
|
23
23
|
if (this.coordinatesAreTopLeft) {
|
|
24
24
|
// Координаты уже левый-верх (Frame перемещение)
|
|
25
|
-
this.core.updateObjectPositionDirect(item.id, item.to);
|
|
25
|
+
this.core.updateObjectPositionDirect(item.id, item.to, { snap: false });
|
|
26
26
|
this.emit(Events.Object.TransformUpdated, {
|
|
27
27
|
objectId: item.id,
|
|
28
28
|
type: 'position',
|
|
@@ -35,7 +35,7 @@ export class GroupMoveCommand extends BaseCommand {
|
|
|
35
35
|
const halfW = (pixiObject.width || 0) / 2;
|
|
36
36
|
const halfH = (pixiObject.height || 0) / 2;
|
|
37
37
|
const topLeft = { x: item.to.x - halfW, y: item.to.y - halfH };
|
|
38
|
-
this.core.updateObjectPositionDirect(item.id, topLeft);
|
|
38
|
+
this.core.updateObjectPositionDirect(item.id, topLeft, { snap: false });
|
|
39
39
|
this.emit(Events.Object.TransformUpdated, {
|
|
40
40
|
objectId: item.id,
|
|
41
41
|
type: 'position',
|
|
@@ -29,7 +29,7 @@ export class MoveObjectCommand extends BaseCommand {
|
|
|
29
29
|
_setPosition(position) {
|
|
30
30
|
// Используем готовую функцию из ядра - она правильно обрабатывает все типы объектов
|
|
31
31
|
// position уже является координатами левого-верхнего угла
|
|
32
|
-
this.coreMoodboard.updateObjectPositionDirect(this.objectId, position);
|
|
32
|
+
this.coreMoodboard.updateObjectPositionDirect(this.objectId, position, { snap: false });
|
|
33
33
|
|
|
34
34
|
// Уведомляем о том, что объект был изменен (для обновления ручек)
|
|
35
35
|
if (this.eventBus) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BaseCommand } from './BaseCommand.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Команда обновления терминалов и/или стиля коннектора.
|
|
5
|
+
* execute() применяет изменения к существующему connector-объекту в state.
|
|
6
|
+
*/
|
|
7
|
+
export class UpdateConnectorCommand extends BaseCommand {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} core Экземпляр CoreMoodBoard
|
|
10
|
+
* @param {string} connectorId id коннектора в state.objects
|
|
11
|
+
* @param {Object} updates { start?, end?, style? }
|
|
12
|
+
*/
|
|
13
|
+
constructor(core, connectorId, updates) {
|
|
14
|
+
super('update_connector', 'Обновить коннектор');
|
|
15
|
+
this.core = core;
|
|
16
|
+
this.connectorId = connectorId;
|
|
17
|
+
this.updates = updates;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
execute() {
|
|
21
|
+
const objects = this.core?.state?.state?.objects;
|
|
22
|
+
if (!Array.isArray(objects)) return;
|
|
23
|
+
|
|
24
|
+
const obj = objects.find(o => o.id === this.connectorId);
|
|
25
|
+
if (!obj) return;
|
|
26
|
+
|
|
27
|
+
if (!obj.properties) obj.properties = {};
|
|
28
|
+
if (this.updates.start !== undefined) obj.properties.start = this.updates.start;
|
|
29
|
+
if (this.updates.end !== undefined) obj.properties.end = this.updates.end;
|
|
30
|
+
if (this.updates.style !== undefined) {
|
|
31
|
+
obj.properties.style = { ...(obj.properties.style || {}), ...this.updates.style };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
undo() {
|
|
36
|
+
// Локальный undo отключен: история состояния загружается с сервера по версиям.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -8,6 +8,9 @@ import { ContextMenu } from '../../ui/ContextMenu.js';
|
|
|
8
8
|
import { HtmlTextLayer } from '../../ui/HtmlTextLayer.js';
|
|
9
9
|
import { MindmapHtmlTextLayer } from '../../ui/mindmap/MindmapHtmlTextLayer.js';
|
|
10
10
|
import { MindmapConnectionLayer } from '../../ui/mindmap/MindmapConnectionLayer.js';
|
|
11
|
+
import { MindmapCollapseLayer } from '../../ui/mindmap/MindmapCollapseLayer.js';
|
|
12
|
+
import { ConnectorLayer } from '../../ui/connectors/ConnectorLayer.js';
|
|
13
|
+
import { ConnectionAnchorsLayer } from '../../ui/connectors/ConnectionAnchorsLayer.js';
|
|
11
14
|
import { HtmlHandlesLayer } from '../../ui/HtmlHandlesLayer.js';
|
|
12
15
|
import { CommentPopover } from '../../ui/CommentPopover.js';
|
|
13
16
|
import { TextPropertiesPanel } from '../../ui/TextPropertiesPanel.js';
|
|
@@ -102,6 +105,14 @@ function initHtmlLayersAndPanels(board) {
|
|
|
102
105
|
board.mindmapHtmlTextLayer.attach();
|
|
103
106
|
board.mindmapConnectionLayer = new MindmapConnectionLayer(board.coreMoodboard.eventBus, board.coreMoodboard);
|
|
104
107
|
board.mindmapConnectionLayer.attach();
|
|
108
|
+
board.mindmapCollapseLayer = new MindmapCollapseLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
|
|
109
|
+
board.mindmapCollapseLayer.attach();
|
|
110
|
+
|
|
111
|
+
board.connectorLayer = new ConnectorLayer(board.coreMoodboard.eventBus, board.coreMoodboard);
|
|
112
|
+
board.connectorLayer.attach();
|
|
113
|
+
|
|
114
|
+
board.connectionAnchorsLayer = new ConnectionAnchorsLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
|
|
115
|
+
board.connectionAnchorsLayer.attach();
|
|
105
116
|
|
|
106
117
|
board.htmlHandlesLayer = new HtmlHandlesLayer(board.canvasContainer, board.coreMoodboard.eventBus, board.coreMoodboard);
|
|
107
118
|
board.htmlHandlesLayer.attach();
|
|
@@ -110,6 +121,9 @@ function initHtmlLayersAndPanels(board) {
|
|
|
110
121
|
window.moodboardHtmlTextLayer = board.htmlTextLayer;
|
|
111
122
|
window.moodboardMindmapHtmlTextLayer = board.mindmapHtmlTextLayer;
|
|
112
123
|
window.moodboardMindmapConnectionLayer = board.mindmapConnectionLayer;
|
|
124
|
+
window.moodboardMindmapCollapseLayer = board.mindmapCollapseLayer;
|
|
125
|
+
window.moodboardConnectorLayer = board.connectorLayer;
|
|
126
|
+
window.moodboardConnectionAnchorsLayer = board.connectionAnchorsLayer;
|
|
113
127
|
window.moodboardHtmlHandlesLayer = board.htmlHandlesLayer;
|
|
114
128
|
}
|
|
115
129
|
|
|
@@ -55,6 +55,15 @@ export function destroyMoodBoard(board) {
|
|
|
55
55
|
safeDestroy(board.mindmapConnectionLayer, 'mindmapConnectionLayer');
|
|
56
56
|
board.mindmapConnectionLayer = null;
|
|
57
57
|
|
|
58
|
+
safeDestroy(board.mindmapCollapseLayer, 'mindmapCollapseLayer');
|
|
59
|
+
board.mindmapCollapseLayer = null;
|
|
60
|
+
|
|
61
|
+
safeDestroy(board.connectorLayer, 'connectorLayer');
|
|
62
|
+
board.connectorLayer = null;
|
|
63
|
+
|
|
64
|
+
safeDestroy(board.connectionAnchorsLayer, 'connectionAnchorsLayer');
|
|
65
|
+
board.connectionAnchorsLayer = null;
|
|
66
|
+
|
|
58
67
|
safeDestroy(board.htmlHandlesLayer, 'htmlHandlesLayer');
|
|
59
68
|
board.htmlHandlesLayer = null;
|
|
60
69
|
|
|
@@ -100,6 +109,15 @@ export function destroyMoodBoard(board) {
|
|
|
100
109
|
if (window.moodboardMindmapConnectionLayer === board.mindmapConnectionLayer) {
|
|
101
110
|
window.moodboardMindmapConnectionLayer = null;
|
|
102
111
|
}
|
|
112
|
+
if (window.moodboardMindmapCollapseLayer === board.mindmapCollapseLayer) {
|
|
113
|
+
window.moodboardMindmapCollapseLayer = null;
|
|
114
|
+
}
|
|
115
|
+
if (window.moodboardConnectorLayer === board.connectorLayer) {
|
|
116
|
+
window.moodboardConnectorLayer = null;
|
|
117
|
+
}
|
|
118
|
+
if (window.moodboardConnectionAnchorsLayer === board.connectionAnchorsLayer) {
|
|
119
|
+
window.moodboardConnectionAnchorsLayer = null;
|
|
120
|
+
}
|
|
103
121
|
if (window.moodboardHtmlHandlesLayer === board.htmlHandlesLayer) {
|
|
104
122
|
window.moodboardHtmlHandlesLayer = null;
|
|
105
123
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ConnectorObject — универсальный коннектор (стрелка/линия с привязкой к объектам).
|
|
5
|
+
*
|
|
6
|
+
* Визуальный рендер делегирован ConnectorLayer; здесь только тип, дефолты и сериализация.
|
|
7
|
+
*
|
|
8
|
+
* properties:
|
|
9
|
+
* start — { boundId, anchor:{x,y}, isPrecise, isExact } | { point:{x,y} }
|
|
10
|
+
* end — то же
|
|
11
|
+
* style — { stroke, width, dash, head:{start,end}, route }
|
|
12
|
+
*
|
|
13
|
+
* position/size номинальны (bounding box линии); реальная геометрия — из терминалов.
|
|
14
|
+
*/
|
|
15
|
+
export class ConnectorObject {
|
|
16
|
+
constructor(objectData = {}) {
|
|
17
|
+
this.objectData = objectData;
|
|
18
|
+
|
|
19
|
+
// Номинальные размеры (bbox); реальная геометрия — из терминалов
|
|
20
|
+
this.width = objectData.width ?? 0;
|
|
21
|
+
this.height = objectData.height ?? 0;
|
|
22
|
+
|
|
23
|
+
const props = objectData.properties || {};
|
|
24
|
+
|
|
25
|
+
this.start = props.start || { point: { x: 0, y: 0 } };
|
|
26
|
+
this.end = props.end || { point: { x: 100, y: 0 } };
|
|
27
|
+
|
|
28
|
+
this.style = {
|
|
29
|
+
stroke: 0x2563EB,
|
|
30
|
+
width: 2,
|
|
31
|
+
dash: false,
|
|
32
|
+
head: { start: false, end: true },
|
|
33
|
+
route: 'straight',
|
|
34
|
+
...(props.style || {}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Невидимый контейнер-заглушка: реальный рендер в ConnectorLayer
|
|
38
|
+
this._container = new PIXI.Container();
|
|
39
|
+
this._container.visible = false;
|
|
40
|
+
this._container.eventMode = 'none';
|
|
41
|
+
|
|
42
|
+
this._container._mb = {
|
|
43
|
+
type: 'connector',
|
|
44
|
+
instance: this,
|
|
45
|
+
properties: this._buildProperties(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_buildProperties() {
|
|
50
|
+
return {
|
|
51
|
+
start: this.start,
|
|
52
|
+
end: this.end,
|
|
53
|
+
style: { ...this.style, head: { ...this.style.head } },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getPixi() {
|
|
58
|
+
return this._container;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Обновляет терминалы и/или стиль; синхронизирует _mb.properties */
|
|
62
|
+
setProperties({ start, end, style } = {}) {
|
|
63
|
+
if (start !== undefined) this.start = start;
|
|
64
|
+
if (end !== undefined) this.end = end;
|
|
65
|
+
if (style !== undefined) this.style = { ...this.style, ...style };
|
|
66
|
+
|
|
67
|
+
if (this._container?._mb) {
|
|
68
|
+
this._container._mb.properties = this._buildProperties();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Обновляет номинальные размеры (используется ядром при resize) */
|
|
73
|
+
updateSize(size) {
|
|
74
|
+
if (!size) return;
|
|
75
|
+
if (size.width != null) this.width = size.width;
|
|
76
|
+
if (size.height != null) this.height = size.height;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
destroy() {
|
|
80
|
+
if (this._container) {
|
|
81
|
+
try { this._container.destroy(); } catch (_) {}
|
|
82
|
+
this._container = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { GeometryUtils } from '../core/rendering/GeometryUtils.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Класс объекта «Рисунок» (карандаш/маркер)
|
|
@@ -27,6 +28,13 @@ export class DrawingObject {
|
|
|
27
28
|
this.graphics = new PIXI.Graphics();
|
|
28
29
|
this._draw(this.points, this.color, this.strokeWidth, this.mode);
|
|
29
30
|
|
|
31
|
+
// PIXI v7 Graphics.containsPoint учитывает только заливку (fillStyle),
|
|
32
|
+
// а рисунок состоит лишь из обводки (lineStyle) — поэтому штатный hit-test
|
|
33
|
+
// линии всегда «мимо», и событийная система не шлёт pointerover/pointerout
|
|
34
|
+
// (от них зависит hover-подсветка). Переопределяем containsPoint на проверку
|
|
35
|
+
// расстояния до сегментов, чтобы наведение ловилось на любом участке линии.
|
|
36
|
+
this.graphics.containsPoint = (globalPoint) => this._containsPoint(globalPoint);
|
|
37
|
+
|
|
30
38
|
// Сохраняем мета для hit-test/resize
|
|
31
39
|
this.graphics._mb = {
|
|
32
40
|
...(this.graphics._mb || {}),
|
|
@@ -46,6 +54,45 @@ export class DrawingObject {
|
|
|
46
54
|
return this.graphics;
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Hit-test линии по расстоянию до её сегментов.
|
|
59
|
+
* Вызывается событийной системой PIXI с ГЛОБАЛЬНОЙ точкой.
|
|
60
|
+
* @param {PIXI.IPointData} globalPoint
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
_containsPoint(globalPoint) {
|
|
64
|
+
const g = this.graphics;
|
|
65
|
+
const pts = this.points;
|
|
66
|
+
if (!pts || pts.length < 2 || !g.toLocal) return false;
|
|
67
|
+
|
|
68
|
+
// Локальные координаты курсора в системе геометрии линии.
|
|
69
|
+
const local = g.toLocal(globalPoint);
|
|
70
|
+
|
|
71
|
+
// Геометрия могла быть перерисована под новый размер (updateSize),
|
|
72
|
+
// тогда как this.points хранит базовые точки. Масштаб берём из
|
|
73
|
+
// ЛОКАЛЬНЫХ границ (не зависят от zoom), чтобы совпасть с local.
|
|
74
|
+
const lb = g.getLocalBounds();
|
|
75
|
+
const baseW = this.baseWidth || 1;
|
|
76
|
+
const baseH = this.baseHeight || 1;
|
|
77
|
+
const scaleX = baseW ? (lb.width / baseW) : 1;
|
|
78
|
+
const scaleY = baseH ? (lb.height / baseH) : 1;
|
|
79
|
+
|
|
80
|
+
// Полоса попадания: половина толщины линии + запас на «любой участок».
|
|
81
|
+
const lineWidth = this.mode === 'marker' ? this.strokeWidth * 2 : this.strokeWidth;
|
|
82
|
+
const threshold = Math.max(8, lineWidth / 2 + 6);
|
|
83
|
+
|
|
84
|
+
for (let j = 0; j < pts.length - 1; j++) {
|
|
85
|
+
const ax = pts[j].x * scaleX;
|
|
86
|
+
const ay = pts[j].y * scaleY;
|
|
87
|
+
const bx = pts[j + 1].x * scaleX;
|
|
88
|
+
const by = pts[j + 1].y * scaleY;
|
|
89
|
+
if (GeometryUtils.distancePointToSegment(local.x, local.y, ax, ay, bx, by) <= threshold) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
49
96
|
/** Обновить визуал без изменения точек */
|
|
50
97
|
setStyle({ mode, strokeColor, strokeWidth } = {}) {
|
|
51
98
|
if (mode) this.mode = mode;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as PIXI from 'pixi.js';
|
|
2
2
|
import { MINDMAP_LAYOUT } from '../ui/mindmap/MindmapLayoutConfig.js';
|
|
3
3
|
|
|
4
|
+
// Толщина обводки в экранных пикселях — совпадает с толщиной веток MindmapConnectionLayer.
|
|
5
|
+
const STROKE_SCREEN_PX = 2;
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Простой объект mindmap: прямоугольник с синей обводкой и полупрозрачной синей заливкой.
|
|
6
9
|
*/
|
|
@@ -18,8 +21,10 @@ export class MindmapObject {
|
|
|
18
21
|
? Math.max(1, Math.round(props.capsuleBaseHeight))
|
|
19
22
|
: Math.max(1, Math.round(Math.min(this.height, MINDMAP_LAYOUT.height)));
|
|
20
23
|
|
|
24
|
+
// Текущий масштаб мирового слоя; обновляется через redrawForZoom при каждом зуме.
|
|
25
|
+
this._worldScale = 1;
|
|
26
|
+
|
|
21
27
|
this.graphics = new PIXI.Graphics();
|
|
22
|
-
this.graphics.roundPixels = true;
|
|
23
28
|
this._draw();
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -34,6 +39,16 @@ export class MindmapObject {
|
|
|
34
39
|
this._redrawPreserveTransform();
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Перерисовать пилюлю под текущий масштаб мира.
|
|
44
|
+
* Вызывается из PixiEngine на каждый зум — пересчитывает толщину обводки и
|
|
45
|
+
* число сегментов дуги, чтобы фаски оставались гладкими без зазубрин.
|
|
46
|
+
*/
|
|
47
|
+
redrawForZoom(worldScale) {
|
|
48
|
+
this._worldScale = Math.max(0.01, worldScale || 1);
|
|
49
|
+
this._redrawPreserveTransform();
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
_redrawPreserveTransform() {
|
|
38
53
|
const g = this.graphics;
|
|
39
54
|
const centerX = g.x;
|
|
@@ -54,13 +69,16 @@ export class MindmapObject {
|
|
|
54
69
|
const fixedBaseRadius = Math.max(0, Math.floor(this.capsuleBaseHeight / 2));
|
|
55
70
|
const capsuleRadius = Math.min(dynamicRadius, fixedBaseRadius);
|
|
56
71
|
|
|
72
|
+
// Толщина в мировых единицах: 3 экранных пикселя ÷ масштаб — совпадает с ветками.
|
|
73
|
+
const strokeW = STROKE_SCREEN_PX / this._worldScale;
|
|
74
|
+
|
|
57
75
|
g.beginFill(this.fillColor, this.fillAlpha);
|
|
58
76
|
g.drawRoundedRect(0, 0, this.width, this.height, capsuleRadius);
|
|
59
77
|
g.endFill();
|
|
60
78
|
|
|
61
79
|
try {
|
|
62
80
|
g.lineStyle({
|
|
63
|
-
width:
|
|
81
|
+
width: strokeW,
|
|
64
82
|
color: this.strokeColor,
|
|
65
83
|
alpha: 1,
|
|
66
84
|
alignment: 0,
|
|
@@ -69,7 +87,7 @@ export class MindmapObject {
|
|
|
69
87
|
miterLimit: 2,
|
|
70
88
|
});
|
|
71
89
|
} catch (_) {
|
|
72
|
-
g.lineStyle(
|
|
90
|
+
g.lineStyle(strokeW, this.strokeColor, 1, 0);
|
|
73
91
|
}
|
|
74
92
|
g.drawRoundedRect(0, 0, this.width, this.height, capsuleRadius);
|
|
75
93
|
}
|
|
@@ -83,24 +83,26 @@ export class NoteObject {
|
|
|
83
83
|
this.textField.mask = this.textMask;
|
|
84
84
|
|
|
85
85
|
this._redraw(); // Сначала рисуем фон
|
|
86
|
-
// Прячем текст до загрузки шрифта Caveat, чтобы не показывать системный
|
|
87
86
|
this.textField.visible = false;
|
|
88
87
|
this.container.addChild(this.textField); // Затем добавляем текст поверх
|
|
89
|
-
|
|
90
|
-
// Если
|
|
88
|
+
// Если шрифт уже загружен — позиционируем и показываем сразу.
|
|
89
|
+
// Если нет — НЕ вызываем _updateTextPosition(), чтобы не отравить кэш
|
|
90
|
+
// PIXI.TextMetrics метриками fallback-шрифта (Arial). Позиционирование
|
|
91
|
+
// произойдёт в _ensureWebFontApplied после реальной загрузки шрифта.
|
|
91
92
|
if (this._isFontLoaded(fontFamily, this.fontSize)) {
|
|
93
|
+
this._updateTextPosition();
|
|
92
94
|
this.textField.visible = true;
|
|
93
95
|
} else {
|
|
94
|
-
|
|
95
|
-
// Фолбэк на случай отсутствия Font Loading API — короткая задержка
|
|
96
|
+
// Фолбэк на случай отсутствия Font Loading API — показываем без позиционирования
|
|
96
97
|
try {
|
|
97
98
|
if (!(typeof document !== 'undefined' && document.fonts && typeof document.fonts.load === 'function')) {
|
|
99
|
+
this._updateTextPosition();
|
|
98
100
|
setTimeout(() => { try { this.textField.visible = true; } catch (_) {} }, 300);
|
|
99
101
|
}
|
|
100
102
|
} catch (_) {}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
//
|
|
105
|
+
// Загружаем web-font и применяем корректные метрики
|
|
104
106
|
this._ensureWebFontApplied(fontFamily, this.fontSize);
|
|
105
107
|
|
|
106
108
|
// Отладочная информация
|
|
@@ -290,7 +292,9 @@ export class NoteObject {
|
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
/**
|
|
293
|
-
* Дожидается загрузки веб-шрифта и обновляет PIXI.Text
|
|
295
|
+
* Дожидается загрузки веб-шрифта и обновляет PIXI.Text с правильными метриками.
|
|
296
|
+
* Сбрасывает кэш PIXI.TextMetrics перед позиционированием — это гарантирует,
|
|
297
|
+
* что PIXI не будет использовать ранее закэшированные метрики fallback-шрифта.
|
|
294
298
|
*/
|
|
295
299
|
_ensureWebFontApplied(fontFamily, fontSizePx) {
|
|
296
300
|
try {
|
|
@@ -299,9 +303,13 @@ export class NoteObject {
|
|
|
299
303
|
const size = Math.max(1, Number(fontSizePx) || 32);
|
|
300
304
|
const spec = `normal ${size}px ${primary}`;
|
|
301
305
|
document.fonts.load(spec).then(() => {
|
|
302
|
-
// Обновляем текст после загрузки шрифта и сразу подгоняем без мерцания
|
|
303
306
|
try {
|
|
304
307
|
if (this.textField) this.textField.visible = false;
|
|
308
|
+
// Сбрасываем кэш метрик шрифтов PIXI, чтобы Caveat переизмерился
|
|
309
|
+
// с реальными метриками, а не с ранее закэшированными от Arial
|
|
310
|
+
if (PIXI.TextMetrics?.clearMetrics) {
|
|
311
|
+
PIXI.TextMetrics.clearMetrics();
|
|
312
|
+
}
|
|
305
313
|
this.textField.style.fontFamily = fontFamily;
|
|
306
314
|
this._updateTextPosition();
|
|
307
315
|
if (this.textField) this.textField.visible = true;
|
|
@@ -9,6 +9,7 @@ import { CommentObject } from './CommentObject.js';
|
|
|
9
9
|
import { NoteObject } from './NoteObject.js';
|
|
10
10
|
import { FileObject } from './FileObject.js';
|
|
11
11
|
import { MindmapObject } from './MindmapObject.js';
|
|
12
|
+
import { ConnectorObject } from './ConnectorObject.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Фабрика объектов холста
|
|
@@ -27,7 +28,8 @@ export class ObjectFactory {
|
|
|
27
28
|
['comment', CommentObject],
|
|
28
29
|
['note', NoteObject],
|
|
29
30
|
['file', FileObject],
|
|
30
|
-
['mindmap', MindmapObject]
|
|
31
|
+
['mindmap', MindmapObject],
|
|
32
|
+
['connector', ConnectorObject],
|
|
31
33
|
]);
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -16,7 +16,7 @@ export class ShapeObject {
|
|
|
16
16
|
this.objectData = objectData;
|
|
17
17
|
this.width = objectData.width || 100;
|
|
18
18
|
this.height = objectData.height || 100;
|
|
19
|
-
this.fillColor = objectData.color ??
|
|
19
|
+
this.fillColor = objectData.color ?? 0xffffff;
|
|
20
20
|
const props = objectData.properties || {};
|
|
21
21
|
this.kind = props.kind || 'square';
|
|
22
22
|
this.cornerRadius = props.cornerRadius || 10;
|