@sequent-org/moodboard 1.2.119 → 1.3.0
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 +11 -1
- package/src/assets/icons/rotate-icon.svg +1 -1
- package/src/core/HistoryManager.js +16 -16
- package/src/core/KeyboardManager.js +48 -539
- package/src/core/PixiEngine.js +9 -9
- package/src/core/SaveManager.js +56 -31
- package/src/core/bootstrap/CoreInitializer.js +65 -0
- package/src/core/commands/DeleteObjectCommand.js +8 -0
- package/src/core/commands/GroupDeleteCommand.js +75 -0
- package/src/core/commands/GroupRotateCommand.js +6 -0
- package/src/core/commands/UpdateContentCommand.js +52 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
- package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
- package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
- package/src/core/commands/UpdateTextStyleCommand.js +90 -0
- package/src/core/commands/index.js +6 -0
- package/src/core/events/Events.js +6 -0
- package/src/core/flows/ClipboardFlow.js +553 -0
- package/src/core/flows/LayerAndViewportFlow.js +283 -0
- package/src/core/flows/ObjectLifecycleFlow.js +336 -0
- package/src/core/flows/SaveFlow.js +34 -0
- package/src/core/flows/TransformFlow.js +277 -0
- package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
- package/src/core/index.js +41 -1773
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
- package/src/core/keyboard/KeyboardContextGuards.js +35 -0
- package/src/core/keyboard/KeyboardEventRouter.js +92 -0
- package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
- package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
- package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
- package/src/core/rendering/ObjectRenderer.js +3 -7
- package/src/grid/BaseGrid.js +26 -0
- package/src/grid/CrossGrid.js +7 -6
- package/src/grid/DotGrid.js +89 -33
- package/src/grid/DotGridZoomPhases.js +42 -0
- package/src/grid/LineGrid.js +22 -21
- package/src/moodboard/MoodBoard.js +31 -532
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
- package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
- package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
- package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
- package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
- package/src/objects/FileObject.js +17 -6
- package/src/objects/FrameObject.js +50 -10
- package/src/objects/NoteObject.js +5 -4
- package/src/services/BoardService.js +42 -2
- package/src/services/FrameService.js +83 -42
- package/src/services/ResizePolicyService.js +152 -0
- package/src/services/SettingsApplier.js +7 -2
- package/src/services/ZoomPanController.js +35 -9
- package/src/tools/ToolManager.js +30 -537
- package/src/tools/board-tools/PanTool.js +5 -11
- package/src/tools/manager/ToolActivationController.js +49 -0
- package/src/tools/manager/ToolEventRouter.js +396 -0
- package/src/tools/manager/ToolManagerGuards.js +33 -0
- package/src/tools/manager/ToolManagerLifecycle.js +110 -0
- package/src/tools/manager/ToolRegistry.js +33 -0
- package/src/tools/object-tools/DrawingTool.js +48 -14
- package/src/tools/object-tools/PlacementTool.js +50 -1049
- package/src/tools/object-tools/PlacementToolV2.js +88 -0
- package/src/tools/object-tools/SelectTool.js +174 -2681
- package/src/tools/object-tools/placement/GhostController.js +504 -0
- package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
- package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
- package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
- package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
- package/src/tools/object-tools/selection/CursorController.js +78 -0
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
- package/src/tools/object-tools/selection/HitTestService.js +102 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
- package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
- package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
- package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
- package/src/ui/FilePropertiesPanel.js +61 -32
- package/src/ui/FramePropertiesPanel.js +176 -101
- package/src/ui/HtmlHandlesLayer.js +121 -999
- package/src/ui/MapPanel.js +12 -7
- package/src/ui/NotePropertiesPanel.js +17 -2
- package/src/ui/TextPropertiesPanel.js +124 -738
- package/src/ui/Toolbar.js +71 -1180
- package/src/ui/Topbar.js +23 -25
- package/src/ui/ZoomPanel.js +16 -5
- package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
- package/src/ui/handles/HandlesDomRenderer.js +278 -0
- package/src/ui/handles/HandlesEventBridge.js +102 -0
- package/src/ui/handles/HandlesInteractionController.js +772 -0
- package/src/ui/handles/HandlesPositioningService.js +206 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
- package/src/ui/styles/toolbar.css +2 -0
- package/src/ui/styles/workspace.css +13 -6
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
- package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
- package/src/ui/toolbar/ToolbarRenderer.js +97 -0
- package/src/ui/toolbar/ToolbarStateController.js +79 -0
- package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
- package/src/utils/emojiLoaderNoBundler.js +1 -1
|
@@ -17,7 +17,12 @@ const _scaledICursorSvg = (() => {
|
|
|
17
17
|
|
|
18
18
|
const TEXT_CURSOR = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(_scaledICursorSvg)}") 0 0, text`;
|
|
19
19
|
import { Events } from '../../core/events/Events.js';
|
|
20
|
-
import
|
|
20
|
+
import { GhostController } from './placement/GhostController.js';
|
|
21
|
+
import { PlacementPayloadFactory } from './placement/PlacementPayloadFactory.js';
|
|
22
|
+
import { PlacementInputRouter } from './placement/PlacementInputRouter.js';
|
|
23
|
+
import { PlacementEventsBridge } from './placement/PlacementEventsBridge.js';
|
|
24
|
+
import { PlacementSessionStore } from './placement/PlacementSessionStore.js';
|
|
25
|
+
import { PlacementCoordinateResolver } from './placement/PlacementCoordinateResolver.js';
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Инструмент одноразового размещения объекта по клику на холст
|
|
@@ -30,116 +35,20 @@ export class PlacementTool extends BaseTool {
|
|
|
30
35
|
this.hotkey = null;
|
|
31
36
|
this.app = null;
|
|
32
37
|
this.world = null;
|
|
33
|
-
this.pending = null; // { type, properties }
|
|
34
38
|
this.core = core;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
39
|
+
this.ghostController = new GhostController(this);
|
|
40
|
+
this.payloadFactory = new PlacementPayloadFactory(this);
|
|
41
|
+
this.inputRouter = new PlacementInputRouter(this);
|
|
42
|
+
this.eventsBridge = new PlacementEventsBridge(this);
|
|
43
|
+
this.sessionStore = new PlacementSessionStore(this);
|
|
44
|
+
this.coordinateResolver = new PlacementCoordinateResolver(this);
|
|
45
|
+
this.sessionStore.initialize();
|
|
41
46
|
// Оригинальные стили курсора PIXI, чтобы можно было временно переопределить pointer/default для текстового инструмента
|
|
42
47
|
this._origCursorStyles = null;
|
|
48
|
+
// Сохраняем bound handler для корректного removeEventListener (избежание утечки памяти)
|
|
49
|
+
this._boundOnMouseMove = null;
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
this.eventBus.on(Events.Place.Set, (cfg) => {
|
|
46
|
-
this.pending = cfg ? { ...cfg } : null;
|
|
47
|
-
// Обновляем курсор в зависимости от pending
|
|
48
|
-
if (this.app && this.app.view) {
|
|
49
|
-
const cur = this._getPendingCursor();
|
|
50
|
-
this.cursor = cur;
|
|
51
|
-
this.app.view.style.cursor = (cur === 'default') ? '' : cur;
|
|
52
|
-
}
|
|
53
|
-
// При выборе текста заставляем pointer вести себя как текстовый курсор
|
|
54
|
-
this._updateCursorOverride();
|
|
55
|
-
|
|
56
|
-
// Показываем призрак для записки, эмоджи, фрейма или фигур, если они активны
|
|
57
|
-
if (this.pending && this.app && this.world) {
|
|
58
|
-
if (this.pending.type === 'note') {
|
|
59
|
-
this.showNoteGhost();
|
|
60
|
-
} else if (this.pending.type === 'emoji') {
|
|
61
|
-
this.showEmojiGhost();
|
|
62
|
-
} else if (this.pending.type === 'image') {
|
|
63
|
-
this.showImageUrlGhost();
|
|
64
|
-
} else if (this.pending.type === 'frame') {
|
|
65
|
-
this.showFrameGhost();
|
|
66
|
-
} else if (this.pending.type === 'frame-draw') {
|
|
67
|
-
this.startFrameDrawMode();
|
|
68
|
-
} else if (this.pending.type === 'shape') {
|
|
69
|
-
this.showShapeGhost();
|
|
70
|
-
}
|
|
71
|
-
// Поддержка сценария перетаскивания из панели: отпускание без предварительного mousedown на канвасе
|
|
72
|
-
if (this.pending.placeOnMouseUp && this.app && this.app.view) {
|
|
73
|
-
const onUp = (ev) => {
|
|
74
|
-
this.app.view.removeEventListener('mouseup', onUp);
|
|
75
|
-
if (!this.pending) return;
|
|
76
|
-
const worldPoint = this._toWorld(ev.x, ev.y);
|
|
77
|
-
const position = {
|
|
78
|
-
x: Math.round(worldPoint.x - (this.pending.size?.width ?? 100) / 2),
|
|
79
|
-
y: Math.round(worldPoint.y - (this.pending.size?.height ?? 100) / 2)
|
|
80
|
-
};
|
|
81
|
-
const props = { ...(this.pending.properties || {}) };
|
|
82
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
83
|
-
type: this.pending.type,
|
|
84
|
-
id: this.pending.type,
|
|
85
|
-
position,
|
|
86
|
-
properties: props
|
|
87
|
-
});
|
|
88
|
-
this.pending = null;
|
|
89
|
-
this.hideGhost();
|
|
90
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
91
|
-
};
|
|
92
|
-
this.app.view.addEventListener('mouseup', onUp, { once: true });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Сброс pending при явном выборе select-инструмента
|
|
98
|
-
this.eventBus.on(Events.Tool.Activated, ({ tool }) => {
|
|
99
|
-
if (tool === 'select') {
|
|
100
|
-
this.pending = null;
|
|
101
|
-
this.selectedFile = null;
|
|
102
|
-
this.selectedImage = null;
|
|
103
|
-
this.hideGhost();
|
|
104
|
-
// Возвращаем стандартное поведение курсора, когда уходим с PlacementTool
|
|
105
|
-
this._updateCursorOverride();
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Обработка выбора файла
|
|
110
|
-
this.eventBus.on(Events.Place.FileSelected, (fileData) => {
|
|
111
|
-
this.selectedFile = fileData;
|
|
112
|
-
this.selectedImage = null;
|
|
113
|
-
|
|
114
|
-
// Если PlacementTool уже активен - показываем призрак сразу
|
|
115
|
-
if (this.world) {
|
|
116
|
-
this.showFileGhost();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Обработка отмены выбора файла
|
|
121
|
-
this.eventBus.on(Events.Place.FileCanceled, () => {
|
|
122
|
-
this.selectedFile = null;
|
|
123
|
-
this.hideGhost();
|
|
124
|
-
// Возвращаемся к инструменту выделения
|
|
125
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Обработка выбора изображения
|
|
129
|
-
this.eventBus.on(Events.Place.ImageSelected, (imageData) => {
|
|
130
|
-
this.selectedImage = imageData;
|
|
131
|
-
this.selectedFile = null;
|
|
132
|
-
this.showImageGhost();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Обработка отмены выбора изображения
|
|
136
|
-
this.eventBus.on(Events.Place.ImageCanceled, () => {
|
|
137
|
-
this.selectedImage = null;
|
|
138
|
-
this.hideGhost();
|
|
139
|
-
// Возвращаемся к инструменту выделения
|
|
140
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
141
|
-
});
|
|
142
|
-
}
|
|
51
|
+
this.eventsBridge.attach();
|
|
143
52
|
}
|
|
144
53
|
|
|
145
54
|
activate(app) {
|
|
@@ -150,8 +59,8 @@ export class PlacementTool extends BaseTool {
|
|
|
150
59
|
if (this.app && this.app.view) {
|
|
151
60
|
this.cursor = this._getPendingCursor();
|
|
152
61
|
this.app.view.style.cursor = this.cursor;
|
|
153
|
-
|
|
154
|
-
this.app.view.addEventListener('mousemove', this.
|
|
62
|
+
this._boundOnMouseMove = this._boundOnMouseMove || this._onMouseMove.bind(this);
|
|
63
|
+
this.app.view.addEventListener('mousemove', this._boundOnMouseMove);
|
|
155
64
|
}
|
|
156
65
|
// При активации синхронизируем переопределение курсора pointer для текста
|
|
157
66
|
this._updateCursorOverride();
|
|
@@ -176,10 +85,9 @@ export class PlacementTool extends BaseTool {
|
|
|
176
85
|
|
|
177
86
|
deactivate() {
|
|
178
87
|
super.deactivate();
|
|
179
|
-
if (this.app && this.app.view) {
|
|
88
|
+
if (this.app && this.app.view && this._boundOnMouseMove) {
|
|
180
89
|
this.app.view.style.cursor = '';
|
|
181
|
-
|
|
182
|
-
this.app.view.removeEventListener('mousemove', this._onMouseMove.bind(this));
|
|
90
|
+
this.app.view.removeEventListener('mousemove', this._boundOnMouseMove);
|
|
183
91
|
}
|
|
184
92
|
// Восстанавливаем стандартные стили курсора при выходе из инструмента
|
|
185
93
|
this._updateCursorOverride(true);
|
|
@@ -189,392 +97,38 @@ export class PlacementTool extends BaseTool {
|
|
|
189
97
|
}
|
|
190
98
|
|
|
191
99
|
onMouseDown(event) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Если есть выбранный файл, размещаем его
|
|
195
|
-
if (this.selectedFile) {
|
|
196
|
-
this.placeSelectedFile(event);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Если есть выбранное изображение, размещаем его
|
|
201
|
-
if (this.selectedImage) {
|
|
202
|
-
this.placeSelectedImage(event);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (!this.pending) return;
|
|
207
|
-
// Если включен режим "перетянуть и отпустить" из панели (placeOnMouseUp),
|
|
208
|
-
// то размещение выполняем на mouseup, а здесь только показываем призрак и запоминаем старт
|
|
209
|
-
if (this.pending.placeOnMouseUp) {
|
|
210
|
-
const onUp = (ev) => {
|
|
211
|
-
this.app.view.removeEventListener('mouseup', onUp);
|
|
212
|
-
// Имитация обычного place по текущему положению курсора
|
|
213
|
-
const worldPoint = this._toWorld(ev.x, ev.y);
|
|
214
|
-
const position = {
|
|
215
|
-
x: Math.round(worldPoint.x - (this.pending.size?.width ?? 100) / 2),
|
|
216
|
-
y: Math.round(worldPoint.y - (this.pending.size?.height ?? 100) / 2)
|
|
217
|
-
};
|
|
218
|
-
const props = { ...(this.pending.properties || {}) };
|
|
219
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
220
|
-
type: this.pending.type,
|
|
221
|
-
id: this.pending.type,
|
|
222
|
-
position,
|
|
223
|
-
properties: props
|
|
224
|
-
});
|
|
225
|
-
this.pending = null;
|
|
226
|
-
this.hideGhost();
|
|
227
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
228
|
-
};
|
|
229
|
-
this.app.view.addEventListener('mouseup', onUp, { once: true });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
// Если включен режим рисования фрейма — инициируем рамку
|
|
233
|
-
if (this.pending.type === 'frame-draw') {
|
|
234
|
-
const start = this._toWorld(event.x, event.y);
|
|
235
|
-
this._frameDrawState = { startX: start.x, startY: start.y, graphics: null };
|
|
236
|
-
if (this.world) {
|
|
237
|
-
const g = new PIXI.Graphics();
|
|
238
|
-
g.zIndex = 3000;
|
|
239
|
-
this.world.addChild(g);
|
|
240
|
-
this._frameDrawState.graphics = g;
|
|
241
|
-
}
|
|
242
|
-
// Вешаем временные обработчики движения/отпускания
|
|
243
|
-
this._onFrameDrawMoveBound = (ev) => this._onFrameDrawMove(ev);
|
|
244
|
-
this._onFrameDrawUpBound = (ev) => this._onFrameDrawUp(ev);
|
|
245
|
-
this.app.view.addEventListener('mousemove', this._onFrameDrawMoveBound);
|
|
246
|
-
this.app.view.addEventListener('mouseup', this._onFrameDrawUpBound, { once: true });
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const worldPoint = this._toWorld(event.x, event.y);
|
|
251
|
-
// Базовая позиция (может быть переопределена для конкретных типов)
|
|
252
|
-
let position = {
|
|
253
|
-
x: Math.round(worldPoint.x - (this.pending.size?.width ?? 100) / 2),
|
|
254
|
-
y: Math.round(worldPoint.y - (this.pending.size?.height ?? 100) / 2)
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
let props = this.pending.properties || {};
|
|
258
|
-
const isTextWithEditing = this.pending.type === 'text' && props.editOnCreate;
|
|
259
|
-
const isImage = this.pending.type === 'image';
|
|
260
|
-
const isFile = this.pending.type === 'file';
|
|
261
|
-
const presetSize = {
|
|
262
|
-
width: (this.pending.size && this.pending.size.width) ? this.pending.size.width : (props.width || 200),
|
|
263
|
-
height: (this.pending.size && this.pending.size.height) ? this.pending.size.height : (props.height || 150),
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
if (isTextWithEditing) {
|
|
267
|
-
// Для текста позиция должна совпадать с точкой клика без смещений.
|
|
268
|
-
// Используем ту же систему координат, что HtmlTextLayer/HtmlHandlesLayer:
|
|
269
|
-
// CSS ←→ world через toGlobal/toLocal БЕЗ дополнительных поправок на resolution.
|
|
270
|
-
let worldForText = worldPoint;
|
|
271
|
-
try {
|
|
272
|
-
const app = this.app;
|
|
273
|
-
const view = app?.view;
|
|
274
|
-
const worldLayer = this.world || this._getWorldLayer();
|
|
275
|
-
if (view && view.parentElement && worldLayer && worldLayer.toLocal) {
|
|
276
|
-
const containerRect = view.parentElement.getBoundingClientRect();
|
|
277
|
-
const viewRect = view.getBoundingClientRect();
|
|
278
|
-
const offsetLeft = viewRect.left - containerRect.left;
|
|
279
|
-
const offsetTop = viewRect.top - containerRect.top;
|
|
280
|
-
// event.x / event.y заданы в координатах контейнера ToolManager,
|
|
281
|
-
// поэтому приводим их к экранным координатам относительно view
|
|
282
|
-
const screenX = event.x - offsetLeft;
|
|
283
|
-
const screenY = event.y - offsetTop;
|
|
284
|
-
const globalPoint = new PIXI.Point(screenX, screenY);
|
|
285
|
-
const local = worldLayer.toLocal(globalPoint);
|
|
286
|
-
worldForText = { x: local.x, y: local.y };
|
|
287
|
-
}
|
|
288
|
-
console.log('🧭 Text click', {
|
|
289
|
-
cursor: { x: event.x, y: event.y },
|
|
290
|
-
world: { x: Math.round(worldForText.x), y: Math.round(worldForText.y) }
|
|
291
|
-
});
|
|
292
|
-
} catch (_) {}
|
|
293
|
-
position = {
|
|
294
|
-
x: Math.round(worldForText.x),
|
|
295
|
-
y: Math.round(worldForText.y)
|
|
296
|
-
};
|
|
297
|
-
// Слушаем событие создания объекта, чтобы получить его ID
|
|
298
|
-
const handleObjectCreated = (objectData) => {
|
|
299
|
-
if (objectData.type === 'text') {
|
|
300
|
-
// Убираем слушатель, чтобы не реагировать на другие объекты
|
|
301
|
-
this.eventBus.off('object:created', handleObjectCreated);
|
|
302
|
-
|
|
303
|
-
// Переключаемся на select
|
|
304
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
// Даем небольшую задержку, чтобы HTML-элемент успел создаться
|
|
309
|
-
setTimeout(() => {
|
|
310
|
-
// Открываем редактор с правильным ID и данными объекта
|
|
311
|
-
this.eventBus.emit(Events.Tool.ObjectEdit, {
|
|
312
|
-
object: {
|
|
313
|
-
id: objectData.id,
|
|
314
|
-
type: 'text',
|
|
315
|
-
position: objectData.position,
|
|
316
|
-
properties: { fontSize: props.fontSize || 18, content: '' }
|
|
317
|
-
},
|
|
318
|
-
create: true // Это создание нового объекта с редактированием
|
|
319
|
-
});
|
|
320
|
-
}, 50); // 50ms задержка
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
// Подписываемся на событие создания объекта
|
|
325
|
-
this.eventBus.on('object:created', handleObjectCreated);
|
|
326
|
-
|
|
327
|
-
// Создаем объект через обычный канал
|
|
328
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
329
|
-
type: 'text',
|
|
330
|
-
id: 'text',
|
|
331
|
-
position,
|
|
332
|
-
properties: {
|
|
333
|
-
fontSize: props.fontSize || 18,
|
|
334
|
-
content: '',
|
|
335
|
-
fontFamily: 'Arial, sans-serif', // Дефолтный шрифт
|
|
336
|
-
color: '#000000', // Дефолтный цвет (черный)
|
|
337
|
-
backgroundColor: 'transparent' // Дефолтный фон (прозрачный)
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
} else if (this.pending.type === 'frame') {
|
|
341
|
-
// Для фрейма центр привязываем к курсору так же, как у призрака
|
|
342
|
-
const width = props.width || presetSize.width || 200;
|
|
343
|
-
const height = props.height || presetSize.height || 300;
|
|
344
|
-
position = {
|
|
345
|
-
x: Math.round(worldPoint.x - width / 2),
|
|
346
|
-
y: Math.round(worldPoint.y - height / 2)
|
|
347
|
-
};
|
|
348
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
349
|
-
type: 'frame',
|
|
350
|
-
id: 'frame',
|
|
351
|
-
position,
|
|
352
|
-
properties: { ...props, width, height }
|
|
353
|
-
});
|
|
354
|
-
} else if (isImage && props.selectFileOnPlace) {
|
|
355
|
-
const input = document.createElement('input');
|
|
356
|
-
input.type = 'file';
|
|
357
|
-
input.accept = 'image/*';
|
|
358
|
-
input.style.display = 'none';
|
|
359
|
-
document.body.appendChild(input);
|
|
360
|
-
input.addEventListener('change', async () => {
|
|
361
|
-
try {
|
|
362
|
-
const file = input.files && input.files[0];
|
|
363
|
-
if (!file) return;
|
|
364
|
-
// Читаем как DataURL, чтобы не использовать blob: URL (устраняем ERR_FILE_NOT_FOUND)
|
|
365
|
-
// Загружаем файл на сервер
|
|
366
|
-
try {
|
|
367
|
-
const uploadResult = await this.core.imageUploadService.uploadImage(file, file.name);
|
|
368
|
-
|
|
369
|
-
// Вычисляем целевой размер
|
|
370
|
-
const natW = uploadResult.width || 1;
|
|
371
|
-
const natH = uploadResult.height || 1;
|
|
372
|
-
const targetW = 300; // дефолтная ширина
|
|
373
|
-
const targetH = Math.max(1, Math.round(natH * (targetW / natW)));
|
|
374
|
-
|
|
375
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
376
|
-
type: 'image',
|
|
377
|
-
id: 'image',
|
|
378
|
-
position,
|
|
379
|
-
properties: {
|
|
380
|
-
src: uploadResult.url,
|
|
381
|
-
name: uploadResult.name,
|
|
382
|
-
width: targetW,
|
|
383
|
-
height: targetH
|
|
384
|
-
},
|
|
385
|
-
imageId: uploadResult.imageId || uploadResult.id // Сохраняем ID изображения
|
|
386
|
-
});
|
|
387
|
-
} catch (error) {
|
|
388
|
-
console.error('Ошибка загрузки изображения:', error);
|
|
389
|
-
alert('Ошибка загрузки изображения: ' + error.message);
|
|
390
|
-
}
|
|
391
|
-
} finally {
|
|
392
|
-
input.remove();
|
|
393
|
-
}
|
|
394
|
-
}, { once: true });
|
|
395
|
-
input.click();
|
|
396
|
-
} else if (isFile && props.selectFileOnPlace) {
|
|
397
|
-
// Создаем диалог выбора файла
|
|
398
|
-
const input = document.createElement('input');
|
|
399
|
-
input.type = 'file';
|
|
400
|
-
input.accept = '*/*'; // Принимаем любые файлы
|
|
401
|
-
input.style.display = 'none';
|
|
402
|
-
document.body.appendChild(input);
|
|
403
|
-
input.addEventListener('change', async () => {
|
|
404
|
-
try {
|
|
405
|
-
const file = input.files && input.files[0];
|
|
406
|
-
if (!file) return;
|
|
407
|
-
|
|
408
|
-
// Загружаем файл на сервер
|
|
409
|
-
try {
|
|
410
|
-
const uploadResult = await this.core.fileUploadService.uploadFile(file, file.name);
|
|
411
|
-
|
|
412
|
-
// Создаем объект файла с данными с сервера
|
|
413
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
414
|
-
type: 'file',
|
|
415
|
-
id: 'file',
|
|
416
|
-
position,
|
|
417
|
-
properties: {
|
|
418
|
-
fileName: uploadResult.name,
|
|
419
|
-
fileSize: uploadResult.size,
|
|
420
|
-
mimeType: uploadResult.mimeType,
|
|
421
|
-
formattedSize: uploadResult.formattedSize,
|
|
422
|
-
url: uploadResult.url,
|
|
423
|
-
width: props.width || 120,
|
|
424
|
-
height: props.height || 140
|
|
425
|
-
},
|
|
426
|
-
fileId: uploadResult.fileId || uploadResult.id // Сохраняем ID файла
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// Возвращаемся к инструменту выделения после создания файла
|
|
430
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
431
|
-
} catch (uploadError) {
|
|
432
|
-
console.error('Ошибка загрузки файла на сервер:', uploadError);
|
|
433
|
-
// Fallback: создаем объект файла с локальными данными
|
|
434
|
-
const fileName = file.name;
|
|
435
|
-
const fileSize = file.size;
|
|
436
|
-
const mimeType = file.type;
|
|
437
|
-
|
|
438
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
439
|
-
type: 'file',
|
|
440
|
-
id: 'file',
|
|
441
|
-
position,
|
|
442
|
-
properties: {
|
|
443
|
-
fileName: fileName,
|
|
444
|
-
fileSize: fileSize,
|
|
445
|
-
mimeType: mimeType,
|
|
446
|
-
width: props.width || 120,
|
|
447
|
-
height: props.height || 140
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// Возвращаемся к инструменту выделения
|
|
452
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
453
|
-
|
|
454
|
-
// Показываем предупреждение пользователю
|
|
455
|
-
alert('Ошибка загрузки файла на сервер. Файл добавлен локально.');
|
|
456
|
-
}
|
|
457
|
-
} catch (error) {
|
|
458
|
-
console.error('Ошибка при выборе файла:', error);
|
|
459
|
-
alert('Ошибка при выборе файла: ' + error.message);
|
|
460
|
-
} finally {
|
|
461
|
-
input.remove();
|
|
462
|
-
}
|
|
463
|
-
}, { once: true });
|
|
464
|
-
input.click();
|
|
465
|
-
} else {
|
|
466
|
-
// Для записки: выставляем фактические габариты и центрируем по курсору
|
|
467
|
-
if (this.pending.type === 'note') {
|
|
468
|
-
const base = 250; // квадрат 250x250
|
|
469
|
-
const noteW = (typeof props.width === 'number') ? props.width : base;
|
|
470
|
-
const noteH = (typeof props.height === 'number') ? props.height : base;
|
|
471
|
-
const side = Math.max(noteW, noteH);
|
|
472
|
-
props = { ...props, width: side, height: side };
|
|
473
|
-
position = {
|
|
474
|
-
x: Math.round(worldPoint.x - side / 2),
|
|
475
|
-
y: Math.round(worldPoint.y - side / 2)
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
// Обычное размещение через общий канал
|
|
479
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
480
|
-
type: this.pending.type,
|
|
481
|
-
id: this.pending.type,
|
|
482
|
-
position,
|
|
483
|
-
properties: props
|
|
484
|
-
});
|
|
485
|
-
}
|
|
100
|
+
return this.inputRouter.onMouseDown(event);
|
|
101
|
+
}
|
|
486
102
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
this.hideGhost(); // Скрываем призрак после размещения
|
|
490
|
-
if (!isTextWithEditing && !(isFile && props.selectFileOnPlace)) {
|
|
491
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
492
|
-
}
|
|
103
|
+
__baseOnMouseDown(event) {
|
|
104
|
+
super.onMouseDown(event);
|
|
493
105
|
}
|
|
494
106
|
|
|
495
107
|
startFrameDrawMode() {
|
|
496
|
-
|
|
497
|
-
this.cursor = 'crosshair';
|
|
498
|
-
if (this.app && this.app.view) this.app.view.style.cursor = this.cursor;
|
|
108
|
+
return this.inputRouter.startFrameDrawMode();
|
|
499
109
|
}
|
|
500
110
|
|
|
501
111
|
_onFrameDrawMove(event) {
|
|
502
|
-
|
|
503
|
-
const p = this._toWorld(event.offsetX, event.offsetY);
|
|
504
|
-
const x = Math.min(this._frameDrawState.startX, p.x);
|
|
505
|
-
const y = Math.min(this._frameDrawState.startY, p.y);
|
|
506
|
-
const w = Math.abs(p.x - this._frameDrawState.startX);
|
|
507
|
-
const h = Math.abs(p.y - this._frameDrawState.startY);
|
|
508
|
-
const g = this._frameDrawState.graphics;
|
|
509
|
-
g.clear();
|
|
510
|
-
// Снапим к полупикселю и используем внутреннее выравнивание линии для чётких 1px краёв
|
|
511
|
-
const x0 = Math.floor(x) + 0.5;
|
|
512
|
-
const y0 = Math.floor(y) + 0.5;
|
|
513
|
-
const w0 = Math.max(1, Math.round(w));
|
|
514
|
-
const h0 = Math.max(1, Math.round(h));
|
|
515
|
-
g.lineStyle(1, 0x3B82F6, 1, 1 /* alignment: inner */);
|
|
516
|
-
g.beginFill(0xFFFFFF, 0.6);
|
|
517
|
-
g.drawRect(x0, y0, w0, h0);
|
|
518
|
-
g.endFill();
|
|
112
|
+
return this.inputRouter.onFrameDrawMove(event);
|
|
519
113
|
}
|
|
520
114
|
|
|
521
115
|
_onFrameDrawUp(event) {
|
|
522
|
-
|
|
523
|
-
if (!this._frameDrawState || !g) return;
|
|
524
|
-
const p = this._toWorld(event.offsetX, event.offsetY);
|
|
525
|
-
const x = Math.min(this._frameDrawState.startX, p.x);
|
|
526
|
-
const y = Math.min(this._frameDrawState.startY, p.y);
|
|
527
|
-
const w = Math.abs(p.x - this._frameDrawState.startX);
|
|
528
|
-
const h = Math.abs(p.y - this._frameDrawState.startY);
|
|
529
|
-
// Удаляем временную графику
|
|
530
|
-
if (g.parent) g.parent.removeChild(g);
|
|
531
|
-
g.destroy();
|
|
532
|
-
this._frameDrawState = null;
|
|
533
|
-
// Создаем фрейм, если размер достаточный
|
|
534
|
-
if (w >= 2 && h >= 2) {
|
|
535
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
536
|
-
type: 'frame',
|
|
537
|
-
id: 'frame',
|
|
538
|
-
position: { x, y },
|
|
539
|
-
properties: { width: Math.round(w), height: Math.round(h), title: 'Произвольный', lockedAspect: false, isArbitrary: true }
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
// Сбрасываем pending и выходим из режима place → select
|
|
543
|
-
this.pending = null;
|
|
544
|
-
this.hideGhost();
|
|
545
|
-
if (this.app && this.app.view) {
|
|
546
|
-
this.app.view.removeEventListener('mousemove', this._onFrameDrawMoveBound);
|
|
547
|
-
this.app.view.style.cursor = '';
|
|
548
|
-
}
|
|
549
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
116
|
+
return this.inputRouter.onFrameDrawUp(event);
|
|
550
117
|
}
|
|
551
118
|
|
|
552
119
|
_toWorld(x, y) {
|
|
553
|
-
|
|
554
|
-
const global = new PIXI.Point(x, y);
|
|
555
|
-
const local = this.world.toLocal(global);
|
|
556
|
-
return { x: local.x, y: local.y };
|
|
120
|
+
return this.coordinateResolver.toWorld(x, y);
|
|
557
121
|
}
|
|
558
122
|
|
|
559
123
|
_getWorldLayer() {
|
|
560
|
-
|
|
561
|
-
const world = this.app.stage.getChildByName && this.app.stage.getChildByName('worldLayer');
|
|
562
|
-
return world || this.app.stage;
|
|
124
|
+
return this.coordinateResolver.getWorldLayer();
|
|
563
125
|
}
|
|
564
126
|
|
|
565
127
|
/**
|
|
566
128
|
* Обработчик движения мыши для обновления позиции "призрака"
|
|
567
129
|
*/
|
|
568
130
|
_onMouseMove(event) {
|
|
569
|
-
|
|
570
|
-
// Сохраним последние координаты мыши (в экранных координатах) — пригодится для первичной позиции призрака
|
|
571
|
-
if (this.app && this.app.view) {
|
|
572
|
-
this.app.view._lastMouseX = event.x;
|
|
573
|
-
this.app.view._lastMouseY = event.y;
|
|
574
|
-
}
|
|
575
|
-
const worldPoint = this._toWorld(event.offsetX, event.offsetY);
|
|
576
|
-
this.updateGhostPosition(worldPoint.x, worldPoint.y);
|
|
577
|
-
}
|
|
131
|
+
return this.inputRouter.onMouseMove(event);
|
|
578
132
|
}
|
|
579
133
|
|
|
580
134
|
/**
|
|
@@ -623,106 +177,21 @@ export class PlacementTool extends BaseTool {
|
|
|
623
177
|
* Показать "призрак" файла
|
|
624
178
|
*/
|
|
625
179
|
showFileGhost() {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
this.hideGhost(); // Сначала убираем старый призрак
|
|
629
|
-
|
|
630
|
-
// Создаем контейнер для призрака
|
|
631
|
-
this.ghostContainer = new PIXI.Container();
|
|
632
|
-
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
633
|
-
// Сразу ставим контейнер в позицию курсора, чтобы он не мигал в левом верхнем углу
|
|
634
|
-
if (this.app && this.app.view) {
|
|
635
|
-
const rect = this.app.view.getBoundingClientRect();
|
|
636
|
-
const cursorX = (typeof this.app.view._lastMouseX === 'number') ? this.app.view._lastMouseX : (rect.left + rect.width / 2);
|
|
637
|
-
const cursorY = (typeof this.app.view._lastMouseY === 'number') ? this.app.view._lastMouseY : (rect.top + rect.height / 2);
|
|
638
|
-
const worldPoint = this._toWorld(cursorX, cursorY);
|
|
639
|
-
this.updateGhostPosition(worldPoint.x, worldPoint.y);
|
|
640
|
-
}
|
|
641
|
-
// Попробуем дождаться загрузки веб-шрифта Caveat до отрисовки
|
|
642
|
-
// Для файлов используем selectedFile, а не pending
|
|
643
|
-
const fileFont = (this.selectedFile.properties?.fontFamily) || 'Caveat, Arial, cursive';
|
|
644
|
-
const primaryFont = String(fileFont).split(',')[0].trim().replace(/^['"]|['"]$/g, '') || 'Caveat';
|
|
645
|
-
|
|
646
|
-
// Размеры
|
|
647
|
-
const width = this.selectedFile.properties.width || 120;
|
|
648
|
-
const height = this.selectedFile.properties.height || 140;
|
|
649
|
-
|
|
650
|
-
// Размытая тень (как у FileObject)
|
|
651
|
-
const shadow = new PIXI.Graphics();
|
|
652
|
-
try {
|
|
653
|
-
shadow.filters = [new PIXI.filters.BlurFilter(6)];
|
|
654
|
-
} catch (e) {}
|
|
655
|
-
shadow.beginFill(0x000000, 1);
|
|
656
|
-
shadow.drawRect(0, 0, width, height);
|
|
657
|
-
shadow.endFill();
|
|
658
|
-
shadow.x = 2;
|
|
659
|
-
shadow.y = 3;
|
|
660
|
-
shadow.alpha = 0.18;
|
|
661
|
-
|
|
662
|
-
// Белый прямоугольник без рамки
|
|
663
|
-
const background = new PIXI.Graphics();
|
|
664
|
-
background.beginFill(0xFFFFFF, 1);
|
|
665
|
-
background.drawRect(0, 0, width, height);
|
|
666
|
-
background.endFill();
|
|
667
|
-
|
|
668
|
-
// Иконка-заглушка файла наверху (центрируем фактическую ширину)
|
|
669
|
-
const icon = new PIXI.Graphics();
|
|
670
|
-
const iconSize = Math.min(48, width * 0.4);
|
|
671
|
-
const iconWidthDrawn = iconSize * 0.8;
|
|
672
|
-
const iconX = (width - iconWidthDrawn) / 2;
|
|
673
|
-
const iconY = 16;
|
|
674
|
-
icon.beginFill(0x6B7280, 1);
|
|
675
|
-
icon.drawRect(iconX, iconY, iconWidthDrawn, iconSize);
|
|
676
|
-
icon.endFill();
|
|
677
|
-
|
|
678
|
-
// Текст названия файла
|
|
679
|
-
const fileName = this.selectedFile.fileName || 'File';
|
|
680
|
-
const displayName = fileName;
|
|
681
|
-
const nameText = new PIXI.Text(displayName, {
|
|
682
|
-
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
|
|
683
|
-
fontSize: 12,
|
|
684
|
-
fill: 0x333333,
|
|
685
|
-
align: 'center',
|
|
686
|
-
wordWrap: true,
|
|
687
|
-
breakWords: true,
|
|
688
|
-
wordWrapWidth: Math.max(1, width - 24) // padding 12px по бокам
|
|
689
|
-
});
|
|
690
|
-
nameText.anchor.set(0.5, 0);
|
|
691
|
-
nameText.x = width / 2;
|
|
692
|
-
nameText.y = iconY + iconSize + 8;
|
|
693
|
-
|
|
694
|
-
// Добавляем в контейнер в правильном порядке
|
|
695
|
-
this.ghostContainer.addChild(shadow);
|
|
696
|
-
this.ghostContainer.addChild(background);
|
|
697
|
-
this.ghostContainer.addChild(icon);
|
|
698
|
-
this.ghostContainer.addChild(nameText);
|
|
699
|
-
|
|
700
|
-
// Центрируем контейнер относительно курсора
|
|
701
|
-
this.ghostContainer.pivot.x = width / 2;
|
|
702
|
-
this.ghostContainer.pivot.y = height / 2;
|
|
703
|
-
|
|
704
|
-
this.world.addChild(this.ghostContainer);
|
|
180
|
+
return this.ghostController.showFileGhost();
|
|
705
181
|
}
|
|
706
182
|
|
|
707
183
|
/**
|
|
708
184
|
* Скрыть "призрак" файла
|
|
709
185
|
*/
|
|
710
186
|
hideGhost() {
|
|
711
|
-
|
|
712
|
-
this.world.removeChild(this.ghostContainer);
|
|
713
|
-
this.ghostContainer.destroy();
|
|
714
|
-
this.ghostContainer = null;
|
|
715
|
-
}
|
|
187
|
+
return this.ghostController.hideGhost();
|
|
716
188
|
}
|
|
717
189
|
|
|
718
190
|
/**
|
|
719
191
|
* Обновить позицию "призрака" файла
|
|
720
192
|
*/
|
|
721
193
|
updateGhostPosition(x, y) {
|
|
722
|
-
|
|
723
|
-
this.ghostContainer.x = x;
|
|
724
|
-
this.ghostContainer.y = y;
|
|
725
|
-
}
|
|
194
|
+
return this.ghostController.updateGhostPosition(x, y);
|
|
726
195
|
}
|
|
727
196
|
|
|
728
197
|
/**
|
|
@@ -748,37 +217,19 @@ export class PlacementTool extends BaseTool {
|
|
|
748
217
|
);
|
|
749
218
|
|
|
750
219
|
// Создаем объект файла с данными с сервера
|
|
751
|
-
this.
|
|
752
|
-
type: 'file',
|
|
753
|
-
id: 'file',
|
|
754
|
-
position,
|
|
755
|
-
properties: {
|
|
756
|
-
fileName: uploadResult.name,
|
|
757
|
-
fileSize: uploadResult.size,
|
|
758
|
-
mimeType: uploadResult.mimeType,
|
|
759
|
-
formattedSize: uploadResult.formattedSize,
|
|
760
|
-
url: uploadResult.url,
|
|
761
|
-
width: props.width || 120,
|
|
762
|
-
height: props.height || 140
|
|
763
|
-
},
|
|
764
|
-
fileId: uploadResult.fileId || uploadResult.id // Сохраняем ID файла
|
|
765
|
-
});
|
|
220
|
+
this.payloadFactory.emitFileUploaded(position, uploadResult, props.width || 120, props.height || 140);
|
|
766
221
|
|
|
767
222
|
} catch (uploadError) {
|
|
768
223
|
console.error('Ошибка загрузки файла на сервер:', uploadError);
|
|
769
224
|
// Fallback: создаем объект файла с локальными данными
|
|
770
|
-
this.
|
|
771
|
-
type: 'file',
|
|
772
|
-
id: 'file',
|
|
225
|
+
this.payloadFactory.emitFileFallback(
|
|
773
226
|
position,
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
});
|
|
227
|
+
this.selectedFile.fileName,
|
|
228
|
+
this.selectedFile.fileSize,
|
|
229
|
+
this.selectedFile.mimeType,
|
|
230
|
+
props.width || 120,
|
|
231
|
+
props.height || 140
|
|
232
|
+
);
|
|
782
233
|
|
|
783
234
|
// Показываем предупреждение пользователю
|
|
784
235
|
alert('Ошибка загрузки файла на сервер. Файл добавлен локально.');
|
|
@@ -794,478 +245,49 @@ export class PlacementTool extends BaseTool {
|
|
|
794
245
|
* Показать "призрак" изображения
|
|
795
246
|
*/
|
|
796
247
|
async showImageGhost() {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
this.hideGhost(); // Сначала убираем старый призрак
|
|
800
|
-
|
|
801
|
-
// Создаем контейнер для призрака
|
|
802
|
-
this.ghostContainer = new PIXI.Container();
|
|
803
|
-
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
804
|
-
|
|
805
|
-
// Размеры призрака - используем размеры из pending/selected, если есть
|
|
806
|
-
const isEmojiIcon = this.selectedImage.properties?.isEmojiIcon;
|
|
807
|
-
const maxWidth = this.selectedImage.properties.width || (isEmojiIcon ? 64 : 300);
|
|
808
|
-
const maxHeight = this.selectedImage.properties.height || (isEmojiIcon ? 64 : 200);
|
|
809
|
-
|
|
810
|
-
try {
|
|
811
|
-
// Создаем превью изображения
|
|
812
|
-
const imageUrl = URL.createObjectURL(this.selectedImage.file);
|
|
813
|
-
const texture = await PIXI.Texture.fromURL(imageUrl);
|
|
814
|
-
|
|
815
|
-
// Вычисляем пропорциональные размеры
|
|
816
|
-
const imageAspect = texture.width / texture.height;
|
|
817
|
-
let width = maxWidth;
|
|
818
|
-
let height = maxWidth / imageAspect;
|
|
819
|
-
|
|
820
|
-
if (height > maxHeight) {
|
|
821
|
-
height = maxHeight;
|
|
822
|
-
width = maxHeight * imageAspect;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Создаем спрайт изображения
|
|
826
|
-
const sprite = new PIXI.Sprite(texture);
|
|
827
|
-
sprite.width = width;
|
|
828
|
-
sprite.height = height;
|
|
829
|
-
|
|
830
|
-
// Рамка вокруг изображения
|
|
831
|
-
const border = new PIXI.Graphics();
|
|
832
|
-
border.lineStyle(2, 0xDEE2E6, 0.8);
|
|
833
|
-
border.drawRoundedRect(-2, -2, width + 4, height + 4, 4);
|
|
834
|
-
|
|
835
|
-
this.ghostContainer.addChild(border);
|
|
836
|
-
this.ghostContainer.addChild(sprite);
|
|
837
|
-
|
|
838
|
-
// Центрируем контейнер относительно курсора
|
|
839
|
-
this.ghostContainer.pivot.x = width / 2;
|
|
840
|
-
this.ghostContainer.pivot.y = height / 2;
|
|
841
|
-
|
|
842
|
-
// Освобождаем URL
|
|
843
|
-
URL.revokeObjectURL(imageUrl);
|
|
844
|
-
|
|
845
|
-
} catch (error) {
|
|
846
|
-
console.warn('Не удалось загрузить превью изображения, показываем заглушку:', error);
|
|
847
|
-
|
|
848
|
-
// Fallback: простой прямоугольник-заглушка
|
|
849
|
-
const graphics = new PIXI.Graphics();
|
|
850
|
-
graphics.beginFill(0xF8F9FA, 0.8);
|
|
851
|
-
graphics.lineStyle(2, 0xDEE2E6, 0.8);
|
|
852
|
-
graphics.drawRoundedRect(0, 0, maxWidth, maxHeight, 8);
|
|
853
|
-
graphics.endFill();
|
|
854
|
-
|
|
855
|
-
// Иконка изображения
|
|
856
|
-
graphics.beginFill(0x6C757D, 0.6);
|
|
857
|
-
graphics.drawRoundedRect(maxWidth * 0.2, maxHeight * 0.15, maxWidth * 0.6, maxHeight * 0.3, 4);
|
|
858
|
-
graphics.endFill();
|
|
859
|
-
|
|
860
|
-
// Текст названия файла
|
|
861
|
-
const fileName = this.selectedImage.fileName || 'Image';
|
|
862
|
-
const displayName = fileName.length > 20 ? fileName.substring(0, 17) + '...' : fileName;
|
|
863
|
-
|
|
864
|
-
const nameText = new PIXI.Text(displayName, {
|
|
865
|
-
fontFamily: 'Arial, sans-serif',
|
|
866
|
-
fontSize: 12,
|
|
867
|
-
fill: 0x495057,
|
|
868
|
-
align: 'center',
|
|
869
|
-
wordWrap: true,
|
|
870
|
-
wordWrapWidth: maxWidth - 10
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
nameText.x = (maxWidth - nameText.width) / 2;
|
|
874
|
-
nameText.y = maxHeight * 0.55;
|
|
875
|
-
|
|
876
|
-
this.ghostContainer.addChild(graphics);
|
|
877
|
-
this.ghostContainer.addChild(nameText);
|
|
878
|
-
|
|
879
|
-
// Центрируем контейнер относительно курсора
|
|
880
|
-
this.ghostContainer.pivot.x = maxWidth / 2;
|
|
881
|
-
this.ghostContainer.pivot.y = maxHeight / 2;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
this.world.addChild(this.ghostContainer);
|
|
248
|
+
return this.ghostController.showImageGhost();
|
|
885
249
|
}
|
|
886
250
|
|
|
887
251
|
/**
|
|
888
252
|
* Показать "призрак" изображения по URL (для выбора из панели эмоджи)
|
|
889
253
|
*/
|
|
890
254
|
async showImageUrlGhost() {
|
|
891
|
-
|
|
892
|
-
const src = this.pending.properties?.src;
|
|
893
|
-
if (!src) return;
|
|
894
|
-
|
|
895
|
-
this.hideGhost();
|
|
896
|
-
|
|
897
|
-
this.ghostContainer = new PIXI.Container();
|
|
898
|
-
this.ghostContainer.alpha = 0.6;
|
|
899
|
-
|
|
900
|
-
// Для эмоджи используем точные размеры из pending для согласованности
|
|
901
|
-
const isEmojiIcon = this.pending.properties?.isEmojiIcon;
|
|
902
|
-
const maxWidth = this.pending.size?.width || this.pending.properties?.width || (isEmojiIcon ? 64 : 56);
|
|
903
|
-
const maxHeight = this.pending.size?.height || this.pending.properties?.height || (isEmojiIcon ? 64 : 56);
|
|
904
|
-
|
|
905
|
-
try {
|
|
906
|
-
const texture = await PIXI.Texture.fromURL(src);
|
|
907
|
-
const imageAspect = (texture.width || 1) / (texture.height || 1);
|
|
908
|
-
let width = maxWidth;
|
|
909
|
-
let height = maxWidth / imageAspect;
|
|
910
|
-
if (height > maxHeight) {
|
|
911
|
-
height = maxHeight;
|
|
912
|
-
width = maxHeight * imageAspect;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const sprite = new PIXI.Sprite(texture);
|
|
916
|
-
sprite.width = Math.max(1, Math.round(width));
|
|
917
|
-
sprite.height = Math.max(1, Math.round(height));
|
|
918
|
-
|
|
919
|
-
const border = new PIXI.Graphics();
|
|
920
|
-
try { border.lineStyle({ width: 2, color: 0xDEE2E6, alpha: 0.8 }); }
|
|
921
|
-
catch (_) { border.lineStyle(2, 0xDEE2E6, 0.8); }
|
|
922
|
-
border.drawRoundedRect(-2, -2, sprite.width + 4, sprite.height + 4, 4);
|
|
923
|
-
|
|
924
|
-
this.ghostContainer.addChild(border);
|
|
925
|
-
this.ghostContainer.addChild(sprite);
|
|
926
|
-
this.ghostContainer.pivot.set(sprite.width / 2, sprite.height / 2);
|
|
927
|
-
} catch (e) {
|
|
928
|
-
const g = new PIXI.Graphics();
|
|
929
|
-
g.beginFill(0xF0F0F0, 0.8);
|
|
930
|
-
g.lineStyle(2, 0xDEE2E6, 0.8);
|
|
931
|
-
g.drawRoundedRect(0, 0, maxWidth, maxHeight, 8);
|
|
932
|
-
g.endFill();
|
|
933
|
-
this.ghostContainer.addChild(g);
|
|
934
|
-
this.ghostContainer.pivot.set(maxWidth / 2, maxHeight / 2);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
this.world.addChild(this.ghostContainer);
|
|
938
|
-
|
|
939
|
-
// Для эмоджи не используем кастомный курсор, чтобы избежать дублирования призраков
|
|
940
|
-
if (!isEmojiIcon) {
|
|
941
|
-
// Кастомный курсор только для обычных изображений
|
|
942
|
-
try {
|
|
943
|
-
if (this.app && this.app.view && src) {
|
|
944
|
-
const cursorSize = 24;
|
|
945
|
-
const url = encodeURI(src);
|
|
946
|
-
// Используем CSS cursor с изображением, если поддерживается
|
|
947
|
-
this.cursor = `url(${url}) ${Math.floor(cursorSize/2)} ${Math.floor(cursorSize/2)}, default`;
|
|
948
|
-
this.app.view.style.cursor = this.cursor;
|
|
949
|
-
}
|
|
950
|
-
} catch (_) {}
|
|
951
|
-
} else {
|
|
952
|
-
// Для эмоджи используем стандартный курсор
|
|
953
|
-
if (this.app && this.app.view) {
|
|
954
|
-
this.cursor = 'crosshair';
|
|
955
|
-
this.app.view.style.cursor = this.cursor;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
255
|
+
return this.ghostController.showImageUrlGhost();
|
|
958
256
|
}
|
|
959
257
|
|
|
960
258
|
/**
|
|
961
259
|
* Показать "призрак" текста
|
|
962
260
|
*/
|
|
963
261
|
showTextGhost() {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
this.hideGhost(); // Сначала убираем старый призрак
|
|
967
|
-
|
|
968
|
-
// Создаем контейнер для призрака
|
|
969
|
-
this.ghostContainer = new PIXI.Container();
|
|
970
|
-
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
971
|
-
|
|
972
|
-
// Размеры призрака текста
|
|
973
|
-
const fontSize = this.pending.properties?.fontSize || 18;
|
|
974
|
-
const width = 120;
|
|
975
|
-
const height = fontSize + 20; // Высота зависит от размера шрифта
|
|
976
|
-
|
|
977
|
-
// Фон для текста (полупрозрачный прямоугольник)
|
|
978
|
-
const background = new PIXI.Graphics();
|
|
979
|
-
background.beginFill(0xFFFFFF, 0.8);
|
|
980
|
-
background.lineStyle(1, 0x007BFF, 0.8);
|
|
981
|
-
background.drawRoundedRect(0, 0, width, height, 4);
|
|
982
|
-
background.endFill();
|
|
983
|
-
|
|
984
|
-
// Текст-заглушка
|
|
985
|
-
const placeholderText = new PIXI.Text('Текст', {
|
|
986
|
-
fontFamily: 'Arial, sans-serif',
|
|
987
|
-
fontSize: fontSize,
|
|
988
|
-
fill: 0x6C757D,
|
|
989
|
-
align: 'left'
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
placeholderText.x = 8;
|
|
993
|
-
placeholderText.y = (height - placeholderText.height) / 2;
|
|
994
|
-
|
|
995
|
-
// Иконка курсора (маленькая вертикальная линия)
|
|
996
|
-
const cursor = new PIXI.Graphics();
|
|
997
|
-
cursor.lineStyle(2, 0x007BFF, 0.8);
|
|
998
|
-
cursor.moveTo(placeholderText.x + placeholderText.width + 4, placeholderText.y);
|
|
999
|
-
cursor.lineTo(placeholderText.x + placeholderText.width + 4, placeholderText.y + placeholderText.height);
|
|
1000
|
-
|
|
1001
|
-
this.ghostContainer.addChild(background);
|
|
1002
|
-
this.ghostContainer.addChild(placeholderText);
|
|
1003
|
-
this.ghostContainer.addChild(cursor);
|
|
1004
|
-
|
|
1005
|
-
// Центрируем контейнер относительно курсора
|
|
1006
|
-
this.ghostContainer.pivot.x = width / 2;
|
|
1007
|
-
this.ghostContainer.pivot.y = height / 2;
|
|
1008
|
-
|
|
1009
|
-
this.world.addChild(this.ghostContainer);
|
|
262
|
+
return this.ghostController.showTextGhost();
|
|
1010
263
|
}
|
|
1011
264
|
|
|
1012
265
|
/**
|
|
1013
266
|
* Показать "призрак" записки
|
|
1014
267
|
*/
|
|
1015
268
|
showNoteGhost() {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
this.hideGhost(); // Сначала убираем старый призрак
|
|
1019
|
-
|
|
1020
|
-
// Создаем контейнер для призрака
|
|
1021
|
-
this.ghostContainer = new PIXI.Container();
|
|
1022
|
-
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
1023
|
-
|
|
1024
|
-
// Размеры и стили (без текста у призрака)
|
|
1025
|
-
const width = this.pending.properties?.width || 250;
|
|
1026
|
-
const height = this.pending.properties?.height || 250;
|
|
1027
|
-
const backgroundColor = (typeof this.pending.properties?.backgroundColor === 'number')
|
|
1028
|
-
? this.pending.properties.backgroundColor
|
|
1029
|
-
: 0xFFF9C4; // желтый как у записки
|
|
1030
|
-
const textColor = (typeof this.pending.properties?.textColor === 'number')
|
|
1031
|
-
? this.pending.properties.textColor
|
|
1032
|
-
: 0x1A1A1A;
|
|
1033
|
-
|
|
1034
|
-
// Тени для призрака отключены по требованию (без тени)
|
|
1035
|
-
|
|
1036
|
-
// Основной фон записки (желтый как у оригинала)
|
|
1037
|
-
const background = new PIXI.Graphics();
|
|
1038
|
-
background.beginFill(backgroundColor, 1);
|
|
1039
|
-
background.drawRoundedRect(0, 0, width, height, 2);
|
|
1040
|
-
background.endFill();
|
|
1041
|
-
|
|
1042
|
-
// У призрака текста нет — только фон записки
|
|
1043
|
-
|
|
1044
|
-
// Порядок добавления: тень → фон → шапка → текст
|
|
1045
|
-
// Без тени
|
|
1046
|
-
this.ghostContainer.addChild(background);
|
|
1047
|
-
|
|
1048
|
-
// Центрируем контейнер относительно курсора
|
|
1049
|
-
this.ghostContainer.pivot.x = width / 2;
|
|
1050
|
-
this.ghostContainer.pivot.y = height / 2;
|
|
1051
|
-
|
|
1052
|
-
this.world.addChild(this.ghostContainer);
|
|
1053
|
-
// Текст убран — дополнительная загрузка шрифтов для призрака не требуется
|
|
269
|
+
return this.ghostController.showNoteGhost();
|
|
1054
270
|
}
|
|
1055
271
|
|
|
1056
272
|
/**
|
|
1057
273
|
* Показать "призрак" эмоджи
|
|
1058
274
|
*/
|
|
1059
275
|
showEmojiGhost() {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
this.hideGhost(); // Сначала убираем старый призрак
|
|
1063
|
-
|
|
1064
|
-
// Создаем контейнер для призрака
|
|
1065
|
-
this.ghostContainer = new PIXI.Container();
|
|
1066
|
-
this.ghostContainer.alpha = 0.7; // Немного менее прозрачный для эмоджи
|
|
1067
|
-
|
|
1068
|
-
// Получаем параметры эмоджи из pending
|
|
1069
|
-
const content = this.pending.properties?.content || '🙂';
|
|
1070
|
-
const fontSize = this.pending.properties?.fontSize || 48;
|
|
1071
|
-
const width = this.pending.properties?.width || fontSize;
|
|
1072
|
-
const height = this.pending.properties?.height || fontSize;
|
|
1073
|
-
|
|
1074
|
-
// Создаем эмоджи текст (как в EmojiObject)
|
|
1075
|
-
const emojiText = new PIXI.Text(content, {
|
|
1076
|
-
fontFamily: 'Segoe UI Emoji, Apple Color Emoji, Noto Color Emoji, Arial',
|
|
1077
|
-
fontSize: fontSize
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
// Устанавливаем якорь в левом верхнем углу (как в EmojiObject)
|
|
1081
|
-
if (typeof emojiText.anchor?.set === 'function') {
|
|
1082
|
-
emojiText.anchor.set(0, 0);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// Получаем базовые размеры для масштабирования
|
|
1086
|
-
const bounds = emojiText.getLocalBounds();
|
|
1087
|
-
const baseW = Math.max(1, bounds.width || 1);
|
|
1088
|
-
const baseH = Math.max(1, bounds.height || 1);
|
|
1089
|
-
|
|
1090
|
-
// Применяем равномерное масштабирование для подгонки под целевые размеры
|
|
1091
|
-
const scaleX = width / baseW;
|
|
1092
|
-
const scaleY = height / baseH;
|
|
1093
|
-
const scale = Math.min(scaleX, scaleY); // Равномерное масштабирование
|
|
1094
|
-
|
|
1095
|
-
emojiText.scale.set(scale, scale);
|
|
1096
|
-
|
|
1097
|
-
// Добавляем лёгкий фон для лучшей видимости призрака
|
|
1098
|
-
const background = new PIXI.Graphics();
|
|
1099
|
-
background.beginFill(0xFFFFFF, 0.3); // Полупрозрачный белый фон
|
|
1100
|
-
background.lineStyle(1, 0xDDDDDD, 0.5); // Тонкая граница
|
|
1101
|
-
background.drawRoundedRect(-4, -4, width + 8, height + 8, 4);
|
|
1102
|
-
background.endFill();
|
|
1103
|
-
|
|
1104
|
-
this.ghostContainer.addChild(background);
|
|
1105
|
-
this.ghostContainer.addChild(emojiText);
|
|
1106
|
-
|
|
1107
|
-
// Центрируем контейнер относительно курсора
|
|
1108
|
-
this.ghostContainer.pivot.x = width / 2;
|
|
1109
|
-
this.ghostContainer.pivot.y = height / 2;
|
|
1110
|
-
|
|
1111
|
-
this.world.addChild(this.ghostContainer);
|
|
276
|
+
return this.ghostController.showEmojiGhost();
|
|
1112
277
|
}
|
|
1113
278
|
|
|
1114
279
|
/**
|
|
1115
280
|
* Показать "призрак" фрейма
|
|
1116
281
|
*/
|
|
1117
282
|
showFrameGhost() {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
this.hideGhost(); // Сначала убираем старый призрак
|
|
1121
|
-
|
|
1122
|
-
// Создаем контейнер для призрака
|
|
1123
|
-
this.ghostContainer = new PIXI.Container();
|
|
1124
|
-
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
1125
|
-
|
|
1126
|
-
// Получаем параметры фрейма из pending
|
|
1127
|
-
const width = this.pending.properties?.width || 200;
|
|
1128
|
-
const height = this.pending.properties?.height || 300;
|
|
1129
|
-
const fillColor = (this.pending.properties?.backgroundColor ?? this.pending.properties?.fillColor) ?? 0xFFFFFF;
|
|
1130
|
-
const title = this.pending.properties?.title || 'Новый';
|
|
1131
|
-
|
|
1132
|
-
// Читаем стили рамки как у реального фрейма (FrameObject)
|
|
1133
|
-
const rootStyles = (typeof window !== 'undefined') ? getComputedStyle(document.documentElement) : null;
|
|
1134
|
-
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '4') : 4;
|
|
1135
|
-
const cssCornerRadius = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-corner-radius') || '6') : 6;
|
|
1136
|
-
const cssBorderColor = rootStyles ? rootStyles.getPropertyValue('--frame-border-color').trim() : '';
|
|
1137
|
-
const borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth : 4;
|
|
1138
|
-
const cornerRadius = Number.isFinite(cssCornerRadius) ? cssCornerRadius : 6;
|
|
1139
|
-
let strokeColor;
|
|
1140
|
-
if (cssBorderColor && cssBorderColor.startsWith('#')) {
|
|
1141
|
-
strokeColor = parseInt(cssBorderColor.slice(1), 16);
|
|
1142
|
-
} else {
|
|
1143
|
-
strokeColor = (typeof this.pending.properties?.borderColor === 'number') ? this.pending.properties.borderColor : 0xE0E0E0;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Создаем фон фрейма (как в FrameObject) — повторяем стили рамки
|
|
1147
|
-
const frameGraphics = new PIXI.Graphics();
|
|
1148
|
-
try {
|
|
1149
|
-
frameGraphics.lineStyle({ width: borderWidth, color: strokeColor, alpha: 1, alignment: 1 });
|
|
1150
|
-
} catch (e) {
|
|
1151
|
-
frameGraphics.lineStyle(borderWidth, strokeColor, 1);
|
|
1152
|
-
}
|
|
1153
|
-
// Заливка как у фрейма, прозрачность задаётся через контейнер (alpha)
|
|
1154
|
-
frameGraphics.beginFill(fillColor, 1);
|
|
1155
|
-
frameGraphics.drawRoundedRect(0, 0, width, height, cornerRadius);
|
|
1156
|
-
frameGraphics.endFill();
|
|
1157
|
-
|
|
1158
|
-
// Создаем заголовок фрейма (как в FrameObject)
|
|
1159
|
-
const titleText = new PIXI.Text(title, {
|
|
1160
|
-
fontFamily: 'Arial, sans-serif',
|
|
1161
|
-
fontSize: 14,
|
|
1162
|
-
fill: 0x333333,
|
|
1163
|
-
fontWeight: 'bold'
|
|
1164
|
-
});
|
|
1165
|
-
// Размещаем заголовок внутри верхней части фрейма
|
|
1166
|
-
titleText.anchor.set(0, 0);
|
|
1167
|
-
titleText.x = 8;
|
|
1168
|
-
titleText.y = 4;
|
|
1169
|
-
|
|
1170
|
-
this.ghostContainer.addChild(frameGraphics);
|
|
1171
|
-
this.ghostContainer.addChild(titleText);
|
|
1172
|
-
|
|
1173
|
-
// Центрируем контейнер относительно курсора
|
|
1174
|
-
this.ghostContainer.pivot.x = width / 2;
|
|
1175
|
-
this.ghostContainer.pivot.y = height / 2;
|
|
1176
|
-
|
|
1177
|
-
this.world.addChild(this.ghostContainer);
|
|
283
|
+
return this.ghostController.showFrameGhost();
|
|
1178
284
|
}
|
|
1179
285
|
|
|
1180
286
|
/**
|
|
1181
287
|
* Показать "призрак" фигуры
|
|
1182
288
|
*/
|
|
1183
289
|
showShapeGhost() {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
this.hideGhost(); // Сначала убираем старый призрак
|
|
1187
|
-
|
|
1188
|
-
// Создаем контейнер для призрака
|
|
1189
|
-
this.ghostContainer = new PIXI.Container();
|
|
1190
|
-
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
1191
|
-
|
|
1192
|
-
// Получаем параметры фигуры из pending
|
|
1193
|
-
const kind = this.pending.properties?.kind || 'square';
|
|
1194
|
-
const width = 100; // Стандартный размер по умолчанию
|
|
1195
|
-
const height = 100;
|
|
1196
|
-
const fillColor = 0x3b82f6; // Синий цвет как в ShapeObject
|
|
1197
|
-
const cornerRadius = this.pending.properties?.cornerRadius || 10;
|
|
1198
|
-
|
|
1199
|
-
// Создаем графику фигуры (точно как в ShapeObject._draw)
|
|
1200
|
-
const shapeGraphics = new PIXI.Graphics();
|
|
1201
|
-
shapeGraphics.beginFill(fillColor, 0.8); // Полупрозрачная заливка
|
|
1202
|
-
|
|
1203
|
-
switch (kind) {
|
|
1204
|
-
case 'circle': {
|
|
1205
|
-
const r = Math.min(width, height) / 2;
|
|
1206
|
-
shapeGraphics.drawCircle(width / 2, height / 2, r);
|
|
1207
|
-
break;
|
|
1208
|
-
}
|
|
1209
|
-
case 'rounded': {
|
|
1210
|
-
const r = cornerRadius || 10;
|
|
1211
|
-
shapeGraphics.drawRoundedRect(0, 0, width, height, r);
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
case 'triangle': {
|
|
1215
|
-
shapeGraphics.moveTo(width / 2, 0);
|
|
1216
|
-
shapeGraphics.lineTo(width, height);
|
|
1217
|
-
shapeGraphics.lineTo(0, height);
|
|
1218
|
-
shapeGraphics.lineTo(width / 2, 0);
|
|
1219
|
-
break;
|
|
1220
|
-
}
|
|
1221
|
-
case 'diamond': {
|
|
1222
|
-
shapeGraphics.moveTo(width / 2, 0);
|
|
1223
|
-
shapeGraphics.lineTo(width, height / 2);
|
|
1224
|
-
shapeGraphics.lineTo(width / 2, height);
|
|
1225
|
-
shapeGraphics.lineTo(0, height / 2);
|
|
1226
|
-
shapeGraphics.lineTo(width / 2, 0);
|
|
1227
|
-
break;
|
|
1228
|
-
}
|
|
1229
|
-
case 'parallelogram': {
|
|
1230
|
-
const skew = Math.min(width * 0.25, 20);
|
|
1231
|
-
shapeGraphics.moveTo(skew, 0);
|
|
1232
|
-
shapeGraphics.lineTo(width, 0);
|
|
1233
|
-
shapeGraphics.lineTo(width - skew, height);
|
|
1234
|
-
shapeGraphics.lineTo(0, height);
|
|
1235
|
-
shapeGraphics.lineTo(skew, 0);
|
|
1236
|
-
break;
|
|
1237
|
-
}
|
|
1238
|
-
case 'arrow': {
|
|
1239
|
-
const shaftH = Math.max(6, height * 0.3);
|
|
1240
|
-
const shaftY = (height - shaftH) / 2;
|
|
1241
|
-
shapeGraphics.drawRect(0, shaftY, width * 0.6, shaftH);
|
|
1242
|
-
shapeGraphics.moveTo(width * 0.6, 0);
|
|
1243
|
-
shapeGraphics.lineTo(width, height / 2);
|
|
1244
|
-
shapeGraphics.lineTo(width * 0.6, height);
|
|
1245
|
-
shapeGraphics.lineTo(width * 0.6, 0);
|
|
1246
|
-
break;
|
|
1247
|
-
}
|
|
1248
|
-
case 'square':
|
|
1249
|
-
default: {
|
|
1250
|
-
shapeGraphics.drawRect(0, 0, width, height);
|
|
1251
|
-
break;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
shapeGraphics.endFill();
|
|
1255
|
-
|
|
1256
|
-
// Добавляем тонкую рамку для лучшей видимости призрака
|
|
1257
|
-
const border = new PIXI.Graphics();
|
|
1258
|
-
border.lineStyle(2, 0x007BFF, 0.6);
|
|
1259
|
-
border.drawRect(-2, -2, width + 4, height + 4);
|
|
1260
|
-
|
|
1261
|
-
this.ghostContainer.addChild(border);
|
|
1262
|
-
this.ghostContainer.addChild(shapeGraphics);
|
|
1263
|
-
|
|
1264
|
-
// Центрируем контейнер относительно курсора
|
|
1265
|
-
this.ghostContainer.pivot.x = width / 2;
|
|
1266
|
-
this.ghostContainer.pivot.y = height / 2;
|
|
1267
|
-
|
|
1268
|
-
this.world.addChild(this.ghostContainer);
|
|
290
|
+
return this.ghostController.showShapeGhost();
|
|
1269
291
|
}
|
|
1270
292
|
|
|
1271
293
|
/**
|
|
@@ -1297,18 +319,7 @@ export class PlacementTool extends BaseTool {
|
|
|
1297
319
|
};
|
|
1298
320
|
|
|
1299
321
|
// Создаем объект изображения с данными с сервера
|
|
1300
|
-
this.
|
|
1301
|
-
type: 'image',
|
|
1302
|
-
id: 'image',
|
|
1303
|
-
position,
|
|
1304
|
-
properties: {
|
|
1305
|
-
src: uploadResult.url,
|
|
1306
|
-
name: uploadResult.name,
|
|
1307
|
-
width: targetW,
|
|
1308
|
-
height: targetH
|
|
1309
|
-
},
|
|
1310
|
-
imageId: uploadResult.imageId || uploadResult.id // Сохраняем ID изображения
|
|
1311
|
-
});
|
|
322
|
+
this.payloadFactory.emitImageUploaded(position, uploadResult, targetW, targetH);
|
|
1312
323
|
|
|
1313
324
|
} catch (uploadError) {
|
|
1314
325
|
console.error('Ошибка загрузки изображения на сервер:', uploadError);
|
|
@@ -1325,17 +336,7 @@ export class PlacementTool extends BaseTool {
|
|
|
1325
336
|
y: Math.round(worldPoint.y - halfH)
|
|
1326
337
|
};
|
|
1327
338
|
|
|
1328
|
-
this.
|
|
1329
|
-
type: 'image',
|
|
1330
|
-
id: 'image',
|
|
1331
|
-
position,
|
|
1332
|
-
properties: {
|
|
1333
|
-
src: imageUrl,
|
|
1334
|
-
name: this.selectedImage.fileName,
|
|
1335
|
-
width: targetW,
|
|
1336
|
-
height: targetH
|
|
1337
|
-
}
|
|
1338
|
-
});
|
|
339
|
+
this.payloadFactory.emitImageFallback(position, imageUrl, this.selectedImage.fileName, targetW, targetH);
|
|
1339
340
|
|
|
1340
341
|
// Показываем предупреждение пользователю
|
|
1341
342
|
alert('Ошибка загрузки изображения на сервер. Изображение добавлено локально.');
|