@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,404 @@
1
+ import { BaseTool } from '../BaseTool.js';
2
+ import { Events } from '../../core/events/Events.js';
3
+ import * as PIXI from 'pixi.js';
4
+
5
+ /**
6
+ * Инструмент рисования (карандаш)
7
+ */
8
+ export class DrawingTool extends BaseTool {
9
+ constructor(eventBus) {
10
+ super('draw', eventBus);
11
+ this.cursor = 'crosshair';
12
+ this.hotkey = 'd';
13
+
14
+ // Состояние рисования
15
+ this.isDrawing = false;
16
+ this.points = [];
17
+ this.tempGraphics = null;
18
+ this.app = null;
19
+ this.world = null;
20
+
21
+ // Параметры кисти по умолчанию
22
+ this.brush = {
23
+ color: 0x111827, // чёрный
24
+ width: 2,
25
+ mode: 'pencil'
26
+ };
27
+
28
+ // Набор уже удалённых объектов в текущем мазке ластика
29
+ this._eraserDeleted = new Set();
30
+ this._eraserIdleTimer = null;
31
+
32
+ // Подписка на изменения кисти (резерв на будущее)
33
+ if (this.eventBus) {
34
+ this.eventBus.on(Events.Draw.BrushSet, (data) => {
35
+ if (!data) return;
36
+ const patch = {};
37
+ if (typeof data.width === 'number') patch.width = data.width;
38
+ if (typeof data.color === 'number') patch.color = data.color;
39
+ if (typeof data.mode === 'string') patch.mode = data.mode;
40
+ this.brush = { ...this.brush, ...patch };
41
+ });
42
+ // Удаление объектов ластиком: кликаем по объекту — если попали, удаляем
43
+ this.eventBus.on(Events.Tool.HitTest, (data) => {
44
+ // Прокси для совместимости, не используем здесь
45
+ });
46
+ }
47
+ }
48
+
49
+ activate(app) {
50
+ super.activate();
51
+ this.app = app;
52
+ this.world = this._getWorldLayer();
53
+ // Кастомный курсор-карандаш (SVG)
54
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><path d='M4 20 L20 4 L28 12 L12 28 L4 28 Z' fill='black'/></svg>`;
55
+ const url = `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}") 0 16, crosshair`;
56
+ if (this.app && this.app.view) this.app.view.style.cursor = url;
57
+ }
58
+
59
+ deactivate() {
60
+ super.deactivate();
61
+ if (this.app && this.app.view) this.app.view.style.cursor = '';
62
+ this._finishAndCommit();
63
+ if (this._eraserIdleTimer) {
64
+ clearTimeout(this._eraserIdleTimer);
65
+ this._eraserIdleTimer = null;
66
+ }
67
+ this.app = null;
68
+ this.world = null;
69
+ }
70
+
71
+ onMouseDown(event) {
72
+ super.onMouseDown(event);
73
+ if (!this.world) this.world = this._getWorldLayer();
74
+ if (!this.world) return;
75
+
76
+ // Если режим ластика — попробуем удалить объект под курсором и показать временный след
77
+ if (this.brush.mode === 'eraser') {
78
+ const hitData = { x: event.x, y: event.y, result: null };
79
+ this.emit(Events.Tool.HitTest, hitData);
80
+ if (hitData.result && hitData.result.object) {
81
+ // Проверяем, что это именно нарисованный объект (drawing)
82
+ const pixReq = { objectId: hitData.result.object, pixiObject: null };
83
+ this.emit(Events.Tool.GetObjectPixi, pixReq);
84
+ const pixObj = pixReq.pixiObject;
85
+ const isDrawing = !!(pixObj && pixObj._mb && pixObj._mb.type === 'drawing');
86
+ if (isDrawing) {
87
+ this.eventBus.emit(Events.UI.ToolbarAction, { type: 'delete-object', id: hitData.result.object });
88
+ }
89
+ }
90
+ // Рисуем временный след ластика
91
+ this.isDrawing = true;
92
+ this.points = [];
93
+ this.tempGraphics = new PIXI.Graphics();
94
+ this.world.addChild(this.tempGraphics);
95
+ const p = this._toWorld(event.x, event.y);
96
+ this.points.push(p);
97
+ this._redrawTemporary();
98
+ return;
99
+ }
100
+
101
+ this.isDrawing = true;
102
+ this.points = [];
103
+ this.tempGraphics = new PIXI.Graphics();
104
+ this.world.addChild(this.tempGraphics);
105
+
106
+ const p = this._toWorld(event.x, event.y);
107
+ this.points.push(p);
108
+ this._redrawTemporary();
109
+ }
110
+
111
+ onMouseMove(event) {
112
+ super.onMouseMove(event);
113
+ if (!this.isDrawing) return;
114
+ const p = this._toWorld(event.x, event.y);
115
+ const prev = this.points[this.points.length - 1];
116
+ // Фильтр слишком частых точек
117
+ if (!prev || Math.hypot(p.x - prev.x, p.y - prev.y) >= 1) {
118
+ this.points.push(p);
119
+ // Ластик: при движении удаляем все фигуры, пересекаемые текущим сегментом
120
+ if (this.brush.mode === 'eraser' && prev) {
121
+ this._eraserSweep(prev, p);
122
+ }
123
+ this._redrawTemporary();
124
+ // Переход в режим круга при остановке курсора
125
+ if (this.brush.mode === 'eraser') {
126
+ if (this._eraserIdleTimer) clearTimeout(this._eraserIdleTimer);
127
+ this._eraserIdleTimer = setTimeout(() => {
128
+ if (!this.isDrawing || !this.tempGraphics) return;
129
+ // Форсируем перерисовку в режиме «стоит на месте»
130
+ this._redrawTemporary(true);
131
+ }, 150);
132
+ }
133
+ }
134
+ }
135
+
136
+ onMouseUp(event) {
137
+ super.onMouseUp(event);
138
+ this._finishAndCommit();
139
+ }
140
+
141
+ _finishAndCommit() {
142
+ if (!this.isDrawing) return;
143
+ this.isDrawing = false;
144
+
145
+ // Если ластик — чистим временную линию и выходим (удаление уже произошло onMouseDown)
146
+ if (this.brush.mode === 'eraser') {
147
+ if (this.tempGraphics && this.tempGraphics.parent) this.tempGraphics.parent.removeChild(this.tempGraphics);
148
+ this.tempGraphics?.destroy();
149
+ this.tempGraphics = null;
150
+ this.points = [];
151
+ this._eraserDeleted.clear();
152
+ return;
153
+ }
154
+
155
+ // Если слишком мало точек — удаляем временную графику
156
+ if (!this.points || this.points.length < 2) {
157
+ if (this.tempGraphics && this.tempGraphics.parent) this.tempGraphics.parent.removeChild(this.tempGraphics);
158
+ this.tempGraphics?.destroy();
159
+ this.tempGraphics = null;
160
+ this.points = [];
161
+ return;
162
+ }
163
+
164
+ // Рассчитываем bbox в мировых координатах
165
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
166
+ for (const pt of this.points) {
167
+ if (pt.x < minX) minX = pt.x;
168
+ if (pt.y < minY) minY = pt.y;
169
+ if (pt.x > maxX) maxX = pt.x;
170
+ if (pt.y > maxY) maxY = pt.y;
171
+ }
172
+ const width = Math.max(1, Math.round(maxX - minX));
173
+ const height = Math.max(1, Math.round(maxY - minY));
174
+
175
+ // Нормализуем точки относительно левого-верхнего угла
176
+ const normPoints = this.points.map(pt => ({ x: pt.x - minX, y: pt.y - minY }));
177
+
178
+ // Создаем объект типа drawing через существующий пайплайн
179
+ const position = { x: Math.round(minX), y: Math.round(minY) };
180
+ const properties = {
181
+ points: normPoints,
182
+ strokeColor: this.brush.color,
183
+ strokeWidth: this.brush.width,
184
+ mode: this.brush.mode,
185
+ // Базовые размеры для масштабирования при ресайзе
186
+ baseWidth: width,
187
+ baseHeight: height,
188
+ // Передаём стартовый размер, чтобы ядро установило width/height у объекта
189
+ width: width,
190
+ height: height
191
+ };
192
+
193
+ // Важно: отправляем глобальное событие без префикса tool:
194
+ this.eventBus.emit(Events.UI.ToolbarAction, { type: 'drawing', id: 'drawing', position, properties });
195
+
196
+ // Чистим временную графику
197
+ if (this.tempGraphics && this.tempGraphics.parent) this.tempGraphics.parent.removeChild(this.tempGraphics);
198
+ this.tempGraphics?.destroy();
199
+ this.tempGraphics = null;
200
+ this.points = [];
201
+ }
202
+
203
+ _redrawTemporary(forceCircle = false) {
204
+ if (!this.tempGraphics) return;
205
+ const g = this.tempGraphics;
206
+ g.clear();
207
+ const pts = this.points;
208
+ if (pts.length === 0) return;
209
+
210
+ // Особая визуализация для ластика: кружок под курсором + короткий «хвост»
211
+ if (this.brush.mode === 'eraser') {
212
+ const color = 0x6B7280; // слегка светлее серый
213
+ const maxAlpha = 0.6;
214
+ const radius = 7; // базовая толщина = radius*2
215
+ const tailMaxLen = 70; // чуть меньшая длина хвоста
216
+ const last = pts[pts.length - 1];
217
+
218
+ if (forceCircle || pts.length < 2) {
219
+ // Кружок под курсором, когда стоим на месте
220
+ g.beginFill(color, maxAlpha);
221
+ g.drawCircle(last.x, last.y, radius);
222
+ g.endFill();
223
+ } else {
224
+ // Формируем полилинию хвоста из последних точек так, чтобы суммарная длина ≤ tailMaxLen
225
+ const tailPoints = [last];
226
+ let acc = 0;
227
+ for (let i = pts.length - 2; i >= 0; i--) {
228
+ const a = pts[i + 1];
229
+ const b = pts[i];
230
+ const dl = Math.hypot(a.x - b.x, a.y - b.y);
231
+ acc += dl;
232
+ tailPoints.push(b);
233
+ if (acc >= tailMaxLen) break;
234
+ }
235
+ tailPoints.reverse(); // от старых к новым
236
+
237
+ // Пересэмплируем хвост равномерно для большей плотности (чтобы не было «бусинок»)
238
+ const targetStep = 3; // px между соседними сэмплами
239
+ const samples = [];
240
+ // Накопитель вдоль полилинии
241
+ let cursor = 0;
242
+ let segIdx = 0;
243
+ let segPos = 0; // позиция внутри сегмента
244
+ // Считаем длины сегментов
245
+ const segLens = [];
246
+ for (let i = 0; i < tailPoints.length - 1; i++) {
247
+ const a = tailPoints[i];
248
+ const b = tailPoints[i + 1];
249
+ segLens.push(Math.hypot(b.x - a.x, b.y - a.y));
250
+ }
251
+ const totalLen = segLens.reduce((s, v) => s + v, 0);
252
+ const sampleCount = Math.max(2, Math.floor(totalLen / targetStep));
253
+ // Добавляем первый
254
+ samples.push({ x: tailPoints[0].x, y: tailPoints[0].y });
255
+ for (let k = 1; k < sampleCount; k++) {
256
+ const dist = k * (totalLen / (sampleCount - 1));
257
+ // Ищем сегмент, в котором находится эта дистанция
258
+ let d = 0;
259
+ let idx = 0;
260
+ while (idx < segLens.length && d + segLens[idx] < dist) {
261
+ d += segLens[idx++];
262
+ }
263
+ if (idx >= segLens.length) {
264
+ samples.push({ x: tailPoints[tailPoints.length - 1].x, y: tailPoints[tailPoints.length - 1].y });
265
+ continue;
266
+ }
267
+ const a = tailPoints[idx];
268
+ const b = tailPoints[idx + 1];
269
+ const t = (dist - d) / (segLens[idx] || 1);
270
+ samples.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
271
+ }
272
+
273
+ // Рисуем одну непрерывную ломаную, нарезанную на короткие штрихи с плавным альфа-градиентом
274
+ const segCount = samples.length - 1;
275
+ for (let i = 0; i < segCount; i++) {
276
+ const a = samples[i];
277
+ const b = samples[i + 1];
278
+ const t = (i + 1) / segCount; // 0..1, ближе к концу — плотнее
279
+ const alpha = Math.max(0.03, Math.pow(t, 1.2) * maxAlpha);
280
+ g.lineStyle({ width: radius * 2, color, alpha, cap: 'round', join: 'round' });
281
+ g.moveTo(a.x, a.y);
282
+ g.lineTo(b.x, b.y);
283
+ }
284
+ }
285
+ return;
286
+ }
287
+
288
+ // Карандаш/маркер: сглаженная кривая
289
+ const alpha = this.brush.mode === 'marker' ? 0.6 : 1;
290
+ const lineWidth = this.brush.mode === 'marker' ? this.brush.width * 2 : this.brush.width;
291
+ g.lineStyle({ width: lineWidth, color: this.brush.color, alpha, cap: 'round', join: 'round', miterLimit: 2, alignment: 0.5 });
292
+ g.blendMode = this.brush.mode === 'marker' ? PIXI.BLEND_MODES.LIGHTEN : PIXI.BLEND_MODES.NORMAL;
293
+
294
+ if (pts.length < 3) {
295
+ g.moveTo(pts[0].x, pts[0].y);
296
+ for (let i = 1; i < pts.length; i++) g.lineTo(pts[i].x, pts[i].y);
297
+ return;
298
+ }
299
+ g.moveTo(pts[0].x, pts[0].y);
300
+ for (let i = 1; i < pts.length - 1; i++) {
301
+ const cx = pts[i].x;
302
+ const cy = pts[i].y;
303
+ const nx = pts[i + 1].x;
304
+ const ny = pts[i + 1].y;
305
+ const mx = (cx + nx) / 2;
306
+ const my = (cy + ny) / 2;
307
+ g.quadraticCurveTo(cx, cy, mx, my);
308
+ }
309
+ const pen = pts[pts.length - 2];
310
+ const last = pts[pts.length - 1];
311
+ g.quadraticCurveTo(pen.x, pen.y, last.x, last.y);
312
+ }
313
+
314
+ _toWorld(x, y) {
315
+ if (!this.world) return { x, y };
316
+ const global = new PIXI.Point(x, y);
317
+ const local = this.world.toLocal(global);
318
+ return { x: local.x, y: local.y };
319
+ }
320
+
321
+ _getWorldLayer() {
322
+ if (!this.app || !this.app.stage) return null;
323
+ const world = this.app.stage.getChildByName && this.app.stage.getChildByName('worldLayer');
324
+ return world || this.app.stage; // фолбэк на stage
325
+ }
326
+
327
+ // Удаляет все объекты, пересекаемые сегментом ластика prev→p
328
+ _eraserSweep(prev, p) {
329
+ const req = { objects: [] };
330
+ this.emit('get:all:objects', req);
331
+ const objects = req.objects || [];
332
+ // Радиус воздействия ластика (связан с отображаемой толщиной)
333
+ const radius = 8;
334
+ const segMinX = Math.min(prev.x, p.x) - radius;
335
+ const segMaxX = Math.max(prev.x, p.x) + radius;
336
+ const segMinY = Math.min(prev.y, p.y) - radius;
337
+ const segMaxY = Math.max(prev.y, p.y) + radius;
338
+
339
+ for (const item of objects) {
340
+ const id = item.id;
341
+ if (this._eraserDeleted.has(id)) continue;
342
+ const pixi = item.pixi;
343
+ if (!pixi) continue;
344
+
345
+ // Быстрая проверка по bbox объекта
346
+ const b = item.bounds;
347
+ if (!b) continue;
348
+ if (b.x > segMaxX || b.x + b.width < segMinX || b.y > segMaxY || b.y + b.height < segMinY) {
349
+ continue;
350
+ }
351
+
352
+ const meta = pixi._mb || {};
353
+ const type = meta.type;
354
+ let intersects = false;
355
+
356
+ if (type === 'drawing') {
357
+ const props = meta.properties || {};
358
+ const pts = Array.isArray(props.points) ? props.points : [];
359
+ if (pts.length >= 2) {
360
+ // Оценка масштабов
361
+ const baseW = props.baseWidth || 1;
362
+ const baseH = props.baseHeight || 1;
363
+ const scaleX = baseW ? (b.width / baseW) : 1;
364
+ const scaleY = baseH ? (b.height / baseH) : 1;
365
+ const eraserThresh = Math.max(6, (props.strokeWidth || 2) / 2 + radius);
366
+ // трансформируем сегмент ластика в локальные координаты фигуры
367
+ const localPrev = pixi.toLocal(new PIXI.Point(prev.x, prev.y));
368
+ const localCurr = pixi.toLocal(new PIXI.Point(p.x, p.y));
369
+ // Проверяем пересечение с каждым отрезком рисунка
370
+ for (let i = 0; i < pts.length - 1 && !intersects; i++) {
371
+ const ax = pts[i].x * scaleX;
372
+ const ay = pts[i].y * scaleY;
373
+ const bx = pts[i + 1].x * scaleX;
374
+ const by = pts[i + 1].y * scaleY;
375
+ const d = this._distancePointToSegment(localPrev.x, localPrev.y, ax, ay, bx, by);
376
+ const d2 = this._distancePointToSegment(localCurr.x, localCurr.y, ax, ay, bx, by);
377
+ if (Math.min(d, d2) <= eraserThresh) intersects = true;
378
+ }
379
+ }
380
+ }
381
+
382
+ if (intersects) {
383
+ this._eraserDeleted.add(id);
384
+ this.eventBus.emit(Events.UI.ToolbarAction, { type: 'delete-object', id });
385
+ }
386
+ }
387
+ }
388
+
389
+ _distancePointToSegment(px, py, ax, ay, bx, by) {
390
+ const abx = bx - ax;
391
+ const aby = by - ay;
392
+ const apx = px - ax;
393
+ const apy = py - ay;
394
+ const ab2 = abx * abx + aby * aby;
395
+ if (ab2 === 0) return Math.hypot(px - ax, py - ay);
396
+ let t = (apx * abx + apy * aby) / ab2;
397
+ t = Math.max(0, Math.min(1, t));
398
+ const cx = ax + t * abx;
399
+ const cy = ay + t * aby;
400
+ return Math.hypot(px - cx, py - cy);
401
+ }
402
+ }
403
+
404
+