@sequent-org/moodboard 1.2.119 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +71 -1180
  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 +662 -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
@@ -0,0 +1,267 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../../core/events/Events.js';
3
+
4
+ export class PlacementInputRouter {
5
+ constructor(host) {
6
+ this.host = host;
7
+ }
8
+
9
+ startFrameDrawMode() {
10
+ this.host.cursor = 'crosshair';
11
+ if (this.host.app && this.host.app.view) this.host.app.view.style.cursor = this.host.cursor;
12
+ }
13
+
14
+ onMouseMove(event) {
15
+ const host = this.host;
16
+ if ((host.selectedFile || host.selectedImage || host.pending) && host.ghostContainer) {
17
+ if (host.app && host.app.view) {
18
+ host.app.view._lastMouseX = event.x;
19
+ host.app.view._lastMouseY = event.y;
20
+ }
21
+ const worldPoint = host._toWorld(event.offsetX, event.offsetY);
22
+ host.updateGhostPosition(worldPoint.x, worldPoint.y);
23
+ }
24
+ }
25
+
26
+ onFrameDrawMove(event) {
27
+ const host = this.host;
28
+ if (!host._frameDrawState || !host._frameDrawState.graphics) return;
29
+ const p = host._toWorld(event.offsetX, event.offsetY);
30
+ const x = Math.min(host._frameDrawState.startX, p.x);
31
+ const y = Math.min(host._frameDrawState.startY, p.y);
32
+ const w = Math.abs(p.x - host._frameDrawState.startX);
33
+ const h = Math.abs(p.y - host._frameDrawState.startY);
34
+ const g = host._frameDrawState.graphics;
35
+ g.clear();
36
+ const x0 = Math.floor(x) + 0.5;
37
+ const y0 = Math.floor(y) + 0.5;
38
+ const w0 = Math.max(1, Math.round(w));
39
+ const h0 = Math.max(1, Math.round(h));
40
+ g.lineStyle(1, 0x3B82F6, 1, 1);
41
+ g.beginFill(0xFFFFFF, 0.6);
42
+ g.drawRect(x0, y0, w0, h0);
43
+ g.endFill();
44
+ }
45
+
46
+ onFrameDrawUp(event) {
47
+ const host = this.host;
48
+ const g = host._frameDrawState?.graphics;
49
+ if (!host._frameDrawState || !g) return;
50
+ const p = host._toWorld(event.offsetX, event.offsetY);
51
+ const x = Math.min(host._frameDrawState.startX, p.x);
52
+ const y = Math.min(host._frameDrawState.startY, p.y);
53
+ const w = Math.abs(p.x - host._frameDrawState.startX);
54
+ const h = Math.abs(p.y - host._frameDrawState.startY);
55
+ if (g.parent) g.parent.removeChild(g);
56
+ g.destroy();
57
+ host._frameDrawState = null;
58
+ if (w >= 2 && h >= 2) {
59
+ host.payloadFactory.emitFrameDrawPlacement(x, y, w, h);
60
+ }
61
+ host.pending = null;
62
+ host.hideGhost();
63
+ if (host.app && host.app.view) {
64
+ host.app.view.removeEventListener('mousemove', host._onFrameDrawMoveBound);
65
+ host.app.view.style.cursor = '';
66
+ }
67
+ host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
68
+ }
69
+
70
+ onMouseDown(event) {
71
+ const host = this.host;
72
+ host.__baseOnMouseDown(event);
73
+
74
+ if (host.selectedFile) {
75
+ host.placeSelectedFile(event);
76
+ return;
77
+ }
78
+
79
+ if (host.selectedImage) {
80
+ host.placeSelectedImage(event);
81
+ return;
82
+ }
83
+
84
+ if (!host.pending) return;
85
+ if (host.pending.placeOnMouseUp) {
86
+ const onUp = (ev) => {
87
+ host.app.view.removeEventListener('mouseup', onUp);
88
+ const worldPoint = host._toWorld(ev.x, ev.y);
89
+ const position = {
90
+ x: Math.round(worldPoint.x - (host.pending.size?.width ?? 100) / 2),
91
+ y: Math.round(worldPoint.y - (host.pending.size?.height ?? 100) / 2)
92
+ };
93
+ const props = { ...(host.pending.properties || {}) };
94
+ host.payloadFactory.emitGenericPlacement(host.pending.type, position, props);
95
+ host.pending = null;
96
+ host.hideGhost();
97
+ host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
98
+ };
99
+ host.app.view.addEventListener('mouseup', onUp, { once: true });
100
+ return;
101
+ }
102
+ if (host.pending.type === 'frame-draw') {
103
+ const start = host._toWorld(event.x, event.y);
104
+ host._frameDrawState = { startX: start.x, startY: start.y, graphics: null };
105
+ if (host.world) {
106
+ const g = new PIXI.Graphics();
107
+ g.zIndex = 3000;
108
+ host.world.addChild(g);
109
+ host._frameDrawState.graphics = g;
110
+ }
111
+ host._onFrameDrawMoveBound = (ev) => host._onFrameDrawMove(ev);
112
+ host._onFrameDrawUpBound = (ev) => host._onFrameDrawUp(ev);
113
+ host.app.view.addEventListener('mousemove', host._onFrameDrawMoveBound);
114
+ host.app.view.addEventListener('mouseup', host._onFrameDrawUpBound, { once: true });
115
+ return;
116
+ }
117
+
118
+ const worldPoint = host._toWorld(event.x, event.y);
119
+ let position = {
120
+ x: Math.round(worldPoint.x - (host.pending.size?.width ?? 100) / 2),
121
+ y: Math.round(worldPoint.y - (host.pending.size?.height ?? 100) / 2)
122
+ };
123
+
124
+ let props = host.pending.properties || {};
125
+ const isTextWithEditing = host.pending.type === 'text' && props.editOnCreate;
126
+ const isImage = host.pending.type === 'image';
127
+ const isFile = host.pending.type === 'file';
128
+ const presetSize = {
129
+ width: (host.pending.size && host.pending.size.width) ? host.pending.size.width : (props.width || 200),
130
+ height: (host.pending.size && host.pending.size.height) ? host.pending.size.height : (props.height || 150),
131
+ };
132
+
133
+ if (isTextWithEditing) {
134
+ let worldForText = worldPoint;
135
+ try {
136
+ const app = host.app;
137
+ const view = app?.view;
138
+ const worldLayer = host.world || host._getWorldLayer();
139
+ if (view && view.parentElement && worldLayer && worldLayer.toLocal) {
140
+ const containerRect = view.parentElement.getBoundingClientRect();
141
+ const viewRect = view.getBoundingClientRect();
142
+ const offsetLeft = viewRect.left - containerRect.left;
143
+ const offsetTop = viewRect.top - containerRect.top;
144
+ const screenX = event.x - offsetLeft;
145
+ const screenY = event.y - offsetTop;
146
+ const globalPoint = new PIXI.Point(screenX, screenY);
147
+ const local = worldLayer.toLocal(globalPoint);
148
+ worldForText = { x: local.x, y: local.y };
149
+ }
150
+ console.log('🧭 Text click', {
151
+ cursor: { x: event.x, y: event.y },
152
+ world: { x: Math.round(worldForText.x), y: Math.round(worldForText.y) }
153
+ });
154
+ } catch (_) {}
155
+ position = {
156
+ x: Math.round(worldForText.x),
157
+ y: Math.round(worldForText.y)
158
+ };
159
+ const handleObjectCreated = (objectData) => {
160
+ if (objectData.type === 'text') {
161
+ host.eventBus.off('object:created', handleObjectCreated);
162
+ host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
163
+ setTimeout(() => {
164
+ host.eventBus.emit(Events.Tool.ObjectEdit, {
165
+ object: {
166
+ id: objectData.id,
167
+ type: 'text',
168
+ position: objectData.position,
169
+ properties: { fontSize: props.fontSize || 18, content: '' }
170
+ },
171
+ create: true
172
+ });
173
+ }, 50);
174
+ }
175
+ };
176
+
177
+ host.eventBus.on('object:created', handleObjectCreated);
178
+ host.payloadFactory.emitTextPlacement(position, props);
179
+ } else if (host.pending.type === 'frame') {
180
+ const width = props.width || presetSize.width || 200;
181
+ const height = props.height || presetSize.height || 300;
182
+ position = {
183
+ x: Math.round(worldPoint.x - width / 2),
184
+ y: Math.round(worldPoint.y - height / 2)
185
+ };
186
+ host.payloadFactory.emitFramePlacement(position, props, width, height);
187
+ } else if (isImage && props.selectFileOnPlace) {
188
+ const input = document.createElement('input');
189
+ input.type = 'file';
190
+ input.accept = 'image/*';
191
+ input.style.display = 'none';
192
+ document.body.appendChild(input);
193
+ input.addEventListener('change', async () => {
194
+ try {
195
+ const file = input.files && input.files[0];
196
+ if (!file) return;
197
+ try {
198
+ const uploadResult = await host.core.imageUploadService.uploadImage(file, file.name);
199
+ const natW = uploadResult.width || 1;
200
+ const natH = uploadResult.height || 1;
201
+ const targetW = 300;
202
+ const targetH = Math.max(1, Math.round(natH * (targetW / natW)));
203
+ host.payloadFactory.emitImageUploaded(position, uploadResult, targetW, targetH);
204
+ } catch (error) {
205
+ console.error('Ошибка загрузки изображения:', error);
206
+ alert('Ошибка загрузки изображения: ' + error.message);
207
+ }
208
+ } finally {
209
+ input.remove();
210
+ }
211
+ }, { once: true });
212
+ input.click();
213
+ } else if (isFile && props.selectFileOnPlace) {
214
+ const input = document.createElement('input');
215
+ input.type = 'file';
216
+ input.accept = '*/*';
217
+ input.style.display = 'none';
218
+ document.body.appendChild(input);
219
+ input.addEventListener('change', async () => {
220
+ try {
221
+ const file = input.files && input.files[0];
222
+ if (!file) return;
223
+
224
+ try {
225
+ const uploadResult = await host.core.fileUploadService.uploadFile(file, file.name);
226
+ host.payloadFactory.emitFileUploaded(position, uploadResult, props.width || 120, props.height || 140);
227
+ host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
228
+ } catch (uploadError) {
229
+ console.error('Ошибка загрузки файла на сервер:', uploadError);
230
+ const fileName = file.name;
231
+ const fileSize = file.size;
232
+ const mimeType = file.type;
233
+
234
+ host.payloadFactory.emitFileFallback(position, fileName, fileSize, mimeType, props.width || 120, props.height || 140);
235
+ host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
236
+ alert('Ошибка загрузки файла на сервер. Файл добавлен локально.');
237
+ }
238
+ } catch (error) {
239
+ console.error('Ошибка при выборе файла:', error);
240
+ alert('Ошибка при выборе файла: ' + error.message);
241
+ } finally {
242
+ input.remove();
243
+ }
244
+ }, { once: true });
245
+ input.click();
246
+ } else {
247
+ if (host.pending.type === 'note') {
248
+ const base = 250;
249
+ const noteW = (typeof props.width === 'number') ? props.width : base;
250
+ const noteH = (typeof props.height === 'number') ? props.height : base;
251
+ const side = Math.max(noteW, noteH);
252
+ props = { ...props, width: side, height: side };
253
+ position = {
254
+ x: Math.round(worldPoint.x - side / 2),
255
+ y: Math.round(worldPoint.y - side / 2)
256
+ };
257
+ }
258
+ host.payloadFactory.emitGenericPlacement(host.pending.type, position, props);
259
+ }
260
+
261
+ host.pending = null;
262
+ host.hideGhost();
263
+ if (!isTextWithEditing && !(isFile && props.selectFileOnPlace)) {
264
+ host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,111 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+
3
+ export class PlacementPayloadFactory {
4
+ constructor(host) {
5
+ this.host = host;
6
+ }
7
+
8
+ emitGenericPlacement(type, position, properties) {
9
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
10
+ type,
11
+ id: type,
12
+ position,
13
+ properties
14
+ });
15
+ }
16
+
17
+ emitFramePlacement(position, properties, width, height) {
18
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
19
+ type: 'frame',
20
+ id: 'frame',
21
+ position,
22
+ properties: { ...properties, width, height }
23
+ });
24
+ }
25
+
26
+ emitTextPlacement(position, properties) {
27
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
28
+ type: 'text',
29
+ id: 'text',
30
+ position,
31
+ properties: {
32
+ fontSize: properties.fontSize || 18,
33
+ content: '',
34
+ fontFamily: 'Arial, sans-serif',
35
+ color: '#000000',
36
+ backgroundColor: 'transparent'
37
+ }
38
+ });
39
+ }
40
+
41
+ emitFrameDrawPlacement(x, y, w, h) {
42
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
43
+ type: 'frame',
44
+ id: 'frame',
45
+ position: { x, y },
46
+ properties: { width: Math.round(w), height: Math.round(h), title: 'Произвольный', lockedAspect: false, isArbitrary: true }
47
+ });
48
+ }
49
+
50
+ emitImageUploaded(position, uploadResult, width, height) {
51
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
52
+ type: 'image',
53
+ id: 'image',
54
+ position,
55
+ properties: {
56
+ src: uploadResult.url,
57
+ name: uploadResult.name,
58
+ width,
59
+ height
60
+ },
61
+ imageId: uploadResult.imageId || uploadResult.id
62
+ });
63
+ }
64
+
65
+ emitImageFallback(position, imageUrl, fileName, width, height) {
66
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
67
+ type: 'image',
68
+ id: 'image',
69
+ position,
70
+ properties: {
71
+ src: imageUrl,
72
+ name: fileName,
73
+ width,
74
+ height
75
+ }
76
+ });
77
+ }
78
+
79
+ emitFileUploaded(position, uploadResult, width, height) {
80
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
81
+ type: 'file',
82
+ id: 'file',
83
+ position,
84
+ properties: {
85
+ fileName: uploadResult.name,
86
+ fileSize: uploadResult.size,
87
+ mimeType: uploadResult.mimeType,
88
+ formattedSize: uploadResult.formattedSize,
89
+ url: uploadResult.url,
90
+ width,
91
+ height
92
+ },
93
+ fileId: uploadResult.fileId || uploadResult.id
94
+ });
95
+ }
96
+
97
+ emitFileFallback(position, fileName, fileSize, mimeType, width, height) {
98
+ this.host.eventBus.emit(Events.UI.ToolbarAction, {
99
+ type: 'file',
100
+ id: 'file',
101
+ position,
102
+ properties: {
103
+ fileName,
104
+ fileSize,
105
+ mimeType,
106
+ width,
107
+ height
108
+ }
109
+ });
110
+ }
111
+ }
@@ -0,0 +1,18 @@
1
+ export class PlacementSessionStore {
2
+ constructor(host) {
3
+ this.host = host;
4
+ }
5
+
6
+ initialize() {
7
+ this.host.pending = null;
8
+ this.host.selectedFile = null;
9
+ this.host.selectedImage = null;
10
+ this.host.ghostContainer = null;
11
+ }
12
+
13
+ clearSelectionState() {
14
+ this.host.pending = null;
15
+ this.host.selectedFile = null;
16
+ this.host.selectedImage = null;
17
+ }
18
+ }
@@ -55,9 +55,6 @@ export class BoxSelectController {
55
55
  this.emit('get:all:objects', request);
56
56
  const matched = [];
57
57
  for (const item of request.objects) {
58
- const meta = item.pixi && item.pixi._mb;
59
- // Исключаем фреймы из выделения рамкой — их можно выбрать только кликом по захватной рамке
60
- if (meta && meta.type === 'frame') continue;
61
58
  if (this.rectIntersectsRect(box, item.bounds)) matched.push(item.id);
62
59
  }
63
60
  let newSelection;
@@ -87,8 +84,6 @@ export class BoxSelectController {
87
84
  this.emit('get:all:objects', request);
88
85
  const matched = [];
89
86
  for (const item of request.objects) {
90
- const meta = item.pixi && item.pixi._mb;
91
- if (meta && meta.type === 'frame') continue;
92
87
  if (this.rectIntersectsRect(box, item.bounds)) matched.push(item.id);
93
88
  }
94
89
  if (matched.length > 0) {
@@ -0,0 +1,71 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+
3
+ export function tryStartAltCloneDuringDrag(event) {
4
+ if (this.isDragging && !this.isAltCloneMode && event.originalEvent && event.originalEvent.altKey) {
5
+ this.isAltCloneMode = true;
6
+ this.cloneSourceId = this.dragTarget;
7
+ this.clonePending = true;
8
+ // Создаём дубликат так, чтобы курсор захватывал ту же точку объекта
9
+ const wpos = this._toWorld(event.x, event.y);
10
+ const targetTopLeft = this._dragGrabOffset
11
+ ? { x: wpos.x - this._dragGrabOffset.x, y: wpos.y - this._dragGrabOffset.y }
12
+ : { x: wpos.x, y: wpos.y };
13
+ this.emit(Events.Tool.DuplicateRequest, {
14
+ originalId: this.cloneSourceId,
15
+ position: targetTopLeft
16
+ });
17
+ // Не сбрасываем dragTarget, чтобы исходник продолжал двигаться до появления копии
18
+ // Визуально это ок: копия появится и захватит drag в onDuplicateReady
19
+ }
20
+ }
21
+
22
+ export function resetCloneStateAfterDragEnd() {
23
+ this.isAltGroupCloneMode = false;
24
+ this.groupClonePending = false;
25
+ this.groupCloneOriginalIds = [];
26
+ this.groupCloneMap = null;
27
+ this.isAltCloneMode = false;
28
+ this.clonePending = false;
29
+ this.cloneSourceId = null;
30
+ }
31
+
32
+ export function onGroupDuplicateReady(idMap) {
33
+ this.groupClonePending = false;
34
+ this.groupCloneMap = idMap;
35
+ if (this._groupDragCtrl) this._groupDragCtrl.onGroupDuplicateReady(idMap);
36
+ // Формируем новое выделение из клонов
37
+ const newIds = [];
38
+ for (const orig of this.groupCloneOriginalIds) {
39
+ const nid = idMap[orig];
40
+ if (nid) newIds.push(nid);
41
+ }
42
+ if (newIds.length > 0) {
43
+ this.setSelection(newIds);
44
+ // Пересчитываем стартовые параметры для продолжения drag
45
+ const gb = this.computeGroupBounds();
46
+ this.groupStartBounds = gb;
47
+ this.groupDragOffset = { x: this.currentX - gb.x, y: this.currentY - gb.y };
48
+ // Сообщаем ядру о старте drag для новых объектов, чтобы зафиксировать начальные позиции
49
+ this.emit('group:drag:start', { objects: newIds });
50
+ }
51
+ }
52
+
53
+ export function onDuplicateReady(newObjectId) {
54
+ this.clonePending = false;
55
+
56
+ // Переключаем выделение на новый объект
57
+ this.clearSelection();
58
+ this.addToSelection(newObjectId);
59
+
60
+ // Завершаем drag исходного объекта и переключаем контроллер на новый объект
61
+ if (this._dragCtrl) this._dragCtrl.end();
62
+ this.dragTarget = newObjectId;
63
+ this.isDragging = true;
64
+ // Стартуем drag нового объекта под текущим курсором (в мировых координатах)
65
+ const w = this._toWorld(this.currentX, this.currentY);
66
+ if (this._dragCtrl) this._dragCtrl.start(newObjectId, { x: w.x, y: w.y });
67
+ // Мгновенно обновляем позицию под курсор
68
+ this.updateDrag({ x: this.currentX, y: this.currentY });
69
+ // Обновляем ручки
70
+ this.updateResizeHandles();
71
+ }
@@ -0,0 +1,10 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ export function toWorld(x, y) {
4
+ if (!this.app || !this.app.stage) return { x, y };
5
+ const world = this.app.stage.getChildByName && this.app.stage.getChildByName('worldLayer');
6
+ if (!world || !world.toLocal) return { x, y };
7
+ const p = new PIXI.Point(x, y);
8
+ const local = world.toLocal(p);
9
+ return { x: local.x, y: local.y };
10
+ }
@@ -0,0 +1,78 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+
3
+ export function updateCursor(event, defaultCursor) {
4
+ // Проверяем, что инструмент не уничтожен
5
+ if (this.destroyed) {
6
+ return;
7
+ }
8
+
9
+ const hitResult = this.hitTest(event.x, event.y);
10
+
11
+ switch (hitResult.type) {
12
+ case 'resize-handle':
13
+ this.cursor = this.getResizeCursor(hitResult.handle);
14
+ break;
15
+ case 'rotate-handle':
16
+ this.cursor = 'grab';
17
+ break;
18
+ case 'object':
19
+ this.cursor = 'move';
20
+ break;
21
+ default:
22
+ this.cursor = defaultCursor;
23
+ }
24
+
25
+ this.setCursor();
26
+ }
27
+
28
+ export function createRotatedResizeCursor(handleType, rotationDegrees) {
29
+ // Базовые углы для каждого типа ручки (в градусах)
30
+ const baseAngles = {
31
+ 'e': 0, // Восток - горизонтальная стрелка →
32
+ 'se': 45, // Юго-восток - диагональная стрелка ↘
33
+ 's': 90, // Юг - вертикальная стрелка ↓
34
+ 'sw': 135, // Юго-запад - диагональная стрелка ↙
35
+ 'w': 180, // Запад - горизонтальная стрелка ←
36
+ 'nw': 225, // Северо-запад - диагональная стрелка ↖
37
+ 'n': 270, // Север - вертикальная стрелка ↑
38
+ 'ne': 315 // Северо-восток - диагональная стрелка ↗
39
+ };
40
+
41
+ // Вычисляем итоговый угол: базовый угол ручки + поворот объекта
42
+ const totalAngle = (baseAngles[handleType] + rotationDegrees) % 360;
43
+
44
+ // Создаем SVG курсор изменения размера, повернутый на нужный угол (белый, крупнее)
45
+ const svg = `<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g transform="rotate(${totalAngle} 16 16)"><path d="M4 16 L9 11 L9 13 L23 13 L23 11 L28 16 L23 21 L23 19 L9 19 L9 21 Z" fill="white" stroke="black" stroke-width="1"/></g></svg>`;
46
+
47
+ // Используем encodeURIComponent вместо btoa для безопасного кодирования
48
+ const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
49
+
50
+ // Возвращаем CSS cursor с кастомным изображением (hotspot в центре 16x16)
51
+ return `url("${dataUrl}") 16 16, auto`;
52
+ }
53
+
54
+ export function getResizeCursor(handle, defaultCursor) {
55
+ // Получаем ID выбранного объекта для определения его поворота
56
+ const selectedObject = Array.from(this.selectedObjects)[0];
57
+ if (!selectedObject) {
58
+ return defaultCursor;
59
+ }
60
+
61
+ // Получаем угол поворота объекта
62
+ const rotationData = { objectId: selectedObject, rotation: 0 };
63
+ this.emit(Events.Tool.GetObjectRotation, rotationData);
64
+ const objectRotation = rotationData.rotation || 0;
65
+
66
+ // Создаем кастомный курсор, повернутый на точный угол объекта
67
+ return this.createRotatedResizeCursor(handle, objectRotation);
68
+ }
69
+
70
+ export function setCursor() {
71
+ if (this.resizeHandles && this.resizeHandles.app && this.resizeHandles.app.view) {
72
+ // Устанавливаем курсор на canvas, а не на body
73
+ this.resizeHandles.app.view.style.cursor = this.cursor;
74
+ } else {
75
+ // Fallback на базовую реализацию
76
+ this.__baseSetCursor();
77
+ }
78
+ }