@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.
Files changed (123) hide show
  1. package/package.json +44 -0
  2. package/src/assets/icons/README.md +105 -0
  3. package/src/assets/icons/attachments.svg +3 -0
  4. package/src/assets/icons/clear.svg +5 -0
  5. package/src/assets/icons/comments.svg +3 -0
  6. package/src/assets/icons/emoji.svg +6 -0
  7. package/src/assets/icons/frame.svg +3 -0
  8. package/src/assets/icons/image.svg +3 -0
  9. package/src/assets/icons/note.svg +3 -0
  10. package/src/assets/icons/pan.svg +3 -0
  11. package/src/assets/icons/pencil.svg +3 -0
  12. package/src/assets/icons/redo.svg +3 -0
  13. package/src/assets/icons/select.svg +9 -0
  14. package/src/assets/icons/shapes.svg +3 -0
  15. package/src/assets/icons/text-add.svg +3 -0
  16. package/src/assets/icons/topbar/README.md +39 -0
  17. package/src/assets/icons/topbar/grid-cross.svg +6 -0
  18. package/src/assets/icons/topbar/grid-dot.svg +3 -0
  19. package/src/assets/icons/topbar/grid-line.svg +3 -0
  20. package/src/assets/icons/topbar/grid-off.svg +3 -0
  21. package/src/assets/icons/topbar/paint.svg +3 -0
  22. package/src/assets/icons/undo.svg +3 -0
  23. package/src/core/ApiClient.js +309 -0
  24. package/src/core/EventBus.js +42 -0
  25. package/src/core/HistoryManager.js +261 -0
  26. package/src/core/KeyboardManager.js +710 -0
  27. package/src/core/PixiEngine.js +439 -0
  28. package/src/core/SaveManager.js +381 -0
  29. package/src/core/StateManager.js +64 -0
  30. package/src/core/commands/BaseCommand.js +68 -0
  31. package/src/core/commands/CopyObjectCommand.js +44 -0
  32. package/src/core/commands/CreateObjectCommand.js +46 -0
  33. package/src/core/commands/DeleteObjectCommand.js +146 -0
  34. package/src/core/commands/EditFileNameCommand.js +107 -0
  35. package/src/core/commands/GroupMoveCommand.js +47 -0
  36. package/src/core/commands/GroupReorderZCommand.js +74 -0
  37. package/src/core/commands/GroupResizeCommand.js +37 -0
  38. package/src/core/commands/GroupRotateCommand.js +41 -0
  39. package/src/core/commands/MoveObjectCommand.js +89 -0
  40. package/src/core/commands/PasteObjectCommand.js +103 -0
  41. package/src/core/commands/ReorderZCommand.js +45 -0
  42. package/src/core/commands/ResizeObjectCommand.js +135 -0
  43. package/src/core/commands/RotateObjectCommand.js +70 -0
  44. package/src/core/commands/index.js +14 -0
  45. package/src/core/events/Events.js +147 -0
  46. package/src/core/index.js +1632 -0
  47. package/src/core/rendering/GeometryUtils.js +89 -0
  48. package/src/core/rendering/HitTestManager.js +186 -0
  49. package/src/core/rendering/LayerManager.js +137 -0
  50. package/src/core/rendering/ObjectRenderer.js +363 -0
  51. package/src/core/rendering/PixiRenderer.js +140 -0
  52. package/src/core/rendering/index.js +9 -0
  53. package/src/grid/BaseGrid.js +164 -0
  54. package/src/grid/CrossGrid.js +75 -0
  55. package/src/grid/DotGrid.js +148 -0
  56. package/src/grid/GridFactory.js +173 -0
  57. package/src/grid/LineGrid.js +115 -0
  58. package/src/index.js +2 -0
  59. package/src/moodboard/ActionHandler.js +114 -0
  60. package/src/moodboard/DataManager.js +114 -0
  61. package/src/moodboard/MoodBoard.js +359 -0
  62. package/src/moodboard/WorkspaceManager.js +103 -0
  63. package/src/objects/BaseObject.js +1 -0
  64. package/src/objects/CommentObject.js +115 -0
  65. package/src/objects/DrawingObject.js +114 -0
  66. package/src/objects/EmojiObject.js +98 -0
  67. package/src/objects/FileObject.js +318 -0
  68. package/src/objects/FrameObject.js +127 -0
  69. package/src/objects/ImageObject.js +72 -0
  70. package/src/objects/NoteObject.js +227 -0
  71. package/src/objects/ObjectFactory.js +61 -0
  72. package/src/objects/ShapeObject.js +134 -0
  73. package/src/objects/StampObject.js +0 -0
  74. package/src/objects/StickerObject.js +0 -0
  75. package/src/objects/TextObject.js +123 -0
  76. package/src/services/BoardService.js +85 -0
  77. package/src/services/FileUploadService.js +398 -0
  78. package/src/services/FrameService.js +138 -0
  79. package/src/services/ImageUploadService.js +246 -0
  80. package/src/services/ZOrderManager.js +50 -0
  81. package/src/services/ZoomPanController.js +78 -0
  82. package/src/src.7z +0 -0
  83. package/src/src.zip +0 -0
  84. package/src/src2.zip +0 -0
  85. package/src/tools/AlignmentGuides.js +326 -0
  86. package/src/tools/BaseTool.js +257 -0
  87. package/src/tools/ResizeHandles.js +381 -0
  88. package/src/tools/ToolManager.js +580 -0
  89. package/src/tools/board-tools/PanTool.js +43 -0
  90. package/src/tools/board-tools/ZoomTool.js +393 -0
  91. package/src/tools/object-tools/DrawingTool.js +404 -0
  92. package/src/tools/object-tools/PlacementTool.js +1005 -0
  93. package/src/tools/object-tools/SelectTool.js +2183 -0
  94. package/src/tools/object-tools/TextTool.js +416 -0
  95. package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
  96. package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
  97. package/src/tools/object-tools/selection/GroupDragController.js +61 -0
  98. package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
  99. package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
  100. package/src/tools/object-tools/selection/HandlesSync.js +96 -0
  101. package/src/tools/object-tools/selection/ResizeController.js +68 -0
  102. package/src/tools/object-tools/selection/RotateController.js +58 -0
  103. package/src/tools/object-tools/selection/SelectionModel.js +42 -0
  104. package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
  105. package/src/ui/CommentPopover.js +187 -0
  106. package/src/ui/ContextMenu.js +340 -0
  107. package/src/ui/FilePropertiesPanel.js +298 -0
  108. package/src/ui/FramePropertiesPanel.js +462 -0
  109. package/src/ui/HtmlHandlesLayer.js +778 -0
  110. package/src/ui/HtmlTextLayer.js +279 -0
  111. package/src/ui/MapPanel.js +290 -0
  112. package/src/ui/NotePropertiesPanel.js +502 -0
  113. package/src/ui/SaveStatus.js +250 -0
  114. package/src/ui/TextPropertiesPanel.js +911 -0
  115. package/src/ui/Toolbar.js +1118 -0
  116. package/src/ui/Topbar.js +220 -0
  117. package/src/ui/ZoomPanel.js +116 -0
  118. package/src/ui/styles/workspace.css +854 -0
  119. package/src/utils/colors.js +0 -0
  120. package/src/utils/geometry.js +0 -0
  121. package/src/utils/iconLoader.js +270 -0
  122. package/src/utils/objectIdGenerator.js +17 -0
  123. 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
+ }