@sequent-org/moodboard 1.4.32 → 1.4.33
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 +5 -1
- package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/src/assets/icons/attachments.svg +3 -1
- package/src/assets/icons/comments.svg +2 -2
- package/src/assets/icons/connector.svg +6 -0
- package/src/assets/icons/emoji.svg +6 -1
- package/src/assets/icons/frame.svg +4 -1
- package/src/assets/icons/image.svg +5 -1
- package/src/assets/icons/laser.svg +1 -0
- package/src/assets/icons/lasso.svg +5 -0
- package/src/assets/icons/mindmap.svg +10 -2
- package/src/assets/icons/note.svg +4 -1
- package/src/assets/icons/pan.svg +5 -2
- package/src/assets/icons/pencil.svg +4 -1
- package/src/assets/icons/reactions.svg +5 -0
- package/src/assets/icons/redo.svg +3 -2
- package/src/assets/icons/select.svg +2 -8
- package/src/assets/icons/shapes.svg +5 -1
- package/src/assets/icons/text-add.svg +15 -1
- package/src/assets/icons/undo.svg +3 -2
- package/src/assets/reactions/1f44d.svg +20 -0
- package/src/assets/reactions/1f44e.svg +20 -0
- package/src/assets/reactions/2705.svg +20 -0
- package/src/assets/reactions/274c.svg +19 -0
- package/src/assets/reactions/2753.svg +20 -0
- package/src/assets/reactions/2764.svg +22 -0
- package/src/assets/reactions/2b50.svg +19 -0
- package/src/assets/reactions/plus-one.svg +25 -0
- package/src/core/PixiEngine.js +23 -0
- package/src/core/bootstrap/CoreInitializer.js +43 -0
- package/src/core/commands/GroupDeleteCommand.js +13 -1
- package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
- package/src/core/commands/UpdateTextStyleCommand.js +17 -6
- package/src/core/commands/index.js +3 -0
- package/src/core/events/Events.js +22 -0
- package/src/core/flows/LayerAndViewportFlow.js +1 -0
- package/src/core/flows/ObjectLifecycleFlow.js +155 -7
- package/src/core/index.js +28 -1
- package/src/grid/CrossGridZoomPhases.js +3 -3
- package/src/initNoBundler.js +1 -1
- package/src/moodboard/DataManager.js +28 -0
- package/src/moodboard/MoodBoard.js +27 -0
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
- package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
- package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
- package/src/objects/ConnectorObject.js +2 -2
- package/src/objects/FrameObject.js +119 -59
- package/src/objects/ShapeObject.js +49 -74
- package/src/objects/shape/ShapeDrawer.js +210 -0
- package/src/services/ConnectorBindingResolver.js +112 -0
- package/src/services/ConnectorRouter.js +210 -0
- package/src/services/comments/CommentService.js +344 -0
- package/src/tools/object-tools/CommentTool.js +85 -0
- package/src/tools/object-tools/DrawingTool.js +110 -10
- package/src/tools/object-tools/LaserPointerTool.js +121 -0
- package/src/tools/object-tools/SelectTool.js +25 -1
- package/src/tools/object-tools/TextTool.js +6 -1
- package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
- package/src/tools/object-tools/connector/connectorGesture.js +33 -19
- package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
- package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
- package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
- package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
- package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
- package/src/ui/CommentPopover.js +6 -0
- package/src/ui/CommentsBar.js +91 -0
- package/src/ui/ConnectorPropertiesPanel.js +150 -0
- package/src/ui/ContextMenu.js +25 -0
- package/src/ui/DrawingPropertiesPanel.js +362 -0
- package/src/ui/FilePropertiesPanel.js +5 -0
- package/src/ui/FramePropertiesPanel.js +5 -0
- package/src/ui/HtmlTextLayer.js +246 -66
- package/src/ui/NotePropertiesPanel.js +6 -0
- package/src/ui/ShapePropertiesPanel.js +307 -0
- package/src/ui/TextPropertiesPanel.js +100 -1
- package/src/ui/Toolbar.js +25 -2
- package/src/ui/Topbar.js +2 -2
- package/src/ui/animation/HoverLiftController.js +6 -7
- package/src/ui/chat/ChatComposer.js +58 -7
- package/src/ui/chat/ChatWindow.js +60 -143
- package/src/ui/comments/CommentListPanel.js +213 -0
- package/src/ui/comments/CommentPinLayer.js +448 -0
- package/src/ui/comments/CommentThreadPopover.js +539 -0
- package/src/ui/comments/commentFormat.js +32 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
- package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
- package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
- package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
- package/src/ui/connectors/ConnectorLayer.js +264 -57
- package/src/ui/handles/HandlesDomRenderer.js +5 -13
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
- package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
- package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
- package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
- package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
- package/src/ui/styles/chat.css +709 -19
- package/src/ui/styles/index.css +1 -0
- package/src/ui/styles/panels.css +112 -2
- package/src/ui/styles/shape-properties-panel.css +250 -0
- package/src/ui/styles/toolbar.css +7 -2
- package/src/ui/styles/topbar.css +1 -1
- package/src/ui/styles/workspace.css +257 -6
- package/src/ui/text-properties/TextFormatControls.js +88 -0
- package/src/ui/text-properties/TextListRenderer.js +137 -0
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
- package/src/ui/toolbar/ReactionsPopupController.js +88 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
- package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
- package/src/ui/toolbar/ToolbarRenderer.js +9 -1
- package/src/ui/toolbar/ToolbarStateController.js +4 -1
- package/src/utils/iconLoader.js +17 -16
- package/src/utils/markdown.js +14 -0
- package/src/utils/richText.js +125 -0
|
@@ -5,6 +5,11 @@ import * as PIXI from 'pixi.js';
|
|
|
5
5
|
/** Максимум точек в одном штрихе (decimation при превышении). */
|
|
6
6
|
const MAX_POINTS = 5000;
|
|
7
7
|
|
|
8
|
+
/** Шаг примагничивания угла (45°) при зажатом Shift. */
|
|
9
|
+
const SNAP_STEP = Math.PI / 4;
|
|
10
|
+
/** Допуск примагничивания: ±8° вокруг каждого кратного 45°. */
|
|
11
|
+
const SNAP_TOLERANCE = (8 * Math.PI) / 180;
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Инструмент рисования (карандаш)
|
|
10
15
|
*/
|
|
@@ -22,6 +27,10 @@ export class DrawingTool extends BaseTool {
|
|
|
22
27
|
this.app = null;
|
|
23
28
|
this.world = null;
|
|
24
29
|
|
|
30
|
+
// Якорная точка для режима прямой линии (Shift)
|
|
31
|
+
this._straightAnchor = null;
|
|
32
|
+
this._shiftDown = false;
|
|
33
|
+
|
|
25
34
|
// Параметры кисти по умолчанию
|
|
26
35
|
this.brush = {
|
|
27
36
|
color: 0x111827, // чёрный
|
|
@@ -124,6 +133,7 @@ export class DrawingTool extends BaseTool {
|
|
|
124
133
|
this.world.addChild(this.tempGraphics);
|
|
125
134
|
|
|
126
135
|
const p = this._toWorld(event.x, event.y);
|
|
136
|
+
this._straightAnchor = { ...p };
|
|
127
137
|
this.points.push(p);
|
|
128
138
|
this._redrawTemporary();
|
|
129
139
|
}
|
|
@@ -131,11 +141,23 @@ export class DrawingTool extends BaseTool {
|
|
|
131
141
|
onMouseMove(event) {
|
|
132
142
|
super.onMouseMove(event);
|
|
133
143
|
if (!this.isDrawing) return;
|
|
144
|
+
|
|
134
145
|
const p = this._toWorld(event.x, event.y);
|
|
146
|
+
const isStraight = !!(event.originalEvent?.shiftKey) && this.brush.mode !== 'eraser';
|
|
147
|
+
|
|
148
|
+
if (isStraight) {
|
|
149
|
+
const end = this._snapEndpoint(this._straightAnchor, p);
|
|
150
|
+
this.points = [this._straightAnchor, end];
|
|
151
|
+
this._redrawTemporary();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
135
155
|
const prev = this.points[this.points.length - 1];
|
|
136
156
|
// Фильтр слишком частых точек
|
|
137
157
|
if (!prev || Math.hypot(p.x - prev.x, p.y - prev.y) >= 1) {
|
|
138
158
|
this.points.push(p);
|
|
159
|
+
// Обновляем якорь — при отпускании Shift штрих продолжится с текущей точки
|
|
160
|
+
this._straightAnchor = { ...p };
|
|
139
161
|
// Ластик: при движении удаляем все фигуры, пересекаемые текущим сегментом
|
|
140
162
|
if (this.brush.mode === 'eraser' && prev) {
|
|
141
163
|
this._eraserSweep(prev, p);
|
|
@@ -153,6 +175,26 @@ export class DrawingTool extends BaseTool {
|
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
|
|
178
|
+
onKeyDown(event) {
|
|
179
|
+
if (event.key === 'Shift' && this.isDrawing && this.brush.mode !== 'eraser') {
|
|
180
|
+
this._shiftDown = true;
|
|
181
|
+
// Перерисовываем прямую до текущей позиции курсора
|
|
182
|
+
if (this.currentPoint) {
|
|
183
|
+
const p = this._toWorld(this.currentPoint.x, this.currentPoint.y);
|
|
184
|
+
const end = this._snapEndpoint(this._straightAnchor, p);
|
|
185
|
+
this.points = [this._straightAnchor, end];
|
|
186
|
+
this._redrawTemporary();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onKeyUp(event) {
|
|
192
|
+
if (event.key === 'Shift' && this.isDrawing && this.brush.mode !== 'eraser') {
|
|
193
|
+
this._shiftDown = false;
|
|
194
|
+
// При отпускании Shift якорь уже актуален — продолжение в freehand-режиме
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
156
198
|
onMouseUp(event) {
|
|
157
199
|
super.onMouseUp(event);
|
|
158
200
|
this._finishAndCommit();
|
|
@@ -161,6 +203,8 @@ export class DrawingTool extends BaseTool {
|
|
|
161
203
|
_finishAndCommit() {
|
|
162
204
|
if (!this.isDrawing) return;
|
|
163
205
|
this.isDrawing = false;
|
|
206
|
+
this._straightAnchor = null;
|
|
207
|
+
this._shiftDown = false;
|
|
164
208
|
|
|
165
209
|
// Если ластик — чистим временную линию и выходим (удаление уже произошло onMouseDown)
|
|
166
210
|
if (this.brush.mode === 'eraser') {
|
|
@@ -196,11 +240,15 @@ export class DrawingTool extends BaseTool {
|
|
|
196
240
|
if (pointsToUse.length > MAX_POINTS) {
|
|
197
241
|
pointsToUse = this._decimatePoints(pointsToUse, MAX_POINTS);
|
|
198
242
|
}
|
|
199
|
-
|
|
200
|
-
|
|
243
|
+
|
|
244
|
+
// position — целая (округлённая) точка; нормализуем точки от тех же значений,
|
|
245
|
+
// иначе дробный остаток minX/minY сдвигал бы геометрию относительно position.
|
|
246
|
+
const posX = Math.round(minX);
|
|
247
|
+
const posY = Math.round(minY);
|
|
248
|
+
const normPoints = pointsToUse.map(pt => ({ x: pt.x - posX, y: pt.y - posY }));
|
|
201
249
|
|
|
202
250
|
// Создаем объект типа drawing через существующий пайплайн
|
|
203
|
-
const position = { x:
|
|
251
|
+
const position = { x: posX, y: posY };
|
|
204
252
|
const properties = {
|
|
205
253
|
points: normPoints,
|
|
206
254
|
strokeColor: this.brush.color,
|
|
@@ -335,6 +383,29 @@ export class DrawingTool extends BaseTool {
|
|
|
335
383
|
g.quadraticCurveTo(pen.x, pen.y, last.x, last.y);
|
|
336
384
|
}
|
|
337
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Примагничивает конечную точку прямой к ближайшему кратному 45°,
|
|
388
|
+
* если угол попадает в допуск SNAP_TOLERANCE. Длина линии сохраняется.
|
|
389
|
+
* Если отрезок слишком короткий или угол вне зоны — возвращает p без изменений.
|
|
390
|
+
*/
|
|
391
|
+
_snapEndpoint(anchor, p) {
|
|
392
|
+
const dx = p.x - anchor.x;
|
|
393
|
+
const dy = p.y - anchor.y;
|
|
394
|
+
const len = Math.hypot(dx, dy);
|
|
395
|
+
if (len < 1) return p;
|
|
396
|
+
|
|
397
|
+
const angle = Math.atan2(dy, dx);
|
|
398
|
+
const snapped = Math.round(angle / SNAP_STEP) * SNAP_STEP;
|
|
399
|
+
|
|
400
|
+
if (Math.abs(angle - snapped) <= SNAP_TOLERANCE) {
|
|
401
|
+
return {
|
|
402
|
+
x: anchor.x + Math.cos(snapped) * len,
|
|
403
|
+
y: anchor.y + Math.sin(snapped) * len
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return p;
|
|
407
|
+
}
|
|
408
|
+
|
|
338
409
|
_toWorld(x, y) {
|
|
339
410
|
if (!this.world) return { x, y };
|
|
340
411
|
const global = new PIXI.Point(x, y);
|
|
@@ -389,24 +460,29 @@ export class DrawingTool extends BaseTool {
|
|
|
389
460
|
const props = meta.properties || {};
|
|
390
461
|
const pts = Array.isArray(props.points) ? props.points : [];
|
|
391
462
|
if (pts.length >= 2) {
|
|
392
|
-
//
|
|
463
|
+
// Масштаб берём из локальных границ (не зависят от zoom) — как в _containsPoint DrawingObject
|
|
464
|
+
const lb = typeof pixi.getLocalBounds === 'function' ? pixi.getLocalBounds() : b;
|
|
393
465
|
const baseW = props.baseWidth || 1;
|
|
394
466
|
const baseH = props.baseHeight || 1;
|
|
395
|
-
const scaleX = baseW ? (
|
|
396
|
-
const scaleY = baseH ? (
|
|
467
|
+
const scaleX = baseW ? (lb.width / baseW) : 1;
|
|
468
|
+
const scaleY = baseH ? (lb.height / baseH) : 1;
|
|
397
469
|
const eraserThresh = Math.max(6, (props.strokeWidth || 2) / 2 + radius);
|
|
398
470
|
// трансформируем сегмент ластика в локальные координаты фигуры
|
|
399
471
|
const localPrev = pixi.toLocal(new PIXI.Point(prevGlobal.x, prevGlobal.y));
|
|
400
472
|
const localCurr = pixi.toLocal(new PIXI.Point(currGlobal.x, currGlobal.y));
|
|
401
|
-
// Проверяем пересечение с каждым отрезком
|
|
473
|
+
// Проверяем пересечение отрезка ластика с каждым отрезком рисунка.
|
|
474
|
+
// Используем отрезок→отрезок, а не точка→отрезок: при быстром движении
|
|
475
|
+
// оба конца сегмента ластика могут быть дальше threshold, но сам отрезок
|
|
476
|
+
// пересекает линию — именно это давало «раз через раз» при быстром мазке.
|
|
402
477
|
for (let i = 0; i < pts.length - 1 && !intersects; i++) {
|
|
403
478
|
const ax = pts[i].x * scaleX;
|
|
404
479
|
const ay = pts[i].y * scaleY;
|
|
405
480
|
const bx = pts[i + 1].x * scaleX;
|
|
406
481
|
const by = pts[i + 1].y * scaleY;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
482
|
+
if (this._distanceSegmentToSegment(
|
|
483
|
+
localPrev.x, localPrev.y, localCurr.x, localCurr.y,
|
|
484
|
+
ax, ay, bx, by
|
|
485
|
+
) <= eraserThresh) intersects = true;
|
|
410
486
|
}
|
|
411
487
|
}
|
|
412
488
|
}
|
|
@@ -445,6 +521,30 @@ export class DrawingTool extends BaseTool {
|
|
|
445
521
|
const cy = ay + t * aby;
|
|
446
522
|
return Math.hypot(px - cx, py - cy);
|
|
447
523
|
}
|
|
524
|
+
|
|
525
|
+
// Минимальное расстояние между отрезком p1→p2 и отрезком q1→q2.
|
|
526
|
+
// Возвращает 0 если отрезки пересекаются — гарантирует стирание при
|
|
527
|
+
// быстром поперечном движении, когда ни одна из концевых точек не близка к линии.
|
|
528
|
+
_distanceSegmentToSegment(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y) {
|
|
529
|
+
if (this._segmentsIntersect(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y)) return 0;
|
|
530
|
+
return Math.min(
|
|
531
|
+
this._distancePointToSegment(p1x, p1y, q1x, q1y, q2x, q2y),
|
|
532
|
+
this._distancePointToSegment(p2x, p2y, q1x, q1y, q2x, q2y),
|
|
533
|
+
this._distancePointToSegment(q1x, q1y, p1x, p1y, p2x, p2y),
|
|
534
|
+
this._distancePointToSegment(q2x, q2y, p1x, p1y, p2x, p2y)
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_segmentsIntersect(p1x, p1y, p2x, p2y, q1x, q1y, q2x, q2y) {
|
|
539
|
+
const d1x = p2x - p1x, d1y = p2y - p1y;
|
|
540
|
+
const d2x = q2x - q1x, d2y = q2y - q1y;
|
|
541
|
+
const cross = d1x * d2y - d1y * d2x;
|
|
542
|
+
if (Math.abs(cross) < 1e-10) return false; // параллельны или коллинеарны
|
|
543
|
+
const dx = q1x - p1x, dy = q1y - p1y;
|
|
544
|
+
const t = (dx * d2y - dy * d2x) / cross;
|
|
545
|
+
const u = (dx * d1y - dy * d1x) / cross;
|
|
546
|
+
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
|
|
547
|
+
}
|
|
448
548
|
}
|
|
449
549
|
|
|
450
550
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { BaseTool } from '../BaseTool.js';
|
|
2
|
+
import * as PIXI from 'pixi.js';
|
|
3
|
+
|
|
4
|
+
const LASER_COLOR = 0xff2222;
|
|
5
|
+
const LASER_DOT_COLOR = 0xff4444;
|
|
6
|
+
const TRAIL_MAX_POINTS = 30;
|
|
7
|
+
const FADE_STEPS = 20;
|
|
8
|
+
const FADE_INTERVAL_MS = 40;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Инструмент «Лазерная указка» — оставляет эфемерный затухающий след, ничего не сохраняет.
|
|
12
|
+
*/
|
|
13
|
+
export class LaserPointerTool extends BaseTool {
|
|
14
|
+
constructor(eventBus) {
|
|
15
|
+
super('laser', eventBus);
|
|
16
|
+
this.hotkey = null;
|
|
17
|
+
this.cursor = 'crosshair';
|
|
18
|
+
|
|
19
|
+
this.isDrawing = false;
|
|
20
|
+
this.points = [];
|
|
21
|
+
this.graphics = null;
|
|
22
|
+
this.app = null;
|
|
23
|
+
this._fadeTimer = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
activate(app) {
|
|
27
|
+
super.activate();
|
|
28
|
+
this.app = app;
|
|
29
|
+
if (app && app.view) app.view.style.cursor = 'crosshair';
|
|
30
|
+
if (app && app.stage) {
|
|
31
|
+
app.stage.sortableChildren = true;
|
|
32
|
+
this.graphics = new PIXI.Graphics();
|
|
33
|
+
this.graphics.zIndex = 3000;
|
|
34
|
+
this.graphics.name = 'laser-trail';
|
|
35
|
+
app.stage.addChild(this.graphics);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
deactivate() {
|
|
40
|
+
this._clearFadeTimer();
|
|
41
|
+
if (this.graphics) {
|
|
42
|
+
this.graphics.clear();
|
|
43
|
+
if (this.graphics.parent) this.graphics.parent.removeChild(this.graphics);
|
|
44
|
+
this.graphics.destroy();
|
|
45
|
+
this.graphics = null;
|
|
46
|
+
}
|
|
47
|
+
if (this.app && this.app.view) this.app.view.style.cursor = '';
|
|
48
|
+
this.app = null;
|
|
49
|
+
this.points = [];
|
|
50
|
+
this.isDrawing = false;
|
|
51
|
+
super.deactivate();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onMouseDown(event) {
|
|
55
|
+
this.isDrawing = true;
|
|
56
|
+
this._clearFadeTimer();
|
|
57
|
+
if (this.graphics) this.graphics.alpha = 1;
|
|
58
|
+
this.points = [{ x: event.x, y: event.y }];
|
|
59
|
+
this._redrawTrail();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onMouseMove(event) {
|
|
63
|
+
if (!this.isDrawing) return;
|
|
64
|
+
this.points.push({ x: event.x, y: event.y });
|
|
65
|
+
if (this.points.length > TRAIL_MAX_POINTS) this.points.shift();
|
|
66
|
+
this._redrawTrail();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onMouseUp() {
|
|
70
|
+
this.isDrawing = false;
|
|
71
|
+
this._scheduleFade();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_redrawTrail() {
|
|
75
|
+
if (!this.graphics) return;
|
|
76
|
+
this.graphics.clear();
|
|
77
|
+
const n = this.points.length;
|
|
78
|
+
if (n < 2) return;
|
|
79
|
+
|
|
80
|
+
for (let i = 1; i < n; i++) {
|
|
81
|
+
const t = i / n;
|
|
82
|
+
const width = Math.max(1, t * 3);
|
|
83
|
+
this.graphics.lineStyle(width, LASER_COLOR, t * 0.85);
|
|
84
|
+
this.graphics.moveTo(this.points[i - 1].x, this.points[i - 1].y);
|
|
85
|
+
this.graphics.lineTo(this.points[i].x, this.points[i].y);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const last = this.points[n - 1];
|
|
89
|
+
this.graphics.beginFill(LASER_DOT_COLOR, 0.95);
|
|
90
|
+
this.graphics.drawCircle(last.x, last.y, 5);
|
|
91
|
+
this.graphics.endFill();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_scheduleFade() {
|
|
95
|
+
this._clearFadeTimer();
|
|
96
|
+
let step = 0;
|
|
97
|
+
this._fadeTimer = setInterval(() => {
|
|
98
|
+
step++;
|
|
99
|
+
if (!this.graphics) { clearInterval(this._fadeTimer); this._fadeTimer = null; return; }
|
|
100
|
+
const alpha = Math.max(0, 1 - step / FADE_STEPS);
|
|
101
|
+
this.graphics.alpha = alpha;
|
|
102
|
+
if (step >= FADE_STEPS) {
|
|
103
|
+
clearInterval(this._fadeTimer);
|
|
104
|
+
this._fadeTimer = null;
|
|
105
|
+
if (this.graphics) {
|
|
106
|
+
this.graphics.clear();
|
|
107
|
+
this.graphics.alpha = 1;
|
|
108
|
+
}
|
|
109
|
+
this.points = [];
|
|
110
|
+
}
|
|
111
|
+
}, FADE_INTERVAL_MS);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_clearFadeTimer() {
|
|
115
|
+
if (this._fadeTimer) {
|
|
116
|
+
clearInterval(this._fadeTimer);
|
|
117
|
+
this._fadeTimer = null;
|
|
118
|
+
}
|
|
119
|
+
if (this.graphics) this.graphics.alpha = 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -28,7 +28,9 @@ import {
|
|
|
28
28
|
openFileNameEditor as openFileNameEditorViaController,
|
|
29
29
|
closeFileNameEditor as closeFileNameEditorViaController,
|
|
30
30
|
closeTextEditor as closeTextEditorViaController,
|
|
31
|
-
closeMindmapEditor as closeMindmapEditorViaController
|
|
31
|
+
closeMindmapEditor as closeMindmapEditorViaController,
|
|
32
|
+
openFrameTitleEditor as openFrameTitleEditorViaController,
|
|
33
|
+
closeFrameTitleEditor as closeFrameTitleEditorViaController,
|
|
32
34
|
} from './selection/InlineEditorController.js';
|
|
33
35
|
import {
|
|
34
36
|
hitTest as hitTestViaService,
|
|
@@ -206,6 +208,20 @@ export class SelectTool extends BaseTool {
|
|
|
206
208
|
endBoxSelect() {
|
|
207
209
|
return endBoxSelectViaController.call(this);
|
|
208
210
|
}
|
|
211
|
+
|
|
212
|
+
startLassoSelect(event) {
|
|
213
|
+
this.isLassoSelect = true;
|
|
214
|
+
if (this._lassoSelect) this._lassoSelect.start({ x: event.x, y: event.y }, this.isMultiSelect);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
updateLassoSelect(event) {
|
|
218
|
+
if (this._lassoSelect) this._lassoSelect.update({ x: event.x, y: event.y });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
endLassoSelect() {
|
|
222
|
+
this.isLassoSelect = false;
|
|
223
|
+
if (this._lassoSelect) this._lassoSelect.end();
|
|
224
|
+
}
|
|
209
225
|
rectIntersectsRect(a, b) {
|
|
210
226
|
return !(
|
|
211
227
|
b.x > a.x + a.width ||
|
|
@@ -346,6 +362,14 @@ export class SelectTool extends BaseTool {
|
|
|
346
362
|
return closeMindmapEditorViaController.call(this, commit);
|
|
347
363
|
}
|
|
348
364
|
|
|
365
|
+
_openFrameTitleEditor(object, create = false) {
|
|
366
|
+
return openFrameTitleEditorViaController.call(this, object, create);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_closeFrameTitleEditor(commit) {
|
|
370
|
+
return closeFrameTitleEditorViaController.call(this, commit);
|
|
371
|
+
}
|
|
372
|
+
|
|
349
373
|
destroy() {
|
|
350
374
|
return destroySelectTool.call(this, () => super.destroy());
|
|
351
375
|
}
|
|
@@ -39,7 +39,12 @@ export class TextTool extends BaseTool {
|
|
|
39
39
|
color: '#000000',
|
|
40
40
|
textAlign: 'left',
|
|
41
41
|
fontWeight: 'normal',
|
|
42
|
-
fontStyle: 'normal'
|
|
42
|
+
fontStyle: 'normal',
|
|
43
|
+
bold: false,
|
|
44
|
+
italic: false,
|
|
45
|
+
underline: false,
|
|
46
|
+
strikethrough: false,
|
|
47
|
+
listType: 'none',
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -11,12 +11,21 @@ import {
|
|
|
11
11
|
const DRAG_THRESHOLD = 4;
|
|
12
12
|
/** Порог «у кромки» в CSS-пикселях. */
|
|
13
13
|
const EDGE_THRESHOLD_CSS = 10;
|
|
14
|
+
/** Порог магнита к коннектору цели — строго больше EDGE_THRESHOLD_CSS, иначе грань перехватит. */
|
|
15
|
+
const ANCHOR_SNAP_CSS = 16;
|
|
14
16
|
/** Радиус поиска ближайшего объекта при клике по якорю (world-px). */
|
|
15
17
|
const CLICK_FIND_RADIUS = 400;
|
|
16
18
|
/** Зазор между дубликатом и источником при автосоздании (world-px). */
|
|
17
19
|
const CLONE_GAP = 40;
|
|
18
20
|
/** Типы объектов, к которым можно привязать коннектор (из ConnectionAnchorsLayer). */
|
|
19
21
|
const ALLOWED_BIND_TYPES = new Set(['shape', 'note', 'image', 'text', 'simple-text', 'file']);
|
|
22
|
+
/** Нормализованные якоря коннекторов: top, right, bottom, left. */
|
|
23
|
+
const TARGET_ANCHORS = [
|
|
24
|
+
{ x: 0.5, y: 0 },
|
|
25
|
+
{ x: 1, y: 0.5 },
|
|
26
|
+
{ x: 0.5, y: 1 },
|
|
27
|
+
{ x: 0, y: 0.5 },
|
|
28
|
+
];
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* Обрабатывает жест «pointerdown на точке подключения → drag → drop»
|
|
@@ -92,6 +101,23 @@ export class ConnectorDragController {
|
|
|
92
101
|
return { x: posData.position.x, y: posData.position.y, ...sizeData.size };
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Возвращает ближайший якорь из TARGET_ANCHORS в пределах ANCHOR_SNAP_CSS от worldPt,
|
|
106
|
+
* иначе null. Приоритет выше грани — вызывать в _resolveEnd первым.
|
|
107
|
+
*/
|
|
108
|
+
_snapToAnchor(bounds, worldPt) {
|
|
109
|
+
const scale = this._world()?.scale?.x || 1;
|
|
110
|
+
const thr = ANCHOR_SNAP_CSS / scale;
|
|
111
|
+
let best = null, bestDist = thr;
|
|
112
|
+
for (const a of TARGET_ANCHORS) {
|
|
113
|
+
const ax = bounds.x + a.x * bounds.width;
|
|
114
|
+
const ay = bounds.y + a.y * bounds.height;
|
|
115
|
+
const d = Math.hypot(worldPt.x - ax, worldPt.y - ay);
|
|
116
|
+
if (d <= bestDist) { bestDist = d; best = a; }
|
|
117
|
+
}
|
|
118
|
+
return best;
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
/** Возвращает true, если worldPt находится в пределах EDGE_THRESHOLD_CSS от кромки. */
|
|
96
122
|
_nearEdge(bounds, worldPt) {
|
|
97
123
|
const scale = this._world()?.scale?.x || 1;
|
|
@@ -116,6 +142,12 @@ export class ConnectorDragController {
|
|
|
116
142
|
if (objectId && objectId !== sourceBoundId) {
|
|
117
143
|
const bounds = this._objectBounds(objectId);
|
|
118
144
|
if (bounds) {
|
|
145
|
+
// ПРИОРИТЕТ 1: магнит к коннектору (середина грани)
|
|
146
|
+
const snapAnchor = this._snapToAnchor(bounds, worldPt);
|
|
147
|
+
if (snapAnchor) {
|
|
148
|
+
return { boundId: objectId, anchor: snapAnchor, isPrecise: true, isExact: false };
|
|
149
|
+
}
|
|
150
|
+
// ПРИОРИТЕТ 2: произвольная точка грани
|
|
119
151
|
if (this._nearEdge(bounds, worldPt)) {
|
|
120
152
|
return {
|
|
121
153
|
boundId: objectId,
|
|
@@ -124,6 +156,7 @@ export class ConnectorDragController {
|
|
|
124
156
|
isExact: false,
|
|
125
157
|
};
|
|
126
158
|
}
|
|
159
|
+
// ПРИОРИТЕТ 3: центр объекта
|
|
127
160
|
return { boundId: objectId, anchor: { x: 0.5, y: 0.5 }, isPrecise: false, isExact: false };
|
|
128
161
|
}
|
|
129
162
|
}
|
|
@@ -150,19 +183,33 @@ export class ConnectorDragController {
|
|
|
150
183
|
|
|
151
184
|
if (!this._previewGraphics) return;
|
|
152
185
|
|
|
153
|
-
const worldPt
|
|
154
|
-
const fromPt
|
|
155
|
-
drawPreview(this._previewGraphics, fromPt, worldPt);
|
|
186
|
+
const worldPt = this._toWorld(e.clientX, e.clientY);
|
|
187
|
+
const fromPt = terminalWorldPoint(this.eventBus, this._sourceTerminal);
|
|
156
188
|
|
|
157
189
|
this._highlightGraphics.clear();
|
|
158
190
|
const objectId = this._hitTest(e.clientX, e.clientY);
|
|
191
|
+
let previewEnd = worldPt;
|
|
159
192
|
if (objectId && objectId !== this._sourceTerminal?.boundId) {
|
|
160
193
|
const bounds = this._objectBounds(objectId);
|
|
161
194
|
if (bounds) {
|
|
162
195
|
this._highlightGraphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.85 });
|
|
163
196
|
this._highlightGraphics.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
197
|
+
// Подводим превью к коннектору, если курсор в зоне магнита
|
|
198
|
+
const snapAnchor = this._snapToAnchor(bounds, worldPt);
|
|
199
|
+
if (snapAnchor) {
|
|
200
|
+
previewEnd = {
|
|
201
|
+
x: bounds.x + snapAnchor.x * bounds.width,
|
|
202
|
+
y: bounds.y + snapAnchor.y * bounds.height,
|
|
203
|
+
};
|
|
204
|
+
// Подсвечиваем конкретный коннектор
|
|
205
|
+
this._highlightGraphics.lineStyle(0);
|
|
206
|
+
this._highlightGraphics.beginFill(0x2563EB, 1);
|
|
207
|
+
this._highlightGraphics.drawCircle(previewEnd.x, previewEnd.y, 6);
|
|
208
|
+
this._highlightGraphics.endFill();
|
|
209
|
+
}
|
|
164
210
|
}
|
|
165
211
|
}
|
|
212
|
+
drawPreview(this._previewGraphics, fromPt, previewEnd);
|
|
166
213
|
}
|
|
167
214
|
|
|
168
215
|
_onUp(e) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as PIXI from 'pixi.js';
|
|
2
2
|
import { Events } from '../../../core/events/Events.js';
|
|
3
|
+
import { buildPath } from '../../../services/ConnectorRouter.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Переиспользуемые хелперы жеста коннектора.
|
|
@@ -53,33 +54,46 @@ export function computeAnchor(eventBus, objectId, worldPt) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* Рисует превью
|
|
57
|
+
* Рисует превью коннектора со стрелкой в PIXI-графику (PIXI 7 API).
|
|
58
|
+
* Маршрут совпадает с тем, что будет создан при отпускании (по умолчанию 'elbow'),
|
|
59
|
+
* поэтому «резинка» во время перетаскивания выглядит как итоговый коннектор.
|
|
57
60
|
* graphics — PIXI.Graphics, уже добавленный в worldLayer.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} route 'straight'|'elbow'|'bezier' — должен совпадать с дефолтом createConnectorFromTerminals
|
|
58
63
|
*/
|
|
59
|
-
export function drawPreview(graphics, fromWorldPt, toWorldPt) {
|
|
64
|
+
export function drawPreview(graphics, fromWorldPt, toWorldPt, route = 'elbow') {
|
|
60
65
|
graphics.clear();
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
// Без привязанной грани dir-векторы неизвестны → buildPath даёт H-V-H/V-H-V излом
|
|
68
|
+
const pts = buildPath(fromWorldPt, toWorldPt, route);
|
|
69
|
+
if (pts.length < 2) return;
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
graphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.7, cap: 'round', join: 'round' });
|
|
72
|
+
graphics.moveTo(pts[0].x, pts[0].y);
|
|
73
|
+
for (let i = 1; i < pts.length; i++) {
|
|
74
|
+
graphics.lineTo(pts[i].x, pts[i].y);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Наконечник по направлению последнего сегмента — открытый chevron,
|
|
78
|
+
// как у финального коннектора (ConnectorLayer.drawHead, kind='arrow').
|
|
79
|
+
const tip = pts[pts.length - 1];
|
|
80
|
+
const prev = pts[pts.length - 2];
|
|
81
|
+
const dx = tip.x - prev.x;
|
|
82
|
+
const dy = tip.y - prev.y;
|
|
68
83
|
const len = Math.hypot(dx, dy);
|
|
69
84
|
if (len > 10) {
|
|
70
85
|
const ux = dx / len;
|
|
71
86
|
const uy = dy / len;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
graphics.endFill();
|
|
87
|
+
const px = -uy;
|
|
88
|
+
const py = ux;
|
|
89
|
+
const aLen = 12;
|
|
90
|
+
const aHalf = 5;
|
|
91
|
+
const bx = tip.x - ux * aLen;
|
|
92
|
+
const by = tip.y - uy * aLen;
|
|
93
|
+
graphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.7, cap: 'round', join: 'round' });
|
|
94
|
+
graphics.moveTo(bx + px * aHalf, by + py * aHalf);
|
|
95
|
+
graphics.lineTo(tip.x, tip.y);
|
|
96
|
+
graphics.lineTo(bx - px * aHalf, by - py * aHalf);
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -102,7 +116,7 @@ export function createConnectorFromTerminals(core, eventBus, sourceTerminal, end
|
|
|
102
116
|
width: 2,
|
|
103
117
|
dash: false,
|
|
104
118
|
head: { start: false, end: true },
|
|
105
|
-
route: '
|
|
119
|
+
route: 'elbow',
|
|
106
120
|
},
|
|
107
121
|
});
|
|
108
122
|
}
|
|
@@ -123,6 +123,7 @@ export class PlacementInputRouter {
|
|
|
123
123
|
|
|
124
124
|
let props = host.pending.properties || {};
|
|
125
125
|
const isTextWithEditing = host.pending.type === 'text' && props.editOnCreate;
|
|
126
|
+
const isShapeType = host.pending.type === 'shape';
|
|
126
127
|
const isImage = host.pending.type === 'image';
|
|
127
128
|
const isFile = host.pending.type === 'file';
|
|
128
129
|
const presetSize = {
|
|
@@ -250,12 +251,32 @@ export class PlacementInputRouter {
|
|
|
250
251
|
y: Math.round(worldPoint.y - side / 2)
|
|
251
252
|
};
|
|
252
253
|
}
|
|
254
|
+
if (isShapeType) {
|
|
255
|
+
const handleShapeCreated = (objectData) => {
|
|
256
|
+
if (objectData.type === 'shape') {
|
|
257
|
+
host.eventBus.off(Events.Object.Created, handleShapeCreated);
|
|
258
|
+
host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
host.eventBus.emit(Events.Tool.ObjectEdit, {
|
|
261
|
+
object: {
|
|
262
|
+
id: objectData.id,
|
|
263
|
+
type: 'shape',
|
|
264
|
+
position: objectData.position,
|
|
265
|
+
properties: { content: '' }
|
|
266
|
+
},
|
|
267
|
+
create: true
|
|
268
|
+
});
|
|
269
|
+
}, 50);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
host.eventBus.on(Events.Object.Created, handleShapeCreated);
|
|
273
|
+
}
|
|
253
274
|
host.payloadFactory.emitGenericPlacement(host.pending.type, position, props);
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
host.pending = null;
|
|
257
278
|
host.hideGhost();
|
|
258
|
-
if (!isTextWithEditing && !(isFile && props.selectFileOnPlace)) {
|
|
279
|
+
if (!isTextWithEditing && !isShapeType && !(isFile && props.selectFileOnPlace)) {
|
|
259
280
|
host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
260
281
|
}
|
|
261
282
|
}
|