@sequent-org/moodboard 1.0.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 +44 -0
- package/src/assets/icons/README.md +105 -0
- package/src/assets/icons/attachments.svg +3 -0
- package/src/assets/icons/clear.svg +5 -0
- package/src/assets/icons/comments.svg +3 -0
- package/src/assets/icons/emoji.svg +6 -0
- package/src/assets/icons/frame.svg +3 -0
- package/src/assets/icons/image.svg +3 -0
- package/src/assets/icons/note.svg +3 -0
- package/src/assets/icons/pan.svg +3 -0
- package/src/assets/icons/pencil.svg +3 -0
- package/src/assets/icons/redo.svg +3 -0
- package/src/assets/icons/select.svg +9 -0
- package/src/assets/icons/shapes.svg +3 -0
- package/src/assets/icons/text-add.svg +3 -0
- package/src/assets/icons/topbar/README.md +39 -0
- package/src/assets/icons/topbar/grid-cross.svg +6 -0
- package/src/assets/icons/topbar/grid-dot.svg +3 -0
- package/src/assets/icons/topbar/grid-line.svg +3 -0
- package/src/assets/icons/topbar/grid-off.svg +3 -0
- package/src/assets/icons/topbar/paint.svg +3 -0
- package/src/assets/icons/undo.svg +3 -0
- package/src/core/ApiClient.js +309 -0
- package/src/core/EventBus.js +42 -0
- package/src/core/HistoryManager.js +261 -0
- package/src/core/KeyboardManager.js +710 -0
- package/src/core/PixiEngine.js +439 -0
- package/src/core/SaveManager.js +381 -0
- package/src/core/StateManager.js +64 -0
- package/src/core/commands/BaseCommand.js +68 -0
- package/src/core/commands/CopyObjectCommand.js +44 -0
- package/src/core/commands/CreateObjectCommand.js +46 -0
- package/src/core/commands/DeleteObjectCommand.js +146 -0
- package/src/core/commands/EditFileNameCommand.js +107 -0
- package/src/core/commands/GroupMoveCommand.js +47 -0
- package/src/core/commands/GroupReorderZCommand.js +74 -0
- package/src/core/commands/GroupResizeCommand.js +37 -0
- package/src/core/commands/GroupRotateCommand.js +41 -0
- package/src/core/commands/MoveObjectCommand.js +89 -0
- package/src/core/commands/PasteObjectCommand.js +103 -0
- package/src/core/commands/ReorderZCommand.js +45 -0
- package/src/core/commands/ResizeObjectCommand.js +135 -0
- package/src/core/commands/RotateObjectCommand.js +70 -0
- package/src/core/commands/index.js +14 -0
- package/src/core/events/Events.js +147 -0
- package/src/core/index.js +1632 -0
- package/src/core/rendering/GeometryUtils.js +89 -0
- package/src/core/rendering/HitTestManager.js +186 -0
- package/src/core/rendering/LayerManager.js +137 -0
- package/src/core/rendering/ObjectRenderer.js +363 -0
- package/src/core/rendering/PixiRenderer.js +140 -0
- package/src/core/rendering/index.js +9 -0
- package/src/grid/BaseGrid.js +164 -0
- package/src/grid/CrossGrid.js +75 -0
- package/src/grid/DotGrid.js +148 -0
- package/src/grid/GridFactory.js +173 -0
- package/src/grid/LineGrid.js +115 -0
- package/src/index.js +2 -0
- package/src/moodboard/ActionHandler.js +114 -0
- package/src/moodboard/DataManager.js +114 -0
- package/src/moodboard/MoodBoard.js +359 -0
- package/src/moodboard/WorkspaceManager.js +103 -0
- package/src/objects/BaseObject.js +1 -0
- package/src/objects/CommentObject.js +115 -0
- package/src/objects/DrawingObject.js +114 -0
- package/src/objects/EmojiObject.js +98 -0
- package/src/objects/FileObject.js +318 -0
- package/src/objects/FrameObject.js +127 -0
- package/src/objects/ImageObject.js +72 -0
- package/src/objects/NoteObject.js +227 -0
- package/src/objects/ObjectFactory.js +61 -0
- package/src/objects/ShapeObject.js +134 -0
- package/src/objects/StampObject.js +0 -0
- package/src/objects/StickerObject.js +0 -0
- package/src/objects/TextObject.js +123 -0
- package/src/services/BoardService.js +85 -0
- package/src/services/FileUploadService.js +398 -0
- package/src/services/FrameService.js +138 -0
- package/src/services/ImageUploadService.js +246 -0
- package/src/services/ZOrderManager.js +50 -0
- package/src/services/ZoomPanController.js +78 -0
- package/src/src.7z +0 -0
- package/src/src.zip +0 -0
- package/src/src2.zip +0 -0
- package/src/tools/AlignmentGuides.js +326 -0
- package/src/tools/BaseTool.js +257 -0
- package/src/tools/ResizeHandles.js +381 -0
- package/src/tools/ToolManager.js +580 -0
- package/src/tools/board-tools/PanTool.js +43 -0
- package/src/tools/board-tools/ZoomTool.js +393 -0
- package/src/tools/object-tools/DrawingTool.js +404 -0
- package/src/tools/object-tools/PlacementTool.js +1005 -0
- package/src/tools/object-tools/SelectTool.js +2183 -0
- package/src/tools/object-tools/TextTool.js +416 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
- package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
- package/src/tools/object-tools/selection/GroupDragController.js +61 -0
- package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
- package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
- package/src/tools/object-tools/selection/HandlesSync.js +96 -0
- package/src/tools/object-tools/selection/ResizeController.js +68 -0
- package/src/tools/object-tools/selection/RotateController.js +58 -0
- package/src/tools/object-tools/selection/SelectionModel.js +42 -0
- package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
- package/src/ui/CommentPopover.js +187 -0
- package/src/ui/ContextMenu.js +340 -0
- package/src/ui/FilePropertiesPanel.js +298 -0
- package/src/ui/FramePropertiesPanel.js +462 -0
- package/src/ui/HtmlHandlesLayer.js +778 -0
- package/src/ui/HtmlTextLayer.js +279 -0
- package/src/ui/MapPanel.js +290 -0
- package/src/ui/NotePropertiesPanel.js +502 -0
- package/src/ui/SaveStatus.js +250 -0
- package/src/ui/TextPropertiesPanel.js +911 -0
- package/src/ui/Toolbar.js +1118 -0
- package/src/ui/Topbar.js +220 -0
- package/src/ui/ZoomPanel.js +116 -0
- package/src/ui/styles/workspace.css +854 -0
- package/src/utils/colors.js +0 -0
- package/src/utils/geometry.js +0 -0
- package/src/utils/iconLoader.js +270 -0
- package/src/utils/objectIdGenerator.js +17 -0
- package/src/utils/topbarIconLoader.js +114 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { ObjectFactory } from '../objects/ObjectFactory.js';
|
|
3
|
+
import { ObjectRenderer } from './rendering/ObjectRenderer.js';
|
|
4
|
+
|
|
5
|
+
export class PixiEngine {
|
|
6
|
+
constructor(container, eventBus, options) {
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.eventBus = eventBus;
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.objects = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async init() {
|
|
14
|
+
this.app = new PIXI.Application({
|
|
15
|
+
width: this.options.width,
|
|
16
|
+
height: this.options.height,
|
|
17
|
+
backgroundColor: this.options.backgroundColor,
|
|
18
|
+
antialias: true,
|
|
19
|
+
resolution: (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1,
|
|
20
|
+
autoDensity: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
this.container.appendChild(this.app.view);
|
|
24
|
+
if (PIXI.settings && typeof PIXI.settings.ROUND_PIXELS !== 'undefined') {
|
|
25
|
+
PIXI.settings.ROUND_PIXELS = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Отдельные слои: сетка (не двигается) и мир с объектами (двигается)
|
|
29
|
+
this.gridLayer = new PIXI.Container();
|
|
30
|
+
this.gridLayer.name = 'gridLayer';
|
|
31
|
+
this.gridLayer.zIndex = 0;
|
|
32
|
+
this.app.stage.addChild(this.gridLayer);
|
|
33
|
+
|
|
34
|
+
this.worldLayer = new PIXI.Container();
|
|
35
|
+
this.worldLayer.name = 'worldLayer';
|
|
36
|
+
this.worldLayer.zIndex = 1;
|
|
37
|
+
this.worldLayer.sortableChildren = true;
|
|
38
|
+
this.app.stage.addChild(this.worldLayer);
|
|
39
|
+
|
|
40
|
+
// Инициализируем ObjectRenderer
|
|
41
|
+
this.renderer = new ObjectRenderer(this.objects);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
createObject(objectData) {
|
|
45
|
+
let pixiObject;
|
|
46
|
+
|
|
47
|
+
const instance = ObjectFactory.create(objectData.type, objectData);
|
|
48
|
+
if (instance) {
|
|
49
|
+
pixiObject = instance.getPixi();
|
|
50
|
+
const prevMb = pixiObject._mb || {};
|
|
51
|
+
pixiObject._mb = {
|
|
52
|
+
...prevMb,
|
|
53
|
+
objectId: objectData.id,
|
|
54
|
+
type: objectData.type,
|
|
55
|
+
instance: instance // Сохраняем ссылку на сам объект
|
|
56
|
+
};
|
|
57
|
+
this.objects.set(objectData.id, pixiObject);
|
|
58
|
+
this.worldLayer.addChild(pixiObject);
|
|
59
|
+
} else {
|
|
60
|
+
console.warn(`Unknown object type: ${objectData.type}`);
|
|
61
|
+
pixiObject = this.createDefaultObject(objectData);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (pixiObject) {
|
|
65
|
+
pixiObject.x = objectData.position.x;
|
|
66
|
+
pixiObject.y = objectData.position.y;
|
|
67
|
+
pixiObject.eventMode = 'static'; // Исправляем deprecation warning
|
|
68
|
+
pixiObject.cursor = 'pointer';
|
|
69
|
+
// Сохраняем метаданные о типе и свойствах для последующих перерасчетов (resize),
|
|
70
|
+
// если не были заданы выше (для frame уже установлено)
|
|
71
|
+
const prevMb = pixiObject._mb || {};
|
|
72
|
+
pixiObject._mb = {
|
|
73
|
+
...prevMb,
|
|
74
|
+
objectId: prevMb.objectId ?? objectData.id,
|
|
75
|
+
type: prevMb.type ?? objectData.type,
|
|
76
|
+
properties: prevMb.properties ?? (objectData.properties || {}),
|
|
77
|
+
instance: prevMb.instance
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Устанавливаем центр вращения в центр объекта
|
|
81
|
+
if (pixiObject.anchor !== undefined) {
|
|
82
|
+
// Для объектов с anchor (текст, спрайты)
|
|
83
|
+
pixiObject.anchor.set(0.5, 0.5);
|
|
84
|
+
// Компенсируем смещение после центрирования anchor, если координаты ещё не скомпенсированы
|
|
85
|
+
const needsCompensation = !objectData.transform || !objectData.transform.pivotCompensated;
|
|
86
|
+
if (needsCompensation) {
|
|
87
|
+
// Используем запрошенные размеры объекта (objectData.width/height),
|
|
88
|
+
// т.к. текстура спрайта может ещё не загрузиться и getBounds() вернёт 0
|
|
89
|
+
const halfW = (objectData.width || 0) / 2;
|
|
90
|
+
const halfH = (objectData.height || 0) / 2;
|
|
91
|
+
pixiObject.x += halfW;
|
|
92
|
+
pixiObject.y += halfH;
|
|
93
|
+
}
|
|
94
|
+
} else if (pixiObject instanceof PIXI.Graphics) {
|
|
95
|
+
// Для Graphics объектов устанавливаем pivot в центр
|
|
96
|
+
const bounds = pixiObject.getBounds();
|
|
97
|
+
const pivotX = bounds.width / 2;
|
|
98
|
+
const pivotY = bounds.height / 2;
|
|
99
|
+
pixiObject.pivot.set(pivotX, pivotY);
|
|
100
|
+
|
|
101
|
+
// Компенсируем смещение pivot, только если координаты еще НЕ были скомпенсированы
|
|
102
|
+
// Это проверяется по наличию transform.pivotCompensated
|
|
103
|
+
const needsCompensation = !objectData.transform || !objectData.transform.pivotCompensated;
|
|
104
|
+
|
|
105
|
+
if (needsCompensation) {
|
|
106
|
+
pixiObject.x += pivotX;
|
|
107
|
+
pixiObject.y += pivotY;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Применяем поворот из сохраненного состояния
|
|
112
|
+
if (objectData.transform && objectData.transform.rotation !== undefined) {
|
|
113
|
+
// Преобразуем градусы в радианы (углы сохраняются в градусах)
|
|
114
|
+
const angleRadians = objectData.transform.rotation * Math.PI / 180;
|
|
115
|
+
pixiObject.rotation = angleRadians;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Убеждаемся, что объект может участвовать в hit testing
|
|
119
|
+
if (pixiObject.beginFill) {
|
|
120
|
+
// no-op
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Z-порядок пересчитывается извне (ZOrderManager)
|
|
124
|
+
this.worldLayer.addChild(pixiObject);
|
|
125
|
+
this.objects.set(objectData.id, pixiObject);
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Добавление/обновление сетки в gridLayer
|
|
132
|
+
setGrid(gridInstance) {
|
|
133
|
+
if (!this.gridLayer) return;
|
|
134
|
+
this.gridLayer.removeChildren();
|
|
135
|
+
if (gridInstance && gridInstance.getPixiObject) {
|
|
136
|
+
const g = gridInstance.getPixiObject();
|
|
137
|
+
g.zIndex = 0;
|
|
138
|
+
g.x = 0;
|
|
139
|
+
g.y = 0;
|
|
140
|
+
this.gridLayer.addChild(g);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// createFrame удалён — логика вынесена в FrameObject
|
|
145
|
+
|
|
146
|
+
// createText удалён — логика в TextObject
|
|
147
|
+
|
|
148
|
+
// createEmoji удалён — логика вынесена в EmojiObject
|
|
149
|
+
|
|
150
|
+
// createShape удалён — логика в ShapeObject
|
|
151
|
+
|
|
152
|
+
// createDrawing удалён — логика вынесена в DrawingObject
|
|
153
|
+
|
|
154
|
+
createDefaultObject(objectData) {
|
|
155
|
+
// Заглушка для неизвестных типов
|
|
156
|
+
const graphics = new PIXI.Graphics();
|
|
157
|
+
graphics.beginFill(0xFF0000, 0.5);
|
|
158
|
+
graphics.drawRect(0, 0, objectData.width || 100, objectData.height || 100);
|
|
159
|
+
graphics.endFill();
|
|
160
|
+
return graphics;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
removeObject(objectId) {
|
|
164
|
+
const pixiObject = this.objects.get(objectId);
|
|
165
|
+
if (pixiObject) {
|
|
166
|
+
if (this.worldLayer) {
|
|
167
|
+
this.worldLayer.removeChild(pixiObject);
|
|
168
|
+
} else {
|
|
169
|
+
this.app.stage.removeChild(pixiObject);
|
|
170
|
+
}
|
|
171
|
+
this.objects.delete(objectId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Обновить размер объекта
|
|
177
|
+
*/
|
|
178
|
+
updateObjectSize(objectId, size, objectType = null) {
|
|
179
|
+
const pixiObject = this.objects.get(objectId);
|
|
180
|
+
if (!pixiObject) return;
|
|
181
|
+
|
|
182
|
+
// Сохраняем позицию (центр) на случай, если инстанс пересоздаст геометрию
|
|
183
|
+
const position = { x: pixiObject.x, y: pixiObject.y };
|
|
184
|
+
|
|
185
|
+
console.log(`🎨 Обновляем размер объекта ${objectId}, тип: ${objectType}`);
|
|
186
|
+
|
|
187
|
+
// Для Graphics объектов (рамки, фигуры) нужно пересоздать геометрию
|
|
188
|
+
// Делегируем изменение размера объекту, если есть инстанс с updateSize
|
|
189
|
+
const meta = pixiObject._mb || {};
|
|
190
|
+
if (meta.instance && typeof meta.instance.updateSize === 'function') {
|
|
191
|
+
meta.instance.updateSize(size);
|
|
192
|
+
} else if (pixiObject instanceof PIXI.Graphics) {
|
|
193
|
+
// Fallback для устаревших объектов без инстанса
|
|
194
|
+
this.recreateGraphicsObject(pixiObject, size, position, objectType);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Пересоздать Graphics объект с новым размером
|
|
200
|
+
*/
|
|
201
|
+
recreateGraphicsObject(pixiObject, size, position, objectType = null) {
|
|
202
|
+
// Очищаем графику
|
|
203
|
+
pixiObject.clear();
|
|
204
|
+
|
|
205
|
+
console.log(`🔄 Пересоздаем Graphics объект, тип: ${objectType}`);
|
|
206
|
+
|
|
207
|
+
// Определяем что рисовать по типу объекта
|
|
208
|
+
if (objectType === 'drawing') {
|
|
209
|
+
// Рисунок: перерисовываем по сохранённым точкам с масштабированием под новый size
|
|
210
|
+
const meta = pixiObject._mb || {};
|
|
211
|
+
const props = meta.properties || {};
|
|
212
|
+
const color = props.strokeColor ?? 0x111827;
|
|
213
|
+
const widthPx = props.strokeWidth ?? 2;
|
|
214
|
+
const alpha = props.mode === 'marker' ? 0.6 : 1;
|
|
215
|
+
const pts = Array.isArray(props.points) ? props.points : [];
|
|
216
|
+
const baseW = props.baseWidth || size.width || 1;
|
|
217
|
+
const baseH = props.baseHeight || size.height || 1;
|
|
218
|
+
const scaleX = baseW ? (size.width / baseW) : 1;
|
|
219
|
+
const scaleY = baseH ? (size.height / baseH) : 1;
|
|
220
|
+
const lineWidth = props.mode === 'marker' ? widthPx * 2 : widthPx;
|
|
221
|
+
pixiObject.lineStyle({ width: lineWidth, color, alpha, cap: 'round', join: 'round', miterLimit: 2, alignment: 0.5 });
|
|
222
|
+
pixiObject.blendMode = props.mode === 'marker' ? PIXI.BLEND_MODES.LIGHTEN : PIXI.BLEND_MODES.NORMAL;
|
|
223
|
+
if (pts.length > 0) {
|
|
224
|
+
if (pts.length < 3) {
|
|
225
|
+
pixiObject.moveTo(pts[0].x * scaleX, pts[0].y * scaleY);
|
|
226
|
+
for (let i = 1; i < pts.length; i++) pixiObject.lineTo(pts[i].x * scaleX, pts[i].y * scaleY);
|
|
227
|
+
} else {
|
|
228
|
+
pixiObject.moveTo(pts[0].x * scaleX, pts[0].y * scaleY);
|
|
229
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
230
|
+
const cx = pts[i].x * scaleX, cy = pts[i].y * scaleY;
|
|
231
|
+
const nx = pts[i + 1].x * scaleX, ny = pts[i + 1].y * scaleY;
|
|
232
|
+
const mx = (cx + nx) / 2, my = (cy + ny) / 2;
|
|
233
|
+
pixiObject.quadraticCurveTo(cx, cy, mx, my);
|
|
234
|
+
}
|
|
235
|
+
const pen = pts[pts.length - 2];
|
|
236
|
+
const last = pts[pts.length - 1];
|
|
237
|
+
pixiObject.quadraticCurveTo(pen.x * scaleX, pen.y * scaleY, last.x * scaleX, last.y * scaleY);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// Fallback - определяем по существующему содержимому (если тип не передан)
|
|
242
|
+
console.warn(`⚠️ Тип объекта не определен, используем fallback логику`);
|
|
243
|
+
|
|
244
|
+
// Если есть только контур без заливки - это рамка
|
|
245
|
+
// Если есть заливка - это фигура
|
|
246
|
+
const borderWidth = 2;
|
|
247
|
+
pixiObject.lineStyle(borderWidth, 0x333333, 1);
|
|
248
|
+
pixiObject.beginFill(0xFFFFFF, 0.1);
|
|
249
|
+
|
|
250
|
+
const halfBorder = borderWidth / 2;
|
|
251
|
+
pixiObject.drawRect(halfBorder, halfBorder, size.width - borderWidth, size.height - borderWidth);
|
|
252
|
+
pixiObject.endFill();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Устанавливаем pivot в центр (для правильного вращения)
|
|
256
|
+
const pivotX = size.width / 2;
|
|
257
|
+
const pivotY = size.height / 2;
|
|
258
|
+
// Сохраняем текущий центр до изменения pivot
|
|
259
|
+
const prevCenter = { x: pixiObject.x, y: pixiObject.y };
|
|
260
|
+
pixiObject.pivot.set(pivotX, pivotY);
|
|
261
|
+
// Восстанавливаем центр, чтобы левый-верх в state не «уползал» при ресайзе
|
|
262
|
+
pixiObject.x = prevCenter.x;
|
|
263
|
+
pixiObject.y = prevCenter.y;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Обновить размер текстового объекта
|
|
268
|
+
*/
|
|
269
|
+
// Методы обновления текстов/эмоджи перенесены в соответствующие классы
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Обновить содержимое объекта
|
|
273
|
+
*/
|
|
274
|
+
updateObjectContent(objectId, content) {
|
|
275
|
+
this.renderer.updateObjectContent(objectId, content);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Скрыть текст объекта (используется во время редактирования)
|
|
280
|
+
*/
|
|
281
|
+
hideObjectText(objectId) {
|
|
282
|
+
this.renderer.hideObjectText(objectId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Показать текст объекта (используется после завершения редактирования)
|
|
287
|
+
*/
|
|
288
|
+
showObjectText(objectId) {
|
|
289
|
+
this.renderer.showObjectText(objectId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Обновить угол поворота объекта
|
|
294
|
+
*/
|
|
295
|
+
updateObjectRotation(objectId, angleDegrees) {
|
|
296
|
+
const pixiObject = this.objects.get(objectId);
|
|
297
|
+
if (!pixiObject) return;
|
|
298
|
+
|
|
299
|
+
// Конвертируем градусы в радианы
|
|
300
|
+
const angleRadians = angleDegrees * Math.PI / 180;
|
|
301
|
+
|
|
302
|
+
// Применяем поворот
|
|
303
|
+
pixiObject.rotation = angleRadians;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Установить цвет заливки для фрейма, не изменяя размер и позицию
|
|
308
|
+
* Используется для визуала «во время перетаскивания» (светло-серый фон)
|
|
309
|
+
*/
|
|
310
|
+
setFrameFill(objectId, width, height, fillColor = 0xFFFFFF) {
|
|
311
|
+
const pixiObject = this.objects.get(objectId);
|
|
312
|
+
if (!pixiObject || !(pixiObject instanceof PIXI.Graphics)) return;
|
|
313
|
+
const meta = pixiObject._mb || {};
|
|
314
|
+
if (meta.type !== 'frame') return;
|
|
315
|
+
if (meta.instance) {
|
|
316
|
+
meta.instance.setFill(fillColor);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Поиск объекта в указанной позиции
|
|
322
|
+
*/
|
|
323
|
+
hitTest(x, y) {
|
|
324
|
+
// Получаем все объекты в позиции (PIXI автоматически учитывает трансформации)
|
|
325
|
+
const point = new PIXI.Point(x, y);
|
|
326
|
+
|
|
327
|
+
// Проходим по объектам в worldLayer от верхних к нижним
|
|
328
|
+
const container = this.worldLayer || this.app.stage;
|
|
329
|
+
|
|
330
|
+
for (let i = container.children.length - 1; i >= 0; i--) {
|
|
331
|
+
const child = container.children[i];
|
|
332
|
+
|
|
333
|
+
if (child.containsPoint && child.containsPoint(point)) {
|
|
334
|
+
// Находим ID объекта
|
|
335
|
+
for (const [objectId, pixiObject] of this.objects.entries()) {
|
|
336
|
+
if (pixiObject === child) {
|
|
337
|
+
return {
|
|
338
|
+
type: 'object',
|
|
339
|
+
object: objectId,
|
|
340
|
+
pixiObject: child
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Доп. хит-тест для нарисованных линий (stroke), где containsPoint может не сработать
|
|
346
|
+
const meta = child._mb;
|
|
347
|
+
if (meta && meta.type === 'drawing' && child.toLocal) {
|
|
348
|
+
// Переводим точку в локальные координаты объекта
|
|
349
|
+
const local = child.toLocal(point);
|
|
350
|
+
const props = meta.properties || {};
|
|
351
|
+
const pts = Array.isArray(props.points) ? props.points : [];
|
|
352
|
+
if (pts.length >= 2) {
|
|
353
|
+
// Оценка текущего масштаба относительно базовых размеров
|
|
354
|
+
const baseW = props.baseWidth || 1;
|
|
355
|
+
const baseH = props.baseHeight || 1;
|
|
356
|
+
const b = child.getBounds();
|
|
357
|
+
const scaleX = baseW ? (b.width / baseW) : 1;
|
|
358
|
+
const scaleY = baseH ? (b.height / baseH) : 1;
|
|
359
|
+
// Толщина линии с учётом режима маркера
|
|
360
|
+
const baseWidth = props.strokeWidth || 2;
|
|
361
|
+
const lineWidth = (props.mode === 'marker' ? baseWidth * 2 : baseWidth);
|
|
362
|
+
const threshold = Math.max(4, lineWidth / 2 + 3);
|
|
363
|
+
// Проверяем расстояние до каждого сегмента
|
|
364
|
+
for (let j = 0; j < pts.length - 1; j++) {
|
|
365
|
+
const ax = pts[j].x * scaleX;
|
|
366
|
+
const ay = pts[j].y * scaleY;
|
|
367
|
+
const bx = pts[j + 1].x * scaleX;
|
|
368
|
+
const by = pts[j + 1].y * scaleY;
|
|
369
|
+
const dist = this._distancePointToSegment(local.x, local.y, ax, ay, bx, by);
|
|
370
|
+
if (dist <= threshold) {
|
|
371
|
+
// Найдём и вернём ID
|
|
372
|
+
for (const [objectId, pixiObject] of this.objects.entries()) {
|
|
373
|
+
if (pixiObject === child) {
|
|
374
|
+
return { type: 'object', object: objectId, pixiObject: child };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return { type: 'empty' };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Геометрические помощники/хит-тесты вынести в отдельный сервис при следующем рефакторинге
|
|
388
|
+
_distancePointToSegment(px, py, ax, ay, bx, by) {
|
|
389
|
+
const vectorABx = bx - ax;
|
|
390
|
+
const vectorABy = by - ay;
|
|
391
|
+
const vectorAPx = px - ax;
|
|
392
|
+
const vectorAPy = py - ay;
|
|
393
|
+
const squaredLengthAB = vectorABx * vectorABx + vectorABy * vectorABy;
|
|
394
|
+
if (squaredLengthAB === 0) {
|
|
395
|
+
return Math.hypot(px - ax, py - ay);
|
|
396
|
+
}
|
|
397
|
+
let t = (vectorAPx * vectorABx + vectorAPy * vectorABy) / squaredLengthAB;
|
|
398
|
+
t = Math.max(0, Math.min(1, t));
|
|
399
|
+
const closestX = ax + t * vectorABx;
|
|
400
|
+
const closestY = ay + t * vectorABy;
|
|
401
|
+
return Math.hypot(px - closestX, py - closestY);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Поиск объекта по позиции и типу
|
|
406
|
+
* @param {Object} position - позиция {x, y}
|
|
407
|
+
* @param {string} type - тип объекта
|
|
408
|
+
* @returns {Object|null} найденный объект или null
|
|
409
|
+
*/
|
|
410
|
+
findObjectByPosition(position, type) {
|
|
411
|
+
for (const [objectId, pixiObject] of this.objects) {
|
|
412
|
+
if (!pixiObject || !pixiObject._mb) continue;
|
|
413
|
+
|
|
414
|
+
const childMeta = pixiObject._mb;
|
|
415
|
+
if (childMeta.type !== type) continue;
|
|
416
|
+
|
|
417
|
+
// Получаем границы объекта
|
|
418
|
+
const bounds = pixiObject.getBounds();
|
|
419
|
+
if (!bounds) continue;
|
|
420
|
+
|
|
421
|
+
// Проверяем, находится ли позиция в пределах объекта
|
|
422
|
+
if (bounds.x <= position.x && position.x <= bounds.x + bounds.width &&
|
|
423
|
+
bounds.y <= position.y && position.y <= bounds.y + bounds.height) {
|
|
424
|
+
return {
|
|
425
|
+
id: objectId,
|
|
426
|
+
type: childMeta.type,
|
|
427
|
+
position: { x: pixiObject.x, y: pixiObject.y },
|
|
428
|
+
size: { width: bounds.width, height: bounds.height }
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
destroy() {
|
|
437
|
+
this.app.destroy(true);
|
|
438
|
+
}
|
|
439
|
+
}
|