@sequent-org/moodboard 1.4.30 → 1.4.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/core/PixiEngine.js +34 -5
- package/src/core/bootstrap/CoreInitializer.js +4 -0
- package/src/core/commands/CreateConnectorCommand.js +25 -0
- package/src/core/commands/GroupMoveCommand.js +2 -2
- package/src/core/commands/MoveObjectCommand.js +1 -1
- package/src/core/commands/UpdateConnectorCommand.js +38 -0
- package/src/core/events/Events.js +1 -0
- package/src/mindmap/MindmapCompoundContract.js +1 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +14 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +18 -0
- package/src/objects/ConnectorObject.js +85 -0
- package/src/objects/DrawingObject.js +47 -0
- package/src/objects/MindmapObject.js +21 -3
- package/src/objects/NoteObject.js +16 -8
- package/src/objects/ObjectFactory.js +3 -1
- package/src/objects/ShapeObject.js +1 -1
- package/src/services/ConnectorBindingResolver.js +204 -0
- package/src/services/ai/AiClient.js +30 -2
- package/src/services/ai/ChatSessionController.js +1 -0
- package/src/tools/ToolManager.js +3 -0
- package/src/tools/manager/PointerGestureController.js +206 -0
- package/src/tools/manager/ToolEventRouter.js +10 -0
- package/src/tools/manager/ToolManagerGuards.js +3 -1
- package/src/tools/manager/ToolManagerLifecycle.js +70 -58
- package/src/tools/object-tools/ConnectorTool.js +147 -0
- package/src/tools/object-tools/PlacementTool.js +2 -2
- package/src/tools/object-tools/connector/ConnectorDragController.js +296 -0
- package/src/tools/object-tools/connector/connectorGesture.js +108 -0
- package/src/tools/object-tools/placement/GhostController.js +4 -4
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -2
- package/src/tools/object-tools/placement/PlacementInputRouter.js +5 -5
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +11 -2
- package/src/tools/object-tools/selection/SelectInputRouter.js +33 -4
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +12 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +3 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +1 -2
- package/src/tools/object-tools/selection/TextEditorSyncService.js +4 -4
- package/src/tools/object-tools/selection/TextInlineEditorController.js +21 -3
- package/src/tools/object-tools/selection/TransformInteractionController.js +4 -6
- package/src/ui/HtmlTextLayer.js +212 -5
- package/src/ui/animation/HoverLiftController.js +395 -0
- package/src/ui/chat/ChatComposer.js +0 -5
- package/src/ui/chat/ChatWindow.js +167 -35
- package/src/ui/chat/icons.js +17 -1
- package/src/ui/connectors/ConnectionAnchorsLayer.js +231 -0
- package/src/ui/connectors/ConnectorLayer.js +251 -0
- package/src/ui/handles/HandlesDomRenderer.js +11 -7
- package/src/ui/handles/HandlesInteractionController.js +65 -34
- package/src/ui/handles/HandlesPositioningService.js +41 -6
- package/src/ui/mindmap/MindmapCollapseGraph.js +169 -0
- package/src/ui/mindmap/MindmapCollapseLayer.js +380 -0
- package/src/ui/mindmap/MindmapConnectionLayer.js +50 -25
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +223 -2
- package/src/ui/mindmap/MindmapLayoutConfig.js +12 -0
- package/src/ui/styles/chat.css +1 -0
- package/src/ui/styles/toolbar.css +6 -0
- package/src/ui/styles/workspace.css +83 -21
- package/src/ui/toolbar/ToolbarPopupsController.js +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { Events } from '../../../core/events/Events.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Переиспользуемые хелперы жеста коннектора.
|
|
6
|
+
* Все функции принимают eventBus и данные явными аргументами — без this.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Возвращает world-точку терминала.
|
|
11
|
+
* Свободный терминал: terminal.point напрямую.
|
|
12
|
+
* Привязанный: top-left объекта + anchor * size (CONNECTORS.md раздел 3).
|
|
13
|
+
*/
|
|
14
|
+
export function terminalWorldPoint(eventBus, terminal) {
|
|
15
|
+
if (!terminal) return { x: 0, y: 0 };
|
|
16
|
+
if (terminal.point) return terminal.point;
|
|
17
|
+
|
|
18
|
+
const posData = { objectId: terminal.boundId, position: null };
|
|
19
|
+
const sizeData = { objectId: terminal.boundId, size: null };
|
|
20
|
+
eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
21
|
+
eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
22
|
+
|
|
23
|
+
const pos = posData.position;
|
|
24
|
+
const size = sizeData.size;
|
|
25
|
+
if (pos && size) {
|
|
26
|
+
return {
|
|
27
|
+
x: pos.x + (terminal.anchor?.x ?? 0.5) * (size.width || 0),
|
|
28
|
+
y: pos.y + (terminal.anchor?.y ?? 0.5) * (size.height || 0),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { x: 0, y: 0 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Нормализованный якорь по позиции клика внутри bbox объекта.
|
|
36
|
+
* Если объект не найден — возвращает центр { x:0.5, y:0.5 }.
|
|
37
|
+
*/
|
|
38
|
+
export function computeAnchor(eventBus, objectId, worldPt) {
|
|
39
|
+
const posData = { objectId, position: null };
|
|
40
|
+
const sizeData = { objectId, size: null };
|
|
41
|
+
eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
42
|
+
eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
43
|
+
|
|
44
|
+
const pos = posData.position;
|
|
45
|
+
const size = sizeData.size;
|
|
46
|
+
if (pos && size && size.width > 0 && size.height > 0) {
|
|
47
|
+
return {
|
|
48
|
+
x: Math.max(0, Math.min(1, (worldPt.x - pos.x) / size.width)),
|
|
49
|
+
y: Math.max(0, Math.min(1, (worldPt.y - pos.y) / size.height)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { x: 0.5, y: 0.5 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Рисует превью линии со стрелкой в PIXI-графику (PIXI 7 API).
|
|
57
|
+
* graphics — PIXI.Graphics, уже добавленный в worldLayer.
|
|
58
|
+
*/
|
|
59
|
+
export function drawPreview(graphics, fromWorldPt, toWorldPt) {
|
|
60
|
+
graphics.clear();
|
|
61
|
+
|
|
62
|
+
graphics.lineStyle({ width: 2, color: 0x2563EB, alpha: 0.7, cap: 'round' });
|
|
63
|
+
graphics.moveTo(fromWorldPt.x, fromWorldPt.y);
|
|
64
|
+
graphics.lineTo(toWorldPt.x, toWorldPt.y);
|
|
65
|
+
|
|
66
|
+
const dx = toWorldPt.x - fromWorldPt.x;
|
|
67
|
+
const dy = toWorldPt.y - fromWorldPt.y;
|
|
68
|
+
const len = Math.hypot(dx, dy);
|
|
69
|
+
if (len > 10) {
|
|
70
|
+
const ux = dx / len;
|
|
71
|
+
const uy = dy / len;
|
|
72
|
+
const aLen = 10;
|
|
73
|
+
const aAng = 0.4;
|
|
74
|
+
graphics.beginFill(0x2563EB, 0.7);
|
|
75
|
+
graphics.drawPolygon([
|
|
76
|
+
toWorldPt.x, toWorldPt.y,
|
|
77
|
+
toWorldPt.x - aLen * (ux * Math.cos(aAng) - uy * Math.sin(aAng)),
|
|
78
|
+
toWorldPt.y - aLen * (uy * Math.cos(aAng) + ux * Math.sin(aAng)),
|
|
79
|
+
toWorldPt.x - aLen * (ux * Math.cos(-aAng) - uy * Math.sin(-aAng)),
|
|
80
|
+
toWorldPt.y - aLen * (uy * Math.cos(-aAng) + ux * Math.sin(-aAng)),
|
|
81
|
+
]);
|
|
82
|
+
graphics.endFill();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Создаёт объект коннектора через core.createObject с дефолтным стилем.
|
|
88
|
+
* position — top-left от min(startPt, endPt).
|
|
89
|
+
*/
|
|
90
|
+
export function createConnectorFromTerminals(core, eventBus, sourceTerminal, endTerminal) {
|
|
91
|
+
const startPt = terminalWorldPoint(eventBus, sourceTerminal);
|
|
92
|
+
const endPt = terminalWorldPoint(eventBus, endTerminal);
|
|
93
|
+
const position = {
|
|
94
|
+
x: Math.min(startPt.x, endPt.x),
|
|
95
|
+
y: Math.min(startPt.y, endPt.y),
|
|
96
|
+
};
|
|
97
|
+
core.createObject('connector', position, {
|
|
98
|
+
start: sourceTerminal,
|
|
99
|
+
end: endTerminal,
|
|
100
|
+
style: {
|
|
101
|
+
stroke: 0x2563EB,
|
|
102
|
+
width: 2,
|
|
103
|
+
dash: false,
|
|
104
|
+
head: { start: false, end: true },
|
|
105
|
+
route: 'straight',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -431,7 +431,7 @@ export class GhostController {
|
|
|
431
431
|
const kind = host.pending.properties?.kind || 'square';
|
|
432
432
|
const width = 100;
|
|
433
433
|
const height = 100;
|
|
434
|
-
const fillColor =
|
|
434
|
+
const fillColor = 0xffffff;
|
|
435
435
|
const cornerRadius = host.pending.properties?.cornerRadius || 10;
|
|
436
436
|
|
|
437
437
|
const shapeGraphics = new PIXI.Graphics();
|
|
@@ -523,9 +523,9 @@ export class GhostController {
|
|
|
523
523
|
const fillAlpha = (typeof host.pending.properties?.fillAlpha === 'number')
|
|
524
524
|
? host.pending.properties.fillAlpha
|
|
525
525
|
: 0.25;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
526
|
+
// Толщина обводки согласована с MindmapObject и ветками: 3 экранных пикселя.
|
|
527
|
+
const worldScale = Math.max(0.01, host.world?.scale?.x || 1);
|
|
528
|
+
const strokeWidth = 3 / worldScale;
|
|
529
529
|
const fontSize = Math.max(1, Math.round(host.pending.properties?.fontSize || MINDMAP_LAYOUT.fontSize));
|
|
530
530
|
const fontFamily = host.pending.properties?.fontFamily || 'Roboto, Arial, sans-serif';
|
|
531
531
|
const textColor = host.pending.properties?.textColor || 0x212121;
|
|
@@ -37,7 +37,7 @@ export class PlacementEventsBridge {
|
|
|
37
37
|
}
|
|
38
38
|
if (this.host.pending.placeOnMouseUp && this.host.app && this.host.app.view) {
|
|
39
39
|
const onUp = (ev) => {
|
|
40
|
-
this.host.app.view.removeEventListener('
|
|
40
|
+
this.host.app.view.removeEventListener('pointerup', onUp);
|
|
41
41
|
if (!this.host.pending) return;
|
|
42
42
|
const worldPoint = this.host._toWorld(ev.x, ev.y);
|
|
43
43
|
const position = {
|
|
@@ -50,7 +50,7 @@ export class PlacementEventsBridge {
|
|
|
50
50
|
this.host.hideGhost();
|
|
51
51
|
this.host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
52
52
|
};
|
|
53
|
-
this.host.app.view.addEventListener('
|
|
53
|
+
this.host.app.view.addEventListener('pointerup', onUp, { once: true });
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
});
|
|
@@ -61,7 +61,7 @@ export class PlacementInputRouter {
|
|
|
61
61
|
host.pending = null;
|
|
62
62
|
host.hideGhost();
|
|
63
63
|
if (host.app && host.app.view) {
|
|
64
|
-
host.app.view.removeEventListener('
|
|
64
|
+
host.app.view.removeEventListener('pointermove', host._onFrameDrawMoveBound);
|
|
65
65
|
host.app.view.style.cursor = '';
|
|
66
66
|
}
|
|
67
67
|
host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
@@ -84,7 +84,7 @@ export class PlacementInputRouter {
|
|
|
84
84
|
if (!host.pending) return;
|
|
85
85
|
if (host.pending.placeOnMouseUp) {
|
|
86
86
|
const onUp = (ev) => {
|
|
87
|
-
host.app.view.removeEventListener('
|
|
87
|
+
host.app.view.removeEventListener('pointerup', onUp);
|
|
88
88
|
const worldPoint = host._toWorld(ev.x, ev.y);
|
|
89
89
|
const position = {
|
|
90
90
|
x: Math.round(worldPoint.x - (host.pending.size?.width ?? 100) / 2),
|
|
@@ -96,7 +96,7 @@ export class PlacementInputRouter {
|
|
|
96
96
|
host.hideGhost();
|
|
97
97
|
host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
98
98
|
};
|
|
99
|
-
host.app.view.addEventListener('
|
|
99
|
+
host.app.view.addEventListener('pointerup', onUp, { once: true });
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
if (host.pending.type === 'frame-draw') {
|
|
@@ -110,8 +110,8 @@ export class PlacementInputRouter {
|
|
|
110
110
|
}
|
|
111
111
|
host._onFrameDrawMoveBound = (ev) => host._onFrameDrawMove(ev);
|
|
112
112
|
host._onFrameDrawUpBound = (ev) => host._onFrameDrawUp(ev);
|
|
113
|
-
host.app.view.addEventListener('
|
|
114
|
-
host.app.view.addEventListener('
|
|
113
|
+
host.app.view.addEventListener('pointermove', host._onFrameDrawMoveBound);
|
|
114
|
+
host.app.view.addEventListener('pointerup', host._onFrameDrawUpBound, { once: true });
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
showStaticTextAfterEditing,
|
|
14
14
|
updateGlobalTextEditorHandlesLayer,
|
|
15
15
|
} from './TextEditorLifecycleRegistry.js';
|
|
16
|
-
import { MINDMAP_LAYOUT } from '../../../ui/mindmap/MindmapLayoutConfig.js';
|
|
16
|
+
import { MINDMAP_LAYOUT, MINDMAP_AUTOFIT } from '../../../ui/mindmap/MindmapLayoutConfig.js';
|
|
17
17
|
|
|
18
18
|
function applyMindmapCaretFromClick({ create, objectId, object, textarea }) {
|
|
19
19
|
try {
|
|
@@ -417,9 +417,18 @@ export function openMindmapEditor(object, create = false) {
|
|
|
417
417
|
const placeholderWidth = measureMindmapTextWidthPx(textarea, measureEl, textarea.placeholder || '');
|
|
418
418
|
const baseCssWidth = Math.max(1, Math.round(stableBaseWorldWidth * getWorldToCssScale()));
|
|
419
419
|
const placeholderCssWidth = Math.max(1, Math.ceil(placeholderWidth + padding.left + padding.right));
|
|
420
|
-
const
|
|
420
|
+
const rawNextCssWidth = hasText
|
|
421
421
|
? Math.max(1, Math.ceil(textWidth + padding.left + padding.right))
|
|
422
422
|
: Math.max(baseCssWidth, placeholderCssWidth);
|
|
423
|
+
const level = properties?.mindmap?.level ?? 0;
|
|
424
|
+
const isRoot = level === 0;
|
|
425
|
+
const minCssW = Math.max(1, Math.round(
|
|
426
|
+
(isRoot ? MINDMAP_AUTOFIT.ROOT_MIN_WIDTH : MINDMAP_AUTOFIT.CHILD_MIN_WIDTH) * getWorldToCssScale()
|
|
427
|
+
));
|
|
428
|
+
const maxCssW = Math.max(1, Math.round(
|
|
429
|
+
(isRoot ? MINDMAP_AUTOFIT.ROOT_MAX_WIDTH : MINDMAP_AUTOFIT.CHILD_MAX_WIDTH) * getWorldToCssScale()
|
|
430
|
+
));
|
|
431
|
+
const nextCssWidth = Math.max(minCssW, Math.min(maxCssW, rawNextCssWidth));
|
|
423
432
|
const lineCount = getEditorLineCount();
|
|
424
433
|
const lineHeightPx = getEditorLineHeightPx();
|
|
425
434
|
const baseCssHeight = Math.max(1, Math.round(stableBaseWorldHeight * getWorldToCssScale()));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Events } from '../../../core/events/Events.js';
|
|
2
2
|
|
|
3
|
+
const DRAG_START_THRESHOLD_PX = 4;
|
|
4
|
+
|
|
3
5
|
export function onMouseDown(event) {
|
|
4
6
|
// Если активен текстовый редактор, закрываем его при клике вне
|
|
5
7
|
if (this.textEditor.active) {
|
|
@@ -20,6 +22,7 @@ export function onMouseDown(event) {
|
|
|
20
22
|
return; // Прерываем выполнение, чтобы не обрабатывать клик дальше
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
this._pendingDrag = null;
|
|
23
26
|
this.isMultiSelect = event.originalEvent.ctrlKey || event.originalEvent.metaKey || event.originalEvent.shiftKey;
|
|
24
27
|
|
|
25
28
|
// Проверяем, что под курсором
|
|
@@ -34,8 +37,8 @@ export function onMouseDown(event) {
|
|
|
34
37
|
const gb = this.computeGroupBounds();
|
|
35
38
|
const insideGroup = this.isPointInBounds({ x: event.x, y: event.y }, { x: gb.x, y: gb.y, width: gb.width, height: gb.height });
|
|
36
39
|
if (insideGroup) {
|
|
37
|
-
// Если клик внутри группы
|
|
38
|
-
this.
|
|
40
|
+
// Если клик внутри группы — откладываем до превышения порога смещения
|
|
41
|
+
this._pendingDrag = { isGroup: true, objectId: null, downX: event.x, downY: event.y, event };
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
44
|
// Вне группы — обычная логика
|
|
@@ -116,8 +119,8 @@ export function onMouseDown(event) {
|
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
|
-
//
|
|
120
|
-
this.
|
|
122
|
+
// Откладываем drag до превышения порога смещения
|
|
123
|
+
this._pendingDrag = { isGroup: false, objectId: selId, downX: event.x, downY: event.y, event };
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
126
|
}
|
|
@@ -140,6 +143,19 @@ export function onMouseMove(event) {
|
|
|
140
143
|
this.updateResize(event);
|
|
141
144
|
} else if (this.isRotating || this.isGroupRotating) {
|
|
142
145
|
this.updateRotate(event);
|
|
146
|
+
} else if (this._pendingDrag) {
|
|
147
|
+
const dx = event.x - this._pendingDrag.downX;
|
|
148
|
+
const dy = event.y - this._pendingDrag.downY;
|
|
149
|
+
if (Math.hypot(dx, dy) > DRAG_START_THRESHOLD_PX) {
|
|
150
|
+
const pending = this._pendingDrag;
|
|
151
|
+
this._pendingDrag = null;
|
|
152
|
+
if (pending.isGroup) {
|
|
153
|
+
this.startGroupDrag(pending.event);
|
|
154
|
+
} else {
|
|
155
|
+
this.startDrag(pending.objectId, pending.event);
|
|
156
|
+
}
|
|
157
|
+
this.updateDrag(event);
|
|
158
|
+
}
|
|
143
159
|
} else if (this.isDragging || this.isGroupDragging) {
|
|
144
160
|
this.updateDrag(event);
|
|
145
161
|
} else if (this.isBoxSelect) {
|
|
@@ -147,10 +163,23 @@ export function onMouseMove(event) {
|
|
|
147
163
|
} else {
|
|
148
164
|
// Обновляем курсор в зависимости от того, что под мышью
|
|
149
165
|
this.updateCursor(event);
|
|
166
|
+
|
|
167
|
+
// Эмитим событие наведения на объект
|
|
168
|
+
const hitData = { x: event.x, y: event.y, result: null };
|
|
169
|
+
this.emit(Events.Tool.HitTest, hitData);
|
|
170
|
+
const hoveredObjectId = hitData.result?.object || null;
|
|
171
|
+
if (this.lastHoveredObjectId !== hoveredObjectId) {
|
|
172
|
+
this.lastHoveredObjectId = hoveredObjectId;
|
|
173
|
+
this.emit(Events.Object.Hover, { objectId: hoveredObjectId });
|
|
174
|
+
}
|
|
150
175
|
}
|
|
151
176
|
}
|
|
152
177
|
|
|
153
178
|
export function onMouseUp(event) {
|
|
179
|
+
if (this._pendingDrag) {
|
|
180
|
+
this._pendingDrag = null;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
154
183
|
if (this.isResizing || this.isGroupResizing) {
|
|
155
184
|
this.endResize();
|
|
156
185
|
} else if (this.isRotating || this.isGroupRotating) {
|
|
@@ -93,6 +93,12 @@ export function deactivateSelectTool(superDeactivate) {
|
|
|
93
93
|
if (this.app && this.app.view) {
|
|
94
94
|
this.app.view.style.cursor = '';
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// Эмитим Hover с null при выходе из инструмента
|
|
98
|
+
if (this.lastHoveredObjectId !== null) {
|
|
99
|
+
this.lastHoveredObjectId = null;
|
|
100
|
+
this.emit(Events.Object.Hover, { objectId: null });
|
|
101
|
+
}
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
export function destroySelectTool(superDestroy) {
|
|
@@ -105,6 +111,12 @@ export function destroySelectTool(superDestroy) {
|
|
|
105
111
|
unregisterSelectToolCoreSubscriptions(this);
|
|
106
112
|
this.clearSelection();
|
|
107
113
|
|
|
114
|
+
// Эмитим Hover с null при уничтожении инструмента
|
|
115
|
+
if (this.lastHoveredObjectId !== null) {
|
|
116
|
+
this.lastHoveredObjectId = null;
|
|
117
|
+
this.emit(Events.Object.Hover, { objectId: null });
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
// Уничтожаем ручки изменения размера
|
|
109
121
|
if (this.resizeHandles) {
|
|
110
122
|
this.resizeHandles.destroy();
|
|
@@ -48,6 +48,9 @@ export function initializeSelectToolState(instance) {
|
|
|
48
48
|
instance.currentX = 0;
|
|
49
49
|
instance.currentY = 0;
|
|
50
50
|
|
|
51
|
+
// Последний наведённый объект (для избежания спама Hover событий)
|
|
52
|
+
instance.lastHoveredObjectId = null;
|
|
53
|
+
|
|
51
54
|
// Состояние поворота
|
|
52
55
|
instance.isRotating = false;
|
|
53
56
|
instance.rotateCenter = null;
|
|
@@ -31,8 +31,7 @@ export function applyInitialTextEditorTextareaStyles(textarea, { effectiveFontPx
|
|
|
31
31
|
textarea.style.height = `${initialHeightPx}px`;
|
|
32
32
|
textarea.setAttribute('rows', '1');
|
|
33
33
|
textarea.style.overflowY = 'hidden';
|
|
34
|
-
textarea.style.whiteSpace = 'pre
|
|
35
|
-
textarea.style.wordBreak = 'break-word';
|
|
34
|
+
textarea.style.whiteSpace = 'pre';
|
|
36
35
|
textarea.style.letterSpacing = '0px';
|
|
37
36
|
textarea.style.fontKerning = 'normal';
|
|
38
37
|
}
|
|
@@ -8,14 +8,14 @@ export function createRegularTextAutoSize({
|
|
|
8
8
|
minHBound,
|
|
9
9
|
onSizeChange,
|
|
10
10
|
}) {
|
|
11
|
-
const MAX_AUTO_WIDTH = 360;
|
|
12
|
-
|
|
13
11
|
return () => {
|
|
14
12
|
textarea.style.width = 'auto';
|
|
15
13
|
textarea.style.height = 'auto';
|
|
16
14
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
15
|
+
const fontPx = parseFloat(textarea.style.fontSize) || 16;
|
|
16
|
+
const rightPad = Math.ceil(fontPx * 0.7) + 6;
|
|
17
|
+
const naturalW = Math.max(1, Math.ceil(textarea.scrollWidth + rightPad));
|
|
18
|
+
const targetW = Math.round(Math.max(minWBound, naturalW));
|
|
19
19
|
textarea.style.width = `${targetW}px`;
|
|
20
20
|
wrapper.style.width = `${targetW}px`;
|
|
21
21
|
|
|
@@ -101,6 +101,14 @@ export function openTextEditor(object, create = false) {
|
|
|
101
101
|
}
|
|
102
102
|
// Прячем глобальные HTML-ручки на время редактирования, чтобы не было второй рамки
|
|
103
103
|
hideGlobalTextEditorHandlesLayer();
|
|
104
|
+
// Подавляем пересоздание ручек при паразитных ResizeUpdate (тач double-tap):
|
|
105
|
+
// host.update() внутри HandlesEventBridge вызывается при каждом ResizeUpdate,
|
|
106
|
+
// _handlesSuppressed=true гарантирует что showBounds не создаст ручки поверх textarea.
|
|
107
|
+
try {
|
|
108
|
+
if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer) {
|
|
109
|
+
window.moodboardHtmlHandlesLayer._handlesSuppressed = true;
|
|
110
|
+
}
|
|
111
|
+
} catch (_) {}
|
|
104
112
|
|
|
105
113
|
const app = this.app;
|
|
106
114
|
const world = app?.stage?.getChildByName && app.stage.getChildByName('worldLayer');
|
|
@@ -144,7 +152,7 @@ export function openTextEditor(object, create = false) {
|
|
|
144
152
|
|
|
145
153
|
const textarea = createTextEditorTextarea(content || '');
|
|
146
154
|
// Без доступного статичного HTML-элемента (часто при create) не оставляем Caveat как fallback.
|
|
147
|
-
textarea.style.fontFamily =
|
|
155
|
+
textarea.style.fontFamily = 'Caveat, Arial, cursive';
|
|
148
156
|
|
|
149
157
|
// Вычисляем межстрочный интервал; подгоняем к реальным значениям HTML-отображения
|
|
150
158
|
let lhInitial = computeTextEditorLineHeightPx(effectiveFontPx);
|
|
@@ -370,7 +378,9 @@ export function openTextEditor(object, create = false) {
|
|
|
370
378
|
|
|
371
379
|
// Если создаём новый текст — длина поля ровно как placeholder
|
|
372
380
|
if (create && !isNote) {
|
|
373
|
-
|
|
381
|
+
// +25% — запас на Caveat vs Arial: при незагруженном Caveat span рендерится в Arial,
|
|
382
|
+
// а Caveat (рукописный шрифт) заметно шире для кириллицы.
|
|
383
|
+
const startWidth = Math.max(1, Math.ceil(measureTextEditorPlaceholderWidth(textarea, 'Напишите что-нибудь') * 1.25));
|
|
374
384
|
const startHeight = Math.max(1, lhInitial - BASELINE_FIX + 10); // +5px сверху и +5px снизу
|
|
375
385
|
textarea.style.width = `${startWidth}px`;
|
|
376
386
|
textarea.style.height = `${startHeight}px`;
|
|
@@ -397,7 +407,7 @@ export function openTextEditor(object, create = false) {
|
|
|
397
407
|
? ({ widthPx, heightPx }) => {
|
|
398
408
|
try {
|
|
399
409
|
const scaleX = (worldLayerRef?.scale?.x) || 1;
|
|
400
|
-
const widthWorld = Math.max(1, Math.
|
|
410
|
+
const widthWorld = Math.max(1, Math.ceil(widthPx * viewRes / scaleX));
|
|
401
411
|
const heightWorld = Math.max(1, Math.round(heightPx * viewRes / scaleX));
|
|
402
412
|
const posReq = { objectId, position: null };
|
|
403
413
|
this.eventBus.emit(Events.Tool.GetObjectPosition, posReq);
|
|
@@ -485,5 +495,13 @@ export function openTextEditor(object, create = false) {
|
|
|
485
495
|
}
|
|
486
496
|
|
|
487
497
|
export function closeTextEditor(commit) {
|
|
498
|
+
// Снимаем подавление ручек до вызова closeTextEditorFromState,
|
|
499
|
+
// т.к. тот в конце вызывает updateGlobalTextEditorHandlesLayer() → update() → showBounds,
|
|
500
|
+
// и ручки должны пересоздаться нормально.
|
|
501
|
+
try {
|
|
502
|
+
if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer) {
|
|
503
|
+
window.moodboardHtmlHandlesLayer._handlesSuppressed = false;
|
|
504
|
+
}
|
|
505
|
+
} catch (_) {}
|
|
488
506
|
return closeTextEditorFromState(this, commit);
|
|
489
507
|
}
|
|
@@ -10,18 +10,16 @@ export function handleObjectSelect(objectId, event) {
|
|
|
10
10
|
if (this.isMultiSelect) {
|
|
11
11
|
this.removeFromSelection(objectId);
|
|
12
12
|
} else if (this.selection.size() > 1) {
|
|
13
|
-
|
|
14
|
-
this.startGroupDrag(event);
|
|
13
|
+
this._pendingDrag = { isGroup: true, objectId: null, downX: event.x, downY: event.y, event };
|
|
15
14
|
} else {
|
|
16
|
-
|
|
17
|
-
this.startDrag(objectId, event);
|
|
15
|
+
this._pendingDrag = { isGroup: false, objectId, downX: event.x, downY: event.y, event };
|
|
18
16
|
}
|
|
19
17
|
} else {
|
|
20
18
|
this.addToSelection(objectId);
|
|
21
19
|
if (this.selection.size() > 1) {
|
|
22
|
-
this.
|
|
20
|
+
this._pendingDrag = { isGroup: true, objectId: null, downX: event.x, downY: event.y, event };
|
|
23
21
|
} else {
|
|
24
|
-
this.
|
|
22
|
+
this._pendingDrag = { isGroup: false, objectId, downX: event.x, downY: event.y, event };
|
|
25
23
|
}
|
|
26
24
|
}
|
|
27
25
|
}
|