@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
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import * as PIXI from 'pixi.js';
|
|
2
2
|
|
|
3
|
+
import { Events } from '../../../core/events/Events.js';
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
* BoxSelectController — управление рамкой выделения и выбором по
|
|
6
|
+
* BoxSelectController — управление рамкой выделения и выбором по пересечению.
|
|
7
|
+
*
|
|
8
|
+
* Помимо обычного `setSelection` по intersection, на завершение жеста (`end`)
|
|
9
|
+
* эмитит `Tool.BoxSelectCommit` с дополнительным набором `objects` — id объектов,
|
|
10
|
+
* чьи bbox целиком содержатся в финальном rect (strict contains). Этот канал
|
|
11
|
+
* нужен подписчикам, которым важно отличать «попали целиком» от «зацепили краем»
|
|
12
|
+
* (например, добавление reference-картинок в чат-композер).
|
|
5
13
|
*/
|
|
14
|
+
function isRectContainedInRect(outer, inner) {
|
|
15
|
+
if (!outer || !inner) return false;
|
|
16
|
+
return inner.x >= outer.x
|
|
17
|
+
&& inner.y >= outer.y
|
|
18
|
+
&& inner.x + inner.width <= outer.x + outer.width
|
|
19
|
+
&& inner.y + inner.height <= outer.y + outer.height;
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
export class BoxSelectController {
|
|
7
23
|
constructor({ app, selection, emit, setSelection, clearSelection, rectIntersectsRect }) {
|
|
8
24
|
this.app = app;
|
|
@@ -32,6 +48,7 @@ export class BoxSelectController {
|
|
|
32
48
|
this.selectionGraphics.name = 'selection-box';
|
|
33
49
|
this.app.stage.addChild(this.selectionGraphics);
|
|
34
50
|
}
|
|
51
|
+
this.emit(Events.Tool.BoxSelectStart, {});
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
update(mouse) {
|
|
@@ -78,13 +95,17 @@ export class BoxSelectController {
|
|
|
78
95
|
const y = Math.min(this.selectionBox.startY, this.selectionBox.endY);
|
|
79
96
|
const w = Math.abs(this.selectionBox.endX - this.selectionBox.startX);
|
|
80
97
|
const h = Math.abs(this.selectionBox.endY - this.selectionBox.startY);
|
|
98
|
+
let commitBox = null;
|
|
99
|
+
let containedIds = [];
|
|
100
|
+
let matched = [];
|
|
81
101
|
if (w >= 2 && h >= 2) {
|
|
82
102
|
const box = { x, y, width: w, height: h };
|
|
103
|
+
commitBox = box;
|
|
83
104
|
const request = { objects: [] };
|
|
84
105
|
this.emit('get:all:objects', request);
|
|
85
|
-
const matched = [];
|
|
86
106
|
for (const item of request.objects) {
|
|
87
107
|
if (this.rectIntersectsRect(box, item.bounds)) matched.push(item.id);
|
|
108
|
+
if (isRectContainedInRect(box, item.bounds)) containedIds.push(item.id);
|
|
88
109
|
}
|
|
89
110
|
if (matched.length > 0) {
|
|
90
111
|
if (this.isMultiSelect) {
|
|
@@ -94,6 +115,7 @@ export class BoxSelectController {
|
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
}
|
|
118
|
+
this.emit(Events.Tool.BoxSelectCommit, { box: commitBox, objects: containedIds, selected: matched });
|
|
97
119
|
this.isActive = false;
|
|
98
120
|
this.selectionBox = null;
|
|
99
121
|
if (this.selectionGraphics && this.selectionGraphics.parent) this.selectionGraphics.parent.removeChild(this.selectionGraphics);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { Events } from '../../../core/events/Events.js';
|
|
3
|
+
import {
|
|
4
|
+
createFrameTitleEditorWrapper,
|
|
5
|
+
createFrameTitleEditorInput,
|
|
6
|
+
} from './InlineEditorDomFactory.js';
|
|
7
|
+
import { toScreenWithContainerOffset } from './InlineEditorPositioningService.js';
|
|
8
|
+
|
|
9
|
+
export function openFrameTitleEditor(object, _create = false) {
|
|
10
|
+
let objectId, properties;
|
|
11
|
+
|
|
12
|
+
const objData = object.object || object;
|
|
13
|
+
objectId = objData.id || object.id || null;
|
|
14
|
+
properties = objData.properties || object.properties || {};
|
|
15
|
+
|
|
16
|
+
const currentTitle = properties.title || 'Фрейм';
|
|
17
|
+
|
|
18
|
+
if (this.textEditor.active) {
|
|
19
|
+
if (this.textEditor.objectType === 'frame') {
|
|
20
|
+
this._closeFrameTitleEditor(true);
|
|
21
|
+
} else if (this.textEditor.objectType === 'file') {
|
|
22
|
+
this._closeFileNameEditor(true);
|
|
23
|
+
} else {
|
|
24
|
+
this._closeTextEditor(true);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!objectId) return;
|
|
29
|
+
|
|
30
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
31
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
32
|
+
|
|
33
|
+
let frameInstance = null;
|
|
34
|
+
if (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance) {
|
|
35
|
+
frameInstance = pixiReq.pixiObject._mb.instance;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!frameInstance || typeof frameInstance.hideTitle !== 'function') return;
|
|
39
|
+
|
|
40
|
+
frameInstance.hideTitle();
|
|
41
|
+
|
|
42
|
+
const view = this.app?.view || document.querySelector('canvas');
|
|
43
|
+
if (!view || !view.parentElement) {
|
|
44
|
+
frameInstance.showTitle();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const titleLayer = frameInstance.titleLayer;
|
|
49
|
+
const screenPos = titleLayer
|
|
50
|
+
? toScreenWithContainerOffset(titleLayer, view, 0, 0)
|
|
51
|
+
: { x: 0, y: 0 };
|
|
52
|
+
|
|
53
|
+
// Ширина редактора = ширина titleBg в экранных пикселях (с учётом зум-компенсации)
|
|
54
|
+
let inputWidth = 150;
|
|
55
|
+
if (titleLayer && frameInstance.titleBg) {
|
|
56
|
+
const bgRight = toScreenWithContainerOffset(titleLayer, view, frameInstance.titleBg.width || 150, 0);
|
|
57
|
+
inputWidth = Math.max(100, Math.round(bgRight.x - screenPos.x));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const wrapper = createFrameTitleEditorWrapper();
|
|
61
|
+
const input = createFrameTitleEditorInput(currentTitle);
|
|
62
|
+
wrapper.style.width = `${inputWidth}px`;
|
|
63
|
+
|
|
64
|
+
wrapper.appendChild(input);
|
|
65
|
+
view.parentElement.appendChild(wrapper);
|
|
66
|
+
|
|
67
|
+
wrapper.style.left = `${Math.round(screenPos.x)}px`;
|
|
68
|
+
wrapper.style.top = `${Math.round(screenPos.y)}px`;
|
|
69
|
+
|
|
70
|
+
this.textEditor = {
|
|
71
|
+
active: true,
|
|
72
|
+
objectId,
|
|
73
|
+
textarea: input,
|
|
74
|
+
wrapper,
|
|
75
|
+
position: null,
|
|
76
|
+
properties,
|
|
77
|
+
objectType: 'frame',
|
|
78
|
+
isResizing: false,
|
|
79
|
+
closing: false,
|
|
80
|
+
_frameInstance: frameInstance,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
input.focus();
|
|
84
|
+
input.select();
|
|
85
|
+
|
|
86
|
+
const finalize = (commit) => this._closeFrameTitleEditor(commit);
|
|
87
|
+
|
|
88
|
+
input.addEventListener('blur', () => finalize(true));
|
|
89
|
+
input.addEventListener('keydown', (e) => {
|
|
90
|
+
if (e.key === 'Enter') {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
finalize(true);
|
|
93
|
+
} else if (e.key === 'Escape') {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
finalize(false);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function closeFrameTitleEditor(commit) {
|
|
101
|
+
if (!this.textEditor || !this.textEditor.textarea || this.textEditor.closing) return;
|
|
102
|
+
|
|
103
|
+
this.textEditor.closing = true;
|
|
104
|
+
|
|
105
|
+
const input = this.textEditor.textarea;
|
|
106
|
+
const value = input.value.trim();
|
|
107
|
+
const commitValue = commit && value.length > 0;
|
|
108
|
+
const objectId = this.textEditor.objectId;
|
|
109
|
+
const oldTitle = (this.textEditor.properties?.title) || 'Фрейм';
|
|
110
|
+
const frameInstance = this.textEditor._frameInstance;
|
|
111
|
+
|
|
112
|
+
if (this.textEditor.wrapper && this.textEditor.wrapper.parentNode) {
|
|
113
|
+
this.textEditor.wrapper.remove();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (frameInstance && typeof frameInstance.showTitle === 'function') {
|
|
117
|
+
frameInstance.showTitle();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (commitValue && objectId && value !== oldTitle) {
|
|
121
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
122
|
+
objectId,
|
|
123
|
+
updates: { properties: { title: value } },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.textEditor = {
|
|
128
|
+
active: false,
|
|
129
|
+
objectId: null,
|
|
130
|
+
textarea: null,
|
|
131
|
+
wrapper: null,
|
|
132
|
+
world: null,
|
|
133
|
+
position: null,
|
|
134
|
+
properties: null,
|
|
135
|
+
objectType: 'text',
|
|
136
|
+
isResizing: false,
|
|
137
|
+
closing: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
closeMindmapEditor as closeMindmapEditorViaController,
|
|
11
11
|
openMindmapEditor as openMindmapEditorViaController,
|
|
12
12
|
} from './MindmapInlineEditorController.js';
|
|
13
|
+
import {
|
|
14
|
+
closeFrameTitleEditor as closeFrameTitleEditorViaController,
|
|
15
|
+
openFrameTitleEditor as openFrameTitleEditorViaController,
|
|
16
|
+
} from './FrameTitleInlineEditorController.js';
|
|
13
17
|
|
|
14
18
|
export function openTextEditor(object, create = false) {
|
|
15
19
|
return openTextEditorViaController.call(this, object, create);
|
|
@@ -37,3 +41,11 @@ export function closeTextEditor(commit) {
|
|
|
37
41
|
export function closeMindmapEditor(commit) {
|
|
38
42
|
return closeMindmapEditorViaController.call(this, commit);
|
|
39
43
|
}
|
|
44
|
+
|
|
45
|
+
export function openFrameTitleEditor(object, create = false) {
|
|
46
|
+
return openFrameTitleEditorViaController.call(this, object, create);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function closeFrameTitleEditor(commit) {
|
|
50
|
+
return closeFrameTitleEditorViaController.call(this, commit);
|
|
51
|
+
}
|
|
@@ -30,6 +30,42 @@ export function createFileNameEditorWrapper() {
|
|
|
30
30
|
return wrapper;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function createFrameTitleEditorWrapper() {
|
|
34
|
+
const wrapper = document.createElement('div');
|
|
35
|
+
wrapper.className = 'moodboard-frame-title-editor';
|
|
36
|
+
wrapper.style.cssText = `
|
|
37
|
+
position: absolute;
|
|
38
|
+
z-index: 1000;
|
|
39
|
+
background: white;
|
|
40
|
+
border: 2px solid #2563eb;
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
padding: 3px 6px;
|
|
43
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
44
|
+
min-width: 100px;
|
|
45
|
+
max-width: 320px;
|
|
46
|
+
font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
|
|
47
|
+
`;
|
|
48
|
+
return wrapper;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createFrameTitleEditorInput(title) {
|
|
52
|
+
const input = document.createElement('input');
|
|
53
|
+
input.type = 'text';
|
|
54
|
+
input.value = title;
|
|
55
|
+
input.style.cssText = `
|
|
56
|
+
border: none;
|
|
57
|
+
outline: none;
|
|
58
|
+
background: transparent;
|
|
59
|
+
font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
width: 100%;
|
|
63
|
+
padding: 1px 2px;
|
|
64
|
+
color: #1f2937;
|
|
65
|
+
`;
|
|
66
|
+
return input;
|
|
67
|
+
}
|
|
68
|
+
|
|
33
69
|
export function createFileNameEditorInput(fileName) {
|
|
34
70
|
const input = document.createElement('input');
|
|
35
71
|
input.type = 'text';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Проверяет, находится ли точка внутри полигона (алгоритм ray casting).
|
|
5
|
+
*/
|
|
6
|
+
function pointInPolygon(point, polygon) {
|
|
7
|
+
let inside = false;
|
|
8
|
+
const n = polygon.length;
|
|
9
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
10
|
+
const xi = polygon[i].x, yi = polygon[i].y;
|
|
11
|
+
const xj = polygon[j].x, yj = polygon[j].y;
|
|
12
|
+
if ((yi > point.y) !== (yj > point.y) &&
|
|
13
|
+
point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi) {
|
|
14
|
+
inside = !inside;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return inside;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Проверяет, пересекается ли полигон лассо с прямоугольником объекта (bounds).
|
|
22
|
+
* Достаточно, что хотя бы один угол прямоугольника — внутри полигона.
|
|
23
|
+
*/
|
|
24
|
+
function lassoIntersectsRect(polygon, rect) {
|
|
25
|
+
if (polygon.length < 3) return false;
|
|
26
|
+
const corners = [
|
|
27
|
+
{ x: rect.x, y: rect.y },
|
|
28
|
+
{ x: rect.x + rect.width, y: rect.y },
|
|
29
|
+
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
30
|
+
{ x: rect.x, y: rect.y + rect.height },
|
|
31
|
+
{ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }
|
|
32
|
+
];
|
|
33
|
+
return corners.some((c) => pointInPolygon(c, polygon));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* LassoSelectController — выделение объектов произвольным контуром.
|
|
38
|
+
* Аналог BoxSelectController, но с polygon hit-test вместо AABB.
|
|
39
|
+
*/
|
|
40
|
+
export class LassoSelectController {
|
|
41
|
+
constructor({ app, selection, emit, setSelection, clearSelection }) {
|
|
42
|
+
this.app = app;
|
|
43
|
+
this.selection = selection;
|
|
44
|
+
this.emit = emit;
|
|
45
|
+
this.setSelection = setSelection;
|
|
46
|
+
this.clearSelection = clearSelection;
|
|
47
|
+
|
|
48
|
+
this.isActive = false;
|
|
49
|
+
this.points = [];
|
|
50
|
+
this.lassoGraphics = null;
|
|
51
|
+
this.initialSelection = null;
|
|
52
|
+
this.isMultiSelect = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
start(mouse, isMultiSelect) {
|
|
56
|
+
this.isActive = true;
|
|
57
|
+
this.isMultiSelect = !!isMultiSelect;
|
|
58
|
+
this.points = [{ x: mouse.x, y: mouse.y }];
|
|
59
|
+
this.initialSelection = this.selection.toArray();
|
|
60
|
+
if (!this.isMultiSelect) this.clearSelection();
|
|
61
|
+
|
|
62
|
+
if (this.app && this.app.stage) {
|
|
63
|
+
this.app.stage.sortableChildren = true;
|
|
64
|
+
this.lassoGraphics = new PIXI.Graphics();
|
|
65
|
+
this.lassoGraphics.zIndex = 2000;
|
|
66
|
+
this.lassoGraphics.name = 'lasso-select';
|
|
67
|
+
this.app.stage.addChild(this.lassoGraphics);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
update(mouse) {
|
|
72
|
+
if (!this.isActive) return;
|
|
73
|
+
this.points.push({ x: mouse.x, y: mouse.y });
|
|
74
|
+
this._redraw();
|
|
75
|
+
this._updateSelection();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
end() {
|
|
79
|
+
if (!this.isActive) return;
|
|
80
|
+
this._updateSelection();
|
|
81
|
+
this.isActive = false;
|
|
82
|
+
this.points = [];
|
|
83
|
+
this._destroyGraphics();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_updateSelection() {
|
|
87
|
+
if (this.points.length < 3) return;
|
|
88
|
+
const request = { objects: [] };
|
|
89
|
+
this.emit('get:all:objects', request);
|
|
90
|
+
const matched = [];
|
|
91
|
+
for (const item of request.objects) {
|
|
92
|
+
if (lassoIntersectsRect(this.points, item.bounds)) matched.push(item.id);
|
|
93
|
+
}
|
|
94
|
+
let newSelection;
|
|
95
|
+
if (this.isMultiSelect && this.initialSelection) {
|
|
96
|
+
const base = new Set(this.initialSelection);
|
|
97
|
+
for (const id of matched) base.add(id);
|
|
98
|
+
newSelection = Array.from(base);
|
|
99
|
+
} else {
|
|
100
|
+
newSelection = matched;
|
|
101
|
+
}
|
|
102
|
+
this.setSelection(newSelection);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_redraw() {
|
|
106
|
+
if (!this.lassoGraphics || this.points.length < 2) return;
|
|
107
|
+
this.lassoGraphics.clear();
|
|
108
|
+
this.lassoGraphics.lineStyle(2, 0x3b82f6, 0.9);
|
|
109
|
+
this.lassoGraphics.beginFill(0x3b82f6, 0.08);
|
|
110
|
+
this.lassoGraphics.moveTo(this.points[0].x, this.points[0].y);
|
|
111
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
112
|
+
this.lassoGraphics.lineTo(this.points[i].x, this.points[i].y);
|
|
113
|
+
}
|
|
114
|
+
this.lassoGraphics.closePath();
|
|
115
|
+
this.lassoGraphics.endFill();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_destroyGraphics() {
|
|
119
|
+
if (this.lassoGraphics) {
|
|
120
|
+
if (this.lassoGraphics.parent) this.lassoGraphics.parent.removeChild(this.lassoGraphics);
|
|
121
|
+
this.lassoGraphics.destroy();
|
|
122
|
+
this.lassoGraphics = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -600,6 +600,7 @@ export function openMindmapEditor(object, create = false) {
|
|
|
600
600
|
[Events.Tool.GroupRotateUpdate, onGroupSync],
|
|
601
601
|
[Events.UI.ZoomPercent, () => syncEditorBoundsToObject()],
|
|
602
602
|
[Events.Tool.PanUpdate, () => syncEditorBoundsToObject()],
|
|
603
|
+
[Events.Viewport.Changed, () => syncEditorBoundsToObject()],
|
|
603
604
|
]);
|
|
604
605
|
|
|
605
606
|
const initialContent = String(properties.content || '');
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { Events } from '../../../core/events/Events.js';
|
|
2
2
|
|
|
3
3
|
const DRAG_START_THRESHOLD_PX = 4;
|
|
4
|
+
const TEXT_EDITOR_STYLE_KEYS = ['fontFamily', 'fontSize', 'color', 'backgroundColor', 'markdown', 'bold', 'italic', 'underline', 'strikethrough', 'textAlign', 'lineHeight', 'listType', 'listChecked'];
|
|
5
|
+
|
|
6
|
+
function pickTextEditorProperties(properties = {}) {
|
|
7
|
+
const picked = {};
|
|
8
|
+
TEXT_EDITOR_STYLE_KEYS.forEach((key) => {
|
|
9
|
+
if (properties[key] !== undefined) {
|
|
10
|
+
picked[key] = properties[key];
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return picked;
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
export function onMouseDown(event) {
|
|
6
17
|
// Если активен текстовый редактор, закрываем его при клике вне
|
|
@@ -124,8 +135,12 @@ export function onMouseDown(event) {
|
|
|
124
135
|
return;
|
|
125
136
|
}
|
|
126
137
|
}
|
|
127
|
-
// Иначе — начинаем рамку выделения
|
|
128
|
-
this.
|
|
138
|
+
// Иначе — начинаем рамку выделения (или лассо в режиме лассо)
|
|
139
|
+
if (this.lassoMode) {
|
|
140
|
+
this.startLassoSelect(event);
|
|
141
|
+
} else {
|
|
142
|
+
this.startBoxSelect(event);
|
|
143
|
+
}
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
146
|
|
|
@@ -158,6 +173,8 @@ export function onMouseMove(event) {
|
|
|
158
173
|
}
|
|
159
174
|
} else if (this.isDragging || this.isGroupDragging) {
|
|
160
175
|
this.updateDrag(event);
|
|
176
|
+
} else if (this.isLassoSelect) {
|
|
177
|
+
this.updateLassoSelect(event);
|
|
161
178
|
} else if (this.isBoxSelect) {
|
|
162
179
|
this.updateBoxSelect(event);
|
|
163
180
|
} else {
|
|
@@ -186,6 +203,8 @@ export function onMouseUp(event) {
|
|
|
186
203
|
this.endRotate();
|
|
187
204
|
} else if (this.isDragging || this.isGroupDragging) {
|
|
188
205
|
this.endDrag();
|
|
206
|
+
} else if (this.isLassoSelect) {
|
|
207
|
+
this.endLassoSelect();
|
|
189
208
|
} else if (this.isBoxSelect) {
|
|
190
209
|
this.endBoxSelect();
|
|
191
210
|
}
|
|
@@ -203,20 +222,28 @@ export function onDoubleClick(event) {
|
|
|
203
222
|
const isText = !!(pix && pix._mb && pix._mb.type === 'text');
|
|
204
223
|
const isNote = !!(pix && pix._mb && pix._mb.type === 'note');
|
|
205
224
|
const isFile = !!(pix && pix._mb && pix._mb.type === 'file');
|
|
225
|
+
const isShape = !!(pix && pix._mb && pix._mb.type === 'shape');
|
|
226
|
+
const isFrame = !!(pix && pix._mb && pix._mb.type === 'frame');
|
|
206
227
|
|
|
207
228
|
if (isText) {
|
|
208
229
|
// Получаем позицию объекта для редактирования
|
|
209
230
|
const posData = { objectId: hitResult.object, position: null };
|
|
210
231
|
this.emit(Events.Tool.GetObjectPosition, posData);
|
|
211
232
|
|
|
212
|
-
//
|
|
213
|
-
|
|
233
|
+
// Передаём в inline-editor не только контент, но и текущие текстовые стили:
|
|
234
|
+
// иначе теряются listType/textAlign/lineHeight и Enter/визуальное совпадение
|
|
235
|
+
// в режиме редактирования расходятся со статичным рендером.
|
|
236
|
+
const textProperties = pix._mb?.properties || {};
|
|
237
|
+
const textContent = textProperties.content || '';
|
|
214
238
|
|
|
215
239
|
this.emit(Events.Tool.ObjectEdit, {
|
|
216
240
|
id: hitResult.object,
|
|
217
241
|
type: 'text',
|
|
218
242
|
position: posData.position,
|
|
219
|
-
properties: {
|
|
243
|
+
properties: {
|
|
244
|
+
...pickTextEditorProperties(textProperties),
|
|
245
|
+
content: textContent,
|
|
246
|
+
},
|
|
220
247
|
caretClick: {
|
|
221
248
|
clientX: event?.originalEvent?.clientX,
|
|
222
249
|
clientY: event?.originalEvent?.clientY
|
|
@@ -256,6 +283,36 @@ export function onDoubleClick(event) {
|
|
|
256
283
|
});
|
|
257
284
|
return;
|
|
258
285
|
}
|
|
286
|
+
|
|
287
|
+
if (isShape) {
|
|
288
|
+
const posData = { objectId: hitResult.object, position: null };
|
|
289
|
+
this.emit(Events.Tool.GetObjectPosition, posData);
|
|
290
|
+
const shapeContent = pix._mb?.properties?.content || '';
|
|
291
|
+
this.emit(Events.Tool.ObjectEdit, {
|
|
292
|
+
id: hitResult.object,
|
|
293
|
+
type: 'shape',
|
|
294
|
+
position: posData.position,
|
|
295
|
+
properties: { content: shapeContent },
|
|
296
|
+
caretClick: {
|
|
297
|
+
clientX: event?.originalEvent?.clientX,
|
|
298
|
+
clientY: event?.originalEvent?.clientY,
|
|
299
|
+
},
|
|
300
|
+
create: false,
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (isFrame) {
|
|
306
|
+
const frameProps = pix._mb?.properties || {};
|
|
307
|
+
this.emit(Events.Tool.ObjectEdit, {
|
|
308
|
+
id: hitResult.object,
|
|
309
|
+
type: 'frame',
|
|
310
|
+
properties: { title: frameProps.title || 'Фрейм' },
|
|
311
|
+
create: false,
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
259
316
|
this.editObject(hitResult.object);
|
|
260
317
|
}
|
|
261
318
|
}
|
|
@@ -285,6 +342,8 @@ export function onKeyDown(event) {
|
|
|
285
342
|
if (event.key === 'Escape') {
|
|
286
343
|
if (this.textEditor.objectType === 'file') {
|
|
287
344
|
this._closeFileNameEditor(false);
|
|
345
|
+
} else if (this.textEditor.objectType === 'frame') {
|
|
346
|
+
this._closeFrameTitleEditor(false);
|
|
288
347
|
} else {
|
|
289
348
|
this._closeTextEditor(false);
|
|
290
349
|
}
|
|
@@ -8,6 +8,7 @@ import { GroupResizeController } from './GroupResizeController.js';
|
|
|
8
8
|
import { GroupRotateController } from './GroupRotateController.js';
|
|
9
9
|
import { GroupDragController } from './GroupDragController.js';
|
|
10
10
|
import { BoxSelectController } from './BoxSelectController.js';
|
|
11
|
+
import { LassoSelectController } from './LassoSelectController.js';
|
|
11
12
|
|
|
12
13
|
export function activateSelectTool(app, defaultCursor, superActivate) {
|
|
13
14
|
superActivate();
|
|
@@ -67,6 +68,13 @@ export function activateSelectTool(app, defaultCursor, superActivate) {
|
|
|
67
68
|
clearSelection: () => this.clearSelection(),
|
|
68
69
|
rectIntersectsRect: (a, b) => this.rectIntersectsRect(a, b)
|
|
69
70
|
});
|
|
71
|
+
this._lassoSelect = new LassoSelectController({
|
|
72
|
+
app,
|
|
73
|
+
selection: this.selection,
|
|
74
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
75
|
+
setSelection: (ids) => this.setSelection(ids),
|
|
76
|
+
clearSelection: () => this.clearSelection()
|
|
77
|
+
});
|
|
70
78
|
} else if (!app) {
|
|
71
79
|
console.log('❌ PIXI app не передан в activate');
|
|
72
80
|
} else {
|
|
@@ -76,10 +84,12 @@ export function activateSelectTool(app, defaultCursor, superActivate) {
|
|
|
76
84
|
export function deactivateSelectTool(superDeactivate) {
|
|
77
85
|
superDeactivate();
|
|
78
86
|
|
|
79
|
-
// Закрываем
|
|
87
|
+
// Закрываем текстовый/файловый/frame редактор если открыт
|
|
80
88
|
if (this.textEditor.active) {
|
|
81
89
|
if (this.textEditor.objectType === 'file') {
|
|
82
90
|
this._closeFileNameEditor(true);
|
|
91
|
+
} else if (this.textEditor.objectType === 'frame') {
|
|
92
|
+
this._closeFrameTitleEditor(true);
|
|
83
93
|
} else {
|
|
84
94
|
this._closeTextEditor(true);
|
|
85
95
|
}
|
|
@@ -64,6 +64,10 @@ export function initializeSelectToolState(instance) {
|
|
|
64
64
|
instance.selectionGraphics = null; // PIXI.Graphics для визуализации рамки
|
|
65
65
|
instance.initialSelectionBeforeBox = null; // снимок выделения перед началом box-select
|
|
66
66
|
|
|
67
|
+
// Режим лассо-выделения (произвольный контур вместо прямоугольника)
|
|
68
|
+
instance.lassoMode = false;
|
|
69
|
+
instance.isLassoSelect = false;
|
|
70
|
+
|
|
67
71
|
instance.textEditor = {
|
|
68
72
|
active: false,
|
|
69
73
|
objectId: null,
|
|
@@ -96,6 +100,8 @@ export function registerSelectToolCoreSubscriptions(instance) {
|
|
|
96
100
|
const objectType = object.type || (object.object && object.object.type) || 'text';
|
|
97
101
|
if (objectType === 'file') {
|
|
98
102
|
instance._openFileNameEditor(object, object.create || false);
|
|
103
|
+
} else if (objectType === 'frame') {
|
|
104
|
+
instance._openFrameTitleEditor(object, object.create || false);
|
|
99
105
|
} else if (objectType === 'mindmap') {
|
|
100
106
|
instance._openMindmapEditor(object, object.create || false);
|
|
101
107
|
} else {
|
|
@@ -121,11 +127,16 @@ export function registerSelectToolCoreSubscriptions(instance) {
|
|
|
121
127
|
}
|
|
122
128
|
};
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
const onLassoModeSet = (data) => {
|
|
131
|
+
instance.lassoMode = !!(data && data.active);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
instance._coreHandlers = { onDuplicateReady, onGroupDuplicateReady, onObjectEdit, onObjectDeleted, onLassoModeSet };
|
|
125
135
|
instance.eventBus.on(Events.Tool.DuplicateReady, onDuplicateReady);
|
|
126
136
|
instance.eventBus.on(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
|
|
127
137
|
instance.eventBus.on(Events.Tool.ObjectEdit, onObjectEdit);
|
|
128
138
|
instance.eventBus.on(Events.Object.Deleted, onObjectDeleted);
|
|
139
|
+
instance.eventBus.on(Events.Lasso.ModeSet, onLassoModeSet);
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
export function unregisterSelectToolCoreSubscriptions(instance) {
|
|
@@ -135,5 +146,6 @@ export function unregisterSelectToolCoreSubscriptions(instance) {
|
|
|
135
146
|
instance.eventBus.off(Events.Tool.GroupDuplicateReady, onGroupDuplicateReady);
|
|
136
147
|
instance.eventBus.off(Events.Tool.ObjectEdit, onObjectEdit);
|
|
137
148
|
instance.eventBus.off(Events.Object.Deleted, onObjectDeleted);
|
|
149
|
+
instance.eventBus.off(Events.Lasso.ModeSet, onLassoModeSet);
|
|
138
150
|
instance._coreHandlers = null;
|
|
139
151
|
}
|