@sequent-org/moodboard 1.2.119 → 1.3.1

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 (122) hide show
  1. package/package.json +11 -1
  2. package/src/assets/icons/rotate-icon.svg +1 -1
  3. package/src/core/HistoryManager.js +16 -16
  4. package/src/core/KeyboardManager.js +48 -539
  5. package/src/core/PixiEngine.js +9 -9
  6. package/src/core/SaveManager.js +56 -31
  7. package/src/core/bootstrap/CoreInitializer.js +65 -0
  8. package/src/core/commands/DeleteObjectCommand.js +8 -0
  9. package/src/core/commands/GroupDeleteCommand.js +75 -0
  10. package/src/core/commands/GroupRotateCommand.js +6 -0
  11. package/src/core/commands/UpdateContentCommand.js +52 -0
  12. package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
  13. package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
  14. package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
  15. package/src/core/commands/UpdateTextStyleCommand.js +90 -0
  16. package/src/core/commands/index.js +6 -0
  17. package/src/core/events/Events.js +6 -0
  18. package/src/core/flows/ClipboardFlow.js +553 -0
  19. package/src/core/flows/LayerAndViewportFlow.js +283 -0
  20. package/src/core/flows/ObjectLifecycleFlow.js +336 -0
  21. package/src/core/flows/SaveFlow.js +34 -0
  22. package/src/core/flows/TransformFlow.js +277 -0
  23. package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
  24. package/src/core/index.js +41 -1773
  25. package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
  26. package/src/core/keyboard/KeyboardContextGuards.js +35 -0
  27. package/src/core/keyboard/KeyboardEventRouter.js +92 -0
  28. package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
  29. package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
  30. package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
  31. package/src/core/rendering/ObjectRenderer.js +3 -7
  32. package/src/grid/BaseGrid.js +26 -0
  33. package/src/grid/CrossGrid.js +7 -6
  34. package/src/grid/DotGrid.js +89 -33
  35. package/src/grid/DotGridZoomPhases.js +42 -0
  36. package/src/grid/LineGrid.js +22 -21
  37. package/src/moodboard/MoodBoard.js +31 -532
  38. package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
  39. package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
  40. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
  41. package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
  42. package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
  43. package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
  44. package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
  45. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
  46. package/src/objects/FileObject.js +17 -6
  47. package/src/objects/FrameObject.js +50 -10
  48. package/src/objects/NoteObject.js +5 -4
  49. package/src/services/BoardService.js +42 -2
  50. package/src/services/FrameService.js +83 -42
  51. package/src/services/ResizePolicyService.js +152 -0
  52. package/src/services/SettingsApplier.js +7 -2
  53. package/src/services/ZoomPanController.js +35 -9
  54. package/src/tools/ToolManager.js +30 -537
  55. package/src/tools/board-tools/PanTool.js +5 -11
  56. package/src/tools/manager/ToolActivationController.js +49 -0
  57. package/src/tools/manager/ToolEventRouter.js +396 -0
  58. package/src/tools/manager/ToolManagerGuards.js +33 -0
  59. package/src/tools/manager/ToolManagerLifecycle.js +110 -0
  60. package/src/tools/manager/ToolRegistry.js +33 -0
  61. package/src/tools/object-tools/DrawingTool.js +48 -14
  62. package/src/tools/object-tools/PlacementTool.js +50 -1049
  63. package/src/tools/object-tools/PlacementToolV2.js +88 -0
  64. package/src/tools/object-tools/SelectTool.js +174 -2681
  65. package/src/tools/object-tools/placement/GhostController.js +504 -0
  66. package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
  67. package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
  68. package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
  69. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
  70. package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
  71. package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
  72. package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
  73. package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
  74. package/src/tools/object-tools/selection/CursorController.js +78 -0
  75. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
  76. package/src/tools/object-tools/selection/HitTestService.js +102 -0
  77. package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
  78. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
  79. package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
  80. package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
  81. package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
  82. package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
  83. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
  84. package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
  85. package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
  86. package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
  87. package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
  88. package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
  89. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
  90. package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
  91. package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
  92. package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
  93. package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
  94. package/src/ui/FilePropertiesPanel.js +61 -32
  95. package/src/ui/FramePropertiesPanel.js +176 -101
  96. package/src/ui/HtmlHandlesLayer.js +121 -999
  97. package/src/ui/MapPanel.js +12 -7
  98. package/src/ui/NotePropertiesPanel.js +17 -2
  99. package/src/ui/TextPropertiesPanel.js +124 -738
  100. package/src/ui/Toolbar.js +82 -1181
  101. package/src/ui/Topbar.js +23 -25
  102. package/src/ui/ZoomPanel.js +16 -5
  103. package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
  104. package/src/ui/handles/HandlesDomRenderer.js +278 -0
  105. package/src/ui/handles/HandlesEventBridge.js +102 -0
  106. package/src/ui/handles/HandlesInteractionController.js +772 -0
  107. package/src/ui/handles/HandlesPositioningService.js +206 -0
  108. package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
  109. package/src/ui/styles/toolbar.css +2 -0
  110. package/src/ui/styles/workspace.css +13 -6
  111. package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
  112. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
  113. package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
  114. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
  115. package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
  116. package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
  117. package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
  118. package/src/ui/toolbar/ToolbarPopupsController.js +665 -0
  119. package/src/ui/toolbar/ToolbarRenderer.js +97 -0
  120. package/src/ui/toolbar/ToolbarStateController.js +79 -0
  121. package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
  122. package/src/utils/emojiLoaderNoBundler.js +1 -1
package/src/ui/Topbar.js CHANGED
@@ -40,15 +40,15 @@ export class Topbar {
40
40
  // не подсвечиваем дефолт до прихода актуального типа из ядра
41
41
 
42
42
  // Синхронизация активного состояния по событию из ядра
43
- this.eventBus.on(Events.UI.GridCurrent, ({ type }) => {
44
- this.setActive(type);
45
- });
43
+ this._onGridCurrent = ({ type }) => this.setActive(type);
44
+ this.eventBus.on(Events.UI.GridCurrent, this._onGridCurrent);
46
45
 
47
46
  // Обновляем цвет кнопки "краски" при выборе цвета фона
48
- this.eventBus.on(Events.UI.PaintPick, ({ btnHex }) => {
47
+ this._onPaintPick = ({ btnHex }) => {
49
48
  if (!btnHex) return;
50
49
  this.setPaintButtonHex(btnHex);
51
- });
50
+ };
51
+ this.eventBus.on(Events.UI.PaintPick, this._onPaintPick);
52
52
 
53
53
  // Инициализация цвета кнопки "краска" из настроек (или фона рендерера)
54
54
  try {
@@ -254,18 +254,6 @@ export class Topbar {
254
254
  const colors = this._palette.map(c => ({ id: c.id, name: c.name, hex: c.btnHex, board: c.board }));
255
255
  const grid = document.createElement('div');
256
256
  grid.className = 'moodboard-topbar__paint-grid';
257
- const darken = (hex, amount = 0.25) => {
258
- try {
259
- const h = hex.replace('#','');
260
- const r = parseInt(h.substring(0,2), 16);
261
- const g = parseInt(h.substring(2,4), 16);
262
- const b = parseInt(h.substring(4,6), 16);
263
- const dr = Math.max(0, Math.min(255, Math.round(r * (1 - amount))));
264
- const dg = Math.max(0, Math.min(255, Math.round(g * (1 - amount))));
265
- const db = Math.max(0, Math.min(255, Math.round(b * (1 - amount))));
266
- return `#${dr.toString(16).padStart(2,'0')}${dg.toString(16).padStart(2,'0')}${db.toString(16).padStart(2,'0')}`;
267
- } catch (_) { return hex; }
268
- };
269
257
 
270
258
  colors.forEach(c => {
271
259
  const b = document.createElement('button');
@@ -274,7 +262,7 @@ export class Topbar {
274
262
  b.title = `Цвет ${c.id}`;
275
263
  b.style.background = c.hex;
276
264
  // Тёмная обводка того же цвета
277
- b.style.borderColor = darken(c.hex, 0.35);
265
+ b.style.borderColor = this._darkenHex(c.hex, 0.35);
278
266
  // Цвет галочки — чёрный для максимальной видимости
279
267
  b.style.color = '#111';
280
268
  // Для надёжного сравнения — сохраняем оба значения в dataset
@@ -326,22 +314,32 @@ export class Topbar {
326
314
  this.element.appendChild(pop);
327
315
  this._paintPopover = pop;
328
316
 
329
- // закрытие по клику вне
330
- const onDocClick = (ev) => {
317
+ // закрытие по клику вне (сохраняем ссылку для cleanup при destroy)
318
+ this._onDocClickOutside = (ev) => {
331
319
  if (!pop.contains(ev.target) && ev.target !== anchorBtn) {
332
320
  pop.remove();
333
321
  this._paintPopover = null;
334
- document.removeEventListener('click', onDocClick, true);
322
+ document.removeEventListener('click', this._onDocClickOutside, true);
335
323
  }
336
324
  };
337
- setTimeout(() => document.addEventListener('click', onDocClick, true), 0);
325
+ setTimeout(() => document.addEventListener('click', this._onDocClickOutside, true), 0);
338
326
  }
339
327
 
340
328
  destroy() {
341
- if (this.element) {
342
- this.element.remove();
343
- this.element = null;
329
+ if (!this.element) return;
330
+
331
+ if (this._paintPopover) {
332
+ this._paintPopover.remove();
333
+ this._paintPopover = null;
334
+ document.removeEventListener('click', this._onDocClickOutside, true);
344
335
  }
336
+
337
+ if (this._onGridCurrent) this.eventBus.off(Events.UI.GridCurrent, this._onGridCurrent);
338
+ if (this._onPaintPick) this.eventBus.off(Events.UI.PaintPick, this._onPaintPick);
339
+
340
+ this.element.remove();
341
+ this.element = null;
342
+ this._paintBtn = null;
345
343
  }
346
344
  }
347
345
 
@@ -72,17 +72,18 @@ export class ZoomPanel {
72
72
  }
73
73
  });
74
74
 
75
- document.addEventListener('mousedown', (e) => {
75
+ this._onDocMouseDown = (e) => {
76
76
  if (!this.menuEl) return;
77
- // ИСПРАВЛЕНИЕ: Защита от null элементов
78
77
  if (!this.element || !e.target) return;
79
78
  if (this.element.contains(e.target)) return;
80
79
  this.hideMenu();
81
- });
80
+ };
81
+ document.addEventListener('mousedown', this._onDocMouseDown);
82
82
 
83
- this.eventBus.on(Events.UI.ZoomPercent, ({ percentage }) => {
83
+ this._onZoomPercent = ({ percentage }) => {
84
84
  if (this.valueEl) this.valueEl.textContent = `${percentage}%`;
85
- });
85
+ };
86
+ this.eventBus.on(Events.UI.ZoomPercent, this._onZoomPercent);
86
87
  }
87
88
 
88
89
  showMenu() {
@@ -115,9 +116,19 @@ export class ZoomPanel {
115
116
  }
116
117
 
117
118
  destroy() {
119
+ if (this._onDocMouseDown) {
120
+ document.removeEventListener('mousedown', this._onDocMouseDown);
121
+ this._onDocMouseDown = null;
122
+ }
123
+ if (this._onZoomPercent) {
124
+ this.eventBus.off(Events.UI.ZoomPercent, this._onZoomPercent);
125
+ this._onZoomPercent = null;
126
+ }
127
+ this.hideMenu();
118
128
  if (this.element) this.element.remove();
119
129
  this.element = null;
120
130
  this.labelEl = null;
131
+ this.valueEl = null;
121
132
  }
122
133
  }
123
134
 
@@ -0,0 +1,29 @@
1
+ export class GroupSelectionHandlesController {
2
+ constructor(host) {
3
+ this.host = host;
4
+ }
5
+
6
+ renderForSelection(ids) {
7
+ const preview = this.host._groupRotationPreview;
8
+ if (preview && Array.isArray(preview.ids) && preview.ids.length === ids.length) {
9
+ const hasSameSelection = ids.every((id) => preview.ids.includes(id));
10
+ if (hasSameSelection && preview.startBounds) {
11
+ this.host._showBounds({
12
+ x: preview.center.x - preview.startBounds.width / 2,
13
+ y: preview.center.y - preview.startBounds.height / 2,
14
+ width: preview.startBounds.width,
15
+ height: preview.startBounds.height,
16
+ }, '__group__', {
17
+ rotation: preview.angle || 0,
18
+ });
19
+ return;
20
+ }
21
+ }
22
+ const worldBounds = this.host.positioningService.getGroupSelectionWorldBounds(ids);
23
+ if (!worldBounds) {
24
+ this.host.hide();
25
+ return;
26
+ }
27
+ this.host._showBounds(worldBounds, '__group__');
28
+ }
29
+ }
@@ -0,0 +1,278 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+ import { createRotatedResizeCursor } from '../../tools/object-tools/selection/CursorController.js';
3
+
4
+ const HANDLES_ACCENT_COLOR = '#80D8FF';
5
+
6
+ export class HandlesDomRenderer {
7
+ constructor(host, rotateIconSvg) {
8
+ this.host = host;
9
+ this.rotateIconSvg = rotateIconSvg;
10
+ }
11
+
12
+ setHandlesVisibility(show) {
13
+ if (!this.host.layer) return;
14
+ const box = this.host.layer.querySelector('.mb-handles-box');
15
+ if (!box) return;
16
+
17
+ box.querySelectorAll('[data-dir]').forEach((el) => {
18
+ el.style.display = show ? '' : 'none';
19
+ });
20
+ box.querySelectorAll('[data-edge]').forEach((el) => {
21
+ el.style.display = show ? '' : 'none';
22
+ });
23
+
24
+ const rot = box.querySelector('[data-handle="rotate"]');
25
+ if (rot) rot.style.display = show ? '' : 'none';
26
+ if (show && !box.querySelector('[data-dir]')) {
27
+ this.host.update();
28
+ }
29
+ }
30
+
31
+ showBounds(worldBounds, id, options = {}) {
32
+ if (!this.host.layer) return;
33
+
34
+ const cssRect = this.host.positioningService.worldBoundsToCssRect(worldBounds);
35
+
36
+ let isFileTarget = false;
37
+ let isFrameTarget = false;
38
+ if (id !== '__group__') {
39
+ const req = { objectId: id, pixiObject: null };
40
+ this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
41
+ const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
42
+ isFileTarget = mbType === 'file';
43
+ isFrameTarget = mbType === 'frame';
44
+ }
45
+
46
+ const left = cssRect.left;
47
+ const top = cssRect.top;
48
+ const width = cssRect.width;
49
+ const height = cssRect.height;
50
+
51
+ this.host.layer.innerHTML = '';
52
+ const box = document.createElement('div');
53
+ box.className = 'mb-handles-box';
54
+
55
+ let rotation = options.rotation ?? 0;
56
+ if (id !== '__group__') {
57
+ const rotationData = { objectId: id, rotation: 0 };
58
+ this.host.eventBus.emit(Events.Tool.GetObjectRotation, rotationData);
59
+ rotation = rotationData.rotation || 0;
60
+ }
61
+
62
+ Object.assign(box.style, {
63
+ position: 'absolute', left: `${left}px`, top: `${top}px`,
64
+ width: `${width}px`, height: `${height}px`,
65
+ outline: `2px solid ${HANDLES_ACCENT_COLOR}`, outlineOffset: '0', borderRadius: '3px', boxSizing: 'border-box', pointerEvents: 'none',
66
+ transformOrigin: 'center center',
67
+ transform: `rotate(${rotation}deg)`,
68
+ });
69
+ this.host.layer.appendChild(box);
70
+ if (this.host._handlesSuppressed) {
71
+ this.host.visible = true;
72
+ return;
73
+ }
74
+
75
+ const mkCorner = (dir, x, y) => {
76
+ const cursor = createRotatedResizeCursor(dir, rotation);
77
+ const h = document.createElement('div');
78
+ h.dataset.dir = dir;
79
+ h.dataset.id = id;
80
+ h.className = 'mb-handle';
81
+ h.style.pointerEvents = isFileTarget ? 'none' : 'auto';
82
+ h.style.cursor = cursor;
83
+ h.style.left = `${x - 6}px`;
84
+ h.style.top = `${y - 6}px`;
85
+ h.style.display = isFileTarget ? 'none' : 'block';
86
+
87
+ const inner = document.createElement('div');
88
+ inner.className = 'mb-handle-inner';
89
+ h.appendChild(inner);
90
+
91
+ h.addEventListener('mouseenter', () => {
92
+ h.style.background = HANDLES_ACCENT_COLOR;
93
+ h.style.borderColor = HANDLES_ACCENT_COLOR;
94
+ h.style.cursor = cursor;
95
+ });
96
+ h.addEventListener('mouseleave', () => {
97
+ h.style.background = HANDLES_ACCENT_COLOR;
98
+ h.style.borderColor = HANDLES_ACCENT_COLOR;
99
+ });
100
+
101
+ if (!isFileTarget) {
102
+ h.addEventListener('mousedown', (e) => this.host._onHandleDown(e, box));
103
+ }
104
+
105
+ box.appendChild(h);
106
+ };
107
+
108
+ const x0 = 0;
109
+ const y0 = 0;
110
+ const x1 = width;
111
+ const y1 = height;
112
+ mkCorner('nw', x0, y0);
113
+ mkCorner('ne', x1, y0);
114
+ mkCorner('se', x1, y1);
115
+ mkCorner('sw', x0, y1);
116
+
117
+ const edgeSize = 10;
118
+ const makeEdge = (name, style, cursorHandleType) => {
119
+ const cursor = createRotatedResizeCursor(cursorHandleType, rotation);
120
+ const e = document.createElement('div');
121
+ e.dataset.edge = name;
122
+ e.dataset.id = id;
123
+ e.className = 'mb-edge';
124
+ Object.assign(e.style, style, {
125
+ pointerEvents: isFileTarget ? 'none' : 'auto',
126
+ cursor,
127
+ display: isFileTarget ? 'none' : 'block',
128
+ });
129
+ if (!isFileTarget) {
130
+ e.addEventListener('mousedown', (evt) => this.host._onEdgeResizeDown(evt));
131
+ }
132
+ box.appendChild(e);
133
+ };
134
+
135
+ const cornerGap = 20;
136
+ makeEdge('top', {
137
+ left: `${cornerGap}px`,
138
+ top: `-${edgeSize / 2}px`,
139
+ width: `${Math.max(0, width - 2 * cornerGap)}px`,
140
+ height: `${edgeSize}px`,
141
+ }, 'n');
142
+
143
+ makeEdge('bottom', {
144
+ left: `${cornerGap}px`,
145
+ top: `${height - edgeSize / 2}px`,
146
+ width: `${Math.max(0, width - 2 * cornerGap)}px`,
147
+ height: `${edgeSize}px`,
148
+ }, 's');
149
+
150
+ makeEdge('left', {
151
+ left: `-${edgeSize / 2}px`,
152
+ top: `${cornerGap}px`,
153
+ width: `${edgeSize}px`,
154
+ height: `${Math.max(0, height - 2 * cornerGap)}px`,
155
+ }, 'w');
156
+
157
+ makeEdge('right', {
158
+ left: `${width - edgeSize / 2}px`,
159
+ top: `${cornerGap}px`,
160
+ width: `${edgeSize}px`,
161
+ height: `${Math.max(0, height - 2 * cornerGap)}px`,
162
+ }, 'e');
163
+
164
+ const rotateHandle = document.createElement('div');
165
+ rotateHandle.dataset.handle = 'rotate';
166
+ rotateHandle.dataset.id = id;
167
+ if (isFileTarget || isFrameTarget) {
168
+ Object.assign(rotateHandle.style, { display: 'none', pointerEvents: 'none' });
169
+ } else {
170
+ rotateHandle.className = 'mb-rotate-handle';
171
+ const d = 38;
172
+ const L = Math.max(1, Math.hypot(width, height));
173
+ const centerX = -(width / L) * d;
174
+ const centerY = height + (height / L) * d;
175
+ rotateHandle.style.left = `${Math.round(centerX)}px`;
176
+ rotateHandle.style.top = `${Math.round(centerY - 10)}px`;
177
+ rotateHandle.innerHTML = this.rotateIconSvg;
178
+ const svgEl = rotateHandle.querySelector('svg');
179
+ if (svgEl) {
180
+ svgEl.style.width = '100%';
181
+ svgEl.style.height = '100%';
182
+ svgEl.style.display = 'block';
183
+ }
184
+ rotateHandle.addEventListener('mousedown', (e) => this.host._onRotateHandleDown(e, box));
185
+ }
186
+ box.appendChild(rotateHandle);
187
+
188
+ this.host.visible = true;
189
+ this.host.target = { type: id === '__group__' ? 'group' : 'single', id, bounds: worldBounds };
190
+ }
191
+
192
+ repositionBoxChildren(box) {
193
+ const width = parseFloat(box.style.width);
194
+ const height = parseFloat(box.style.height);
195
+ const cx = width / 2;
196
+ const cy = height / 2;
197
+
198
+ box.querySelectorAll('[data-dir]').forEach((h) => {
199
+ const dir = h.dataset.dir;
200
+ switch (dir) {
201
+ case 'nw':
202
+ h.style.left = `${-6}px`;
203
+ h.style.top = `${-6}px`;
204
+ break;
205
+ case 'ne':
206
+ h.style.left = `${Math.max(-6, width - 6)}px`;
207
+ h.style.top = `${-6}px`;
208
+ break;
209
+ case 'se':
210
+ h.style.left = `${Math.max(-6, width - 6)}px`;
211
+ h.style.top = `${Math.max(-6, height - 6)}px`;
212
+ break;
213
+ case 'sw':
214
+ h.style.left = `${-6}px`;
215
+ h.style.top = `${Math.max(-6, height - 6)}px`;
216
+ break;
217
+ case 'n':
218
+ h.style.left = `${cx - 6}px`;
219
+ h.style.top = `${-6}px`;
220
+ break;
221
+ case 'e':
222
+ h.style.left = `${Math.max(-6, width - 6)}px`;
223
+ h.style.top = `${cy - 6}px`;
224
+ break;
225
+ case 's':
226
+ h.style.left = `${cx - 6}px`;
227
+ h.style.top = `${Math.max(-6, height - 6)}px`;
228
+ break;
229
+ case 'w':
230
+ h.style.left = `${-6}px`;
231
+ h.style.top = `${cy - 6}px`;
232
+ break;
233
+ }
234
+ });
235
+
236
+ const edgeSize = 10;
237
+ const cornerGap = 20;
238
+ const top = box.querySelector('[data-edge="top"]');
239
+ const bottom = box.querySelector('[data-edge="bottom"]');
240
+ const left = box.querySelector('[data-edge="left"]');
241
+ const right = box.querySelector('[data-edge="right"]');
242
+
243
+ if (top) Object.assign(top.style, {
244
+ left: `${cornerGap}px`,
245
+ top: `-${edgeSize / 2}px`,
246
+ width: `${Math.max(0, width - 2 * cornerGap)}px`,
247
+ height: `${edgeSize}px`,
248
+ });
249
+ if (bottom) Object.assign(bottom.style, {
250
+ left: `${cornerGap}px`,
251
+ top: `${height - edgeSize / 2}px`,
252
+ width: `${Math.max(0, width - 2 * cornerGap)}px`,
253
+ height: `${edgeSize}px`,
254
+ });
255
+ if (left) Object.assign(left.style, {
256
+ left: `-${edgeSize / 2}px`,
257
+ top: `${cornerGap}px`,
258
+ width: `${edgeSize}px`,
259
+ height: `${Math.max(0, height - 2 * cornerGap)}px`,
260
+ });
261
+ if (right) Object.assign(right.style, {
262
+ left: `${width - edgeSize / 2}px`,
263
+ top: `${cornerGap}px`,
264
+ width: `${edgeSize}px`,
265
+ height: `${Math.max(0, height - 2 * cornerGap)}px`,
266
+ });
267
+
268
+ const rotateHandle = box.querySelector('[data-handle="rotate"]');
269
+ if (rotateHandle) {
270
+ const d = 20;
271
+ const L = Math.max(1, Math.hypot(width, height));
272
+ const centerX = -(width / L) * d;
273
+ const centerY = height + (height / L) * d;
274
+ rotateHandle.style.left = `${Math.round(centerX - 10)}px`;
275
+ rotateHandle.style.top = `${Math.round(centerY - 10)}px`;
276
+ }
277
+ }
278
+ }
@@ -0,0 +1,102 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+
3
+ export class HandlesEventBridge {
4
+ constructor(host) {
5
+ this.host = host;
6
+ this.subscriptions = [];
7
+ this.isAttached = false;
8
+ }
9
+
10
+ attach() {
11
+ if (this.isAttached) return;
12
+ this.isAttached = true;
13
+
14
+ const bindings = [
15
+ [Events.Tool.SelectionAdd, () => this.host.update()],
16
+ [Events.Tool.SelectionRemove, () => this.host.update()],
17
+ [Events.Tool.SelectionClear, () => {
18
+ this.host._endGroupRotationPreview();
19
+ this.host.hide();
20
+ }],
21
+ [Events.Tool.DragUpdate, () => this.host.update()],
22
+ [Events.Object.Deleted, (data) => {
23
+ const objectId = data?.objectId || data;
24
+ console.log('🗑️ HtmlHandlesLayer: получено событие удаления:', data, 'objectId:', objectId);
25
+ this.host.hide();
26
+ this.host.layer.innerHTML = '';
27
+ setTimeout(() => {
28
+ this.host.update();
29
+ }, 10);
30
+ }],
31
+ [Events.Tool.DragStart, () => { this.host._handlesSuppressed = true; this.host._setHandlesVisibility(false); }],
32
+ [Events.Tool.DragEnd, () => { this.host._handlesSuppressed = false; this.host._setHandlesVisibility(true); }],
33
+ [Events.Tool.ResizeUpdate, () => this.host.update()],
34
+ [Events.Tool.ResizeStart, () => { this.host._handlesSuppressed = true; this.host._setHandlesVisibility(false); }],
35
+ [Events.Tool.ResizeEnd, () => { this.host._handlesSuppressed = false; this.host._setHandlesVisibility(true); }],
36
+ [Events.Tool.RotateUpdate, () => this.host.update()],
37
+ [Events.Tool.RotateStart, () => { this.host._handlesSuppressed = true; this.host._setHandlesVisibility(false); }],
38
+ [Events.Tool.RotateEnd, () => { this.host._handlesSuppressed = false; this.host._setHandlesVisibility(true); }],
39
+ [Events.Tool.GroupDragUpdate, () => {
40
+ this.host._syncGroupRotationPreviewTranslation();
41
+ this.host.update();
42
+ }],
43
+ [Events.Tool.GroupDragStart, () => { this.host._handlesSuppressed = true; this.host._setHandlesVisibility(false); }],
44
+ [Events.Tool.GroupDragEnd, () => { this.host._handlesSuppressed = false; this.host._setHandlesVisibility(true); }],
45
+ [Events.Tool.GroupResizeUpdate, (data) => {
46
+ this.host._updateGroupResizePreview(data);
47
+ this.host.update();
48
+ }],
49
+ [Events.Tool.GroupResizeStart, (data) => {
50
+ this.host._startGroupResizePreview(data);
51
+ this.host._handlesSuppressed = true;
52
+ this.host._setHandlesVisibility(false);
53
+ }],
54
+ [Events.Tool.GroupResizeEnd, () => {
55
+ this.host._finishGroupResizePreview();
56
+ this.host._handlesSuppressed = false;
57
+ this.host._setHandlesVisibility(true);
58
+ }],
59
+ [Events.Tool.GroupRotateUpdate, (data) => {
60
+ this.host._updateGroupRotationPreview(data);
61
+ this.host.update();
62
+ }],
63
+ [Events.Tool.GroupRotateStart, (data) => {
64
+ this.host._startGroupRotationPreview(data);
65
+ this.host._handlesSuppressed = true;
66
+ this.host._setHandlesVisibility(false);
67
+ }],
68
+ [Events.Tool.GroupRotateEnd, () => {
69
+ this.host._finishGroupRotationPreview();
70
+ this.host._handlesSuppressed = false;
71
+ this.host._setHandlesVisibility(true);
72
+ this.host.update();
73
+ }],
74
+ [Events.UI.ZoomPercent, () => this.host.update()],
75
+ [Events.Tool.PanUpdate, () => this.host.update()],
76
+ [Events.History.Changed, (data) => {
77
+ if (data?.lastUndone || data?.lastRedone) {
78
+ this.host._endGroupRotationPreview();
79
+ this.host.update();
80
+ }
81
+ }],
82
+ ];
83
+
84
+ bindings.forEach(([event, handler]) => {
85
+ this.host.eventBus.on(event, handler);
86
+ this.subscriptions.push([event, handler]);
87
+ });
88
+ }
89
+
90
+ detach() {
91
+ if (!this.isAttached) return;
92
+ this.isAttached = false;
93
+ if (typeof this.host.eventBus?.off !== 'function') {
94
+ this.subscriptions = [];
95
+ return;
96
+ }
97
+ this.subscriptions.forEach(([event, handler]) => {
98
+ this.host.eventBus.off(event, handler);
99
+ });
100
+ this.subscriptions = [];
101
+ }
102
+ }