@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,504 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ export class GhostController {
4
+ constructor(host) {
5
+ this.host = host;
6
+ }
7
+
8
+ showFileGhost() {
9
+ const host = this.host;
10
+ if (!host.selectedFile || !host.world) return;
11
+
12
+ this.hideGhost();
13
+
14
+ host.ghostContainer = new PIXI.Container();
15
+ host.ghostContainer.alpha = 0.6;
16
+ if (host.app && host.app.view) {
17
+ const rect = host.app.view.getBoundingClientRect();
18
+ const cursorX = (typeof host.app.view._lastMouseX === 'number') ? host.app.view._lastMouseX : (rect.left + rect.width / 2);
19
+ const cursorY = (typeof host.app.view._lastMouseY === 'number') ? host.app.view._lastMouseY : (rect.top + rect.height / 2);
20
+ const worldPoint = host._toWorld(cursorX, cursorY);
21
+ this.updateGhostPosition(worldPoint.x, worldPoint.y);
22
+ }
23
+ const fileFont = (host.selectedFile.properties?.fontFamily) || 'Caveat, Arial, cursive';
24
+ const primaryFont = String(fileFont).split(',')[0].trim().replace(/^['"]|['"]$/g, '') || 'Caveat';
25
+ void primaryFont;
26
+
27
+ const width = host.selectedFile.properties.width || 120;
28
+ const height = host.selectedFile.properties.height || 140;
29
+
30
+ const shadow = new PIXI.Graphics();
31
+ try {
32
+ shadow.filters = [new PIXI.filters.BlurFilter(6)];
33
+ } catch (e) {}
34
+ shadow.beginFill(0x000000, 1);
35
+ shadow.drawRect(0, 0, width, height);
36
+ shadow.endFill();
37
+ shadow.x = 2;
38
+ shadow.y = 3;
39
+ shadow.alpha = 0.18;
40
+
41
+ const background = new PIXI.Graphics();
42
+ background.beginFill(0xFFFFFF, 1);
43
+ background.drawRect(0, 0, width, height);
44
+ background.endFill();
45
+
46
+ const icon = new PIXI.Graphics();
47
+ const iconSize = Math.min(48, width * 0.4);
48
+ const iconWidthDrawn = iconSize * 0.8;
49
+ const iconX = (width - iconWidthDrawn) / 2;
50
+ const iconY = 16;
51
+ icon.beginFill(0x6B7280, 1);
52
+ icon.drawRect(iconX, iconY, iconWidthDrawn, iconSize);
53
+ icon.endFill();
54
+
55
+ const fileName = host.selectedFile.fileName || 'File';
56
+ const displayName = fileName;
57
+ const nameText = new PIXI.Text(displayName, {
58
+ fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
59
+ fontSize: 12,
60
+ fill: 0x333333,
61
+ align: 'center',
62
+ wordWrap: true,
63
+ breakWords: true,
64
+ wordWrapWidth: Math.max(1, width - 24)
65
+ });
66
+ nameText.anchor.set(0.5, 0);
67
+ nameText.x = width / 2;
68
+ nameText.y = iconY + iconSize + 8;
69
+
70
+ host.ghostContainer.addChild(shadow);
71
+ host.ghostContainer.addChild(background);
72
+ host.ghostContainer.addChild(icon);
73
+ host.ghostContainer.addChild(nameText);
74
+
75
+ host.ghostContainer.pivot.x = width / 2;
76
+ host.ghostContainer.pivot.y = height / 2;
77
+
78
+ host.world.addChild(host.ghostContainer);
79
+ }
80
+
81
+ hideGhost() {
82
+ const host = this.host;
83
+ if (host.ghostContainer && host.world) {
84
+ host.world.removeChild(host.ghostContainer);
85
+ host.ghostContainer.destroy();
86
+ host.ghostContainer = null;
87
+ }
88
+ }
89
+
90
+ updateGhostPosition(x, y) {
91
+ const host = this.host;
92
+ if (host.ghostContainer) {
93
+ host.ghostContainer.x = x;
94
+ host.ghostContainer.y = y;
95
+ }
96
+ }
97
+
98
+ async showImageGhost() {
99
+ const host = this.host;
100
+ if (!host.selectedImage || !host.world) return;
101
+
102
+ this.hideGhost();
103
+
104
+ host.ghostContainer = new PIXI.Container();
105
+ host.ghostContainer.alpha = 0.6;
106
+
107
+ const isEmojiIcon = host.selectedImage.properties?.isEmojiIcon;
108
+ const maxWidth = host.selectedImage.properties.width || (isEmojiIcon ? 64 : 300);
109
+ const maxHeight = host.selectedImage.properties.height || (isEmojiIcon ? 64 : 200);
110
+
111
+ try {
112
+ const imageUrl = URL.createObjectURL(host.selectedImage.file);
113
+ const texture = await PIXI.Texture.fromURL(imageUrl);
114
+
115
+ const imageAspect = texture.width / texture.height;
116
+ let width = maxWidth;
117
+ let height = maxWidth / imageAspect;
118
+
119
+ if (height > maxHeight) {
120
+ height = maxHeight;
121
+ width = maxHeight * imageAspect;
122
+ }
123
+
124
+ const sprite = new PIXI.Sprite(texture);
125
+ sprite.width = width;
126
+ sprite.height = height;
127
+
128
+ const border = new PIXI.Graphics();
129
+ border.lineStyle(2, 0xDEE2E6, 0.8);
130
+ border.drawRoundedRect(-2, -2, width + 4, height + 4, 4);
131
+
132
+ host.ghostContainer.addChild(border);
133
+ host.ghostContainer.addChild(sprite);
134
+
135
+ host.ghostContainer.pivot.x = width / 2;
136
+ host.ghostContainer.pivot.y = height / 2;
137
+
138
+ URL.revokeObjectURL(imageUrl);
139
+ } catch (error) {
140
+ console.warn('Не удалось загрузить превью изображения, показываем заглушку:', error);
141
+
142
+ const graphics = new PIXI.Graphics();
143
+ graphics.beginFill(0xF8F9FA, 0.8);
144
+ graphics.lineStyle(2, 0xDEE2E6, 0.8);
145
+ graphics.drawRoundedRect(0, 0, maxWidth, maxHeight, 8);
146
+ graphics.endFill();
147
+
148
+ graphics.beginFill(0x6C757D, 0.6);
149
+ graphics.drawRoundedRect(maxWidth * 0.2, maxHeight * 0.15, maxWidth * 0.6, maxHeight * 0.3, 4);
150
+ graphics.endFill();
151
+
152
+ const fileName = host.selectedImage.fileName || 'Image';
153
+ const displayName = fileName.length > 20 ? fileName.substring(0, 17) + '...' : fileName;
154
+
155
+ const nameText = new PIXI.Text(displayName, {
156
+ fontFamily: 'Arial, sans-serif',
157
+ fontSize: 12,
158
+ fill: 0x495057,
159
+ align: 'center',
160
+ wordWrap: true,
161
+ wordWrapWidth: maxWidth - 10
162
+ });
163
+
164
+ nameText.x = (maxWidth - nameText.width) / 2;
165
+ nameText.y = maxHeight * 0.55;
166
+
167
+ host.ghostContainer.addChild(graphics);
168
+ host.ghostContainer.addChild(nameText);
169
+
170
+ host.ghostContainer.pivot.x = maxWidth / 2;
171
+ host.ghostContainer.pivot.y = maxHeight / 2;
172
+ }
173
+
174
+ host.world.addChild(host.ghostContainer);
175
+ }
176
+
177
+ async showImageUrlGhost() {
178
+ const host = this.host;
179
+ if (!host.pending || host.pending.type !== 'image' || !host.world) return;
180
+ const src = host.pending.properties?.src;
181
+ if (!src) return;
182
+
183
+ this.hideGhost();
184
+
185
+ host.ghostContainer = new PIXI.Container();
186
+ host.ghostContainer.alpha = 0.6;
187
+
188
+ const isEmojiIcon = host.pending.properties?.isEmojiIcon;
189
+ const maxWidth = host.pending.size?.width || host.pending.properties?.width || (isEmojiIcon ? 64 : 56);
190
+ const maxHeight = host.pending.size?.height || host.pending.properties?.height || (isEmojiIcon ? 64 : 56);
191
+
192
+ try {
193
+ const texture = await PIXI.Texture.fromURL(src);
194
+ const imageAspect = (texture.width || 1) / (texture.height || 1);
195
+ let width = maxWidth;
196
+ let height = maxWidth / imageAspect;
197
+ if (height > maxHeight) {
198
+ height = maxHeight;
199
+ width = maxHeight * imageAspect;
200
+ }
201
+
202
+ const sprite = new PIXI.Sprite(texture);
203
+ sprite.width = Math.max(1, Math.round(width));
204
+ sprite.height = Math.max(1, Math.round(height));
205
+
206
+ const border = new PIXI.Graphics();
207
+ try { border.lineStyle({ width: 2, color: 0xDEE2E6, alpha: 0.8 }); }
208
+ catch (_) { border.lineStyle(2, 0xDEE2E6, 0.8); }
209
+ border.drawRoundedRect(-2, -2, sprite.width + 4, sprite.height + 4, 4);
210
+
211
+ host.ghostContainer.addChild(border);
212
+ host.ghostContainer.addChild(sprite);
213
+ host.ghostContainer.pivot.set(sprite.width / 2, sprite.height / 2);
214
+ } catch (e) {
215
+ const g = new PIXI.Graphics();
216
+ g.beginFill(0xF0F0F0, 0.8);
217
+ g.lineStyle(2, 0xDEE2E6, 0.8);
218
+ g.drawRoundedRect(0, 0, maxWidth, maxHeight, 8);
219
+ g.endFill();
220
+ host.ghostContainer.addChild(g);
221
+ host.ghostContainer.pivot.set(maxWidth / 2, maxHeight / 2);
222
+ }
223
+
224
+ host.world.addChild(host.ghostContainer);
225
+
226
+ if (!isEmojiIcon) {
227
+ try {
228
+ if (host.app && host.app.view && src) {
229
+ const cursorSize = 24;
230
+ const url = encodeURI(src);
231
+ host.cursor = `url(${url}) ${Math.floor(cursorSize / 2)} ${Math.floor(cursorSize / 2)}, default`;
232
+ host.app.view.style.cursor = host.cursor;
233
+ }
234
+ } catch (_) {}
235
+ } else if (host.app && host.app.view) {
236
+ host.cursor = 'crosshair';
237
+ host.app.view.style.cursor = host.cursor;
238
+ }
239
+ }
240
+
241
+ showTextGhost() {
242
+ const host = this.host;
243
+ if (!host.pending || host.pending.type !== 'text' || !host.world) return;
244
+
245
+ this.hideGhost();
246
+
247
+ host.ghostContainer = new PIXI.Container();
248
+ host.ghostContainer.alpha = 0.6;
249
+
250
+ const fontSize = host.pending.properties?.fontSize || 18;
251
+ const width = 120;
252
+ const height = fontSize + 20;
253
+
254
+ const background = new PIXI.Graphics();
255
+ background.beginFill(0xFFFFFF, 0.8);
256
+ background.lineStyle(1, 0x007BFF, 0.8);
257
+ background.drawRoundedRect(0, 0, width, height, 4);
258
+ background.endFill();
259
+
260
+ const placeholderText = new PIXI.Text('Текст', {
261
+ fontFamily: 'Arial, sans-serif',
262
+ fontSize: fontSize,
263
+ fill: 0x6C757D,
264
+ align: 'left'
265
+ });
266
+
267
+ placeholderText.x = 8;
268
+ placeholderText.y = (height - placeholderText.height) / 2;
269
+
270
+ const cursor = new PIXI.Graphics();
271
+ cursor.lineStyle(2, 0x007BFF, 0.8);
272
+ cursor.moveTo(placeholderText.x + placeholderText.width + 4, placeholderText.y);
273
+ cursor.lineTo(placeholderText.x + placeholderText.width + 4, placeholderText.y + placeholderText.height);
274
+
275
+ host.ghostContainer.addChild(background);
276
+ host.ghostContainer.addChild(placeholderText);
277
+ host.ghostContainer.addChild(cursor);
278
+
279
+ host.ghostContainer.pivot.x = width / 2;
280
+ host.ghostContainer.pivot.y = height / 2;
281
+
282
+ host.world.addChild(host.ghostContainer);
283
+ }
284
+
285
+ showNoteGhost() {
286
+ const host = this.host;
287
+ if (!host.pending || host.pending.type !== 'note' || !host.world) return;
288
+
289
+ this.hideGhost();
290
+
291
+ host.ghostContainer = new PIXI.Container();
292
+ host.ghostContainer.alpha = 0.6;
293
+
294
+ const width = host.pending.properties?.width || 250;
295
+ const height = host.pending.properties?.height || 250;
296
+ const backgroundColor = (typeof host.pending.properties?.backgroundColor === 'number')
297
+ ? host.pending.properties.backgroundColor
298
+ : 0xFFF9C4;
299
+ const textColor = (typeof host.pending.properties?.textColor === 'number')
300
+ ? host.pending.properties.textColor
301
+ : 0x1A1A1A;
302
+ void textColor;
303
+
304
+ const background = new PIXI.Graphics();
305
+ background.beginFill(backgroundColor, 1);
306
+ background.drawRoundedRect(0, 0, width, height, 2);
307
+ background.endFill();
308
+
309
+ host.ghostContainer.addChild(background);
310
+
311
+ host.ghostContainer.pivot.x = width / 2;
312
+ host.ghostContainer.pivot.y = height / 2;
313
+
314
+ host.world.addChild(host.ghostContainer);
315
+ }
316
+
317
+ showEmojiGhost() {
318
+ const host = this.host;
319
+ if (!host.pending || host.pending.type !== 'emoji' || !host.world) return;
320
+
321
+ this.hideGhost();
322
+
323
+ host.ghostContainer = new PIXI.Container();
324
+ host.ghostContainer.alpha = 0.7;
325
+
326
+ const content = host.pending.properties?.content || '🙂';
327
+ const fontSize = host.pending.properties?.fontSize || 48;
328
+ const width = host.pending.properties?.width || fontSize;
329
+ const height = host.pending.properties?.height || fontSize;
330
+
331
+ const emojiText = new PIXI.Text(content, {
332
+ fontFamily: 'Segoe UI Emoji, Apple Color Emoji, Noto Color Emoji, Arial',
333
+ fontSize: fontSize
334
+ });
335
+
336
+ if (typeof emojiText.anchor?.set === 'function') {
337
+ emojiText.anchor.set(0, 0);
338
+ }
339
+
340
+ const bounds = emojiText.getLocalBounds();
341
+ const baseW = Math.max(1, bounds.width || 1);
342
+ const baseH = Math.max(1, bounds.height || 1);
343
+
344
+ const scaleX = width / baseW;
345
+ const scaleY = height / baseH;
346
+ const scale = Math.min(scaleX, scaleY);
347
+
348
+ emojiText.scale.set(scale, scale);
349
+
350
+ const background = new PIXI.Graphics();
351
+ background.beginFill(0xFFFFFF, 0.3);
352
+ background.lineStyle(1, 0xDDDDDD, 0.5);
353
+ background.drawRoundedRect(-4, -4, width + 8, height + 8, 4);
354
+ background.endFill();
355
+
356
+ host.ghostContainer.addChild(background);
357
+ host.ghostContainer.addChild(emojiText);
358
+
359
+ host.ghostContainer.pivot.x = width / 2;
360
+ host.ghostContainer.pivot.y = height / 2;
361
+
362
+ host.world.addChild(host.ghostContainer);
363
+ }
364
+
365
+ showFrameGhost() {
366
+ const host = this.host;
367
+ if (!host.pending || host.pending.type !== 'frame' || !host.world) return;
368
+
369
+ this.hideGhost();
370
+
371
+ host.ghostContainer = new PIXI.Container();
372
+ host.ghostContainer.alpha = 0.6;
373
+
374
+ const width = host.pending.properties?.width || 200;
375
+ const height = host.pending.properties?.height || 300;
376
+ const fillColor = (host.pending.properties?.backgroundColor ?? host.pending.properties?.fillColor) ?? 0xFFFFFF;
377
+ const title = host.pending.properties?.title || 'Новый';
378
+
379
+ const rootStyles = (typeof window !== 'undefined') ? getComputedStyle(document.documentElement) : null;
380
+ const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '4') : 4;
381
+ const cssCornerRadius = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-corner-radius') || '6') : 6;
382
+ const cssBorderColor = rootStyles ? rootStyles.getPropertyValue('--frame-border-color').trim() : '';
383
+ const borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth : 4;
384
+ const cornerRadius = Number.isFinite(cssCornerRadius) ? cssCornerRadius : 6;
385
+ let strokeColor;
386
+ if (cssBorderColor && cssBorderColor.startsWith('#')) {
387
+ strokeColor = parseInt(cssBorderColor.slice(1), 16);
388
+ } else {
389
+ strokeColor = (typeof host.pending.properties?.borderColor === 'number') ? host.pending.properties.borderColor : 0xE0E0E0;
390
+ }
391
+
392
+ const frameGraphics = new PIXI.Graphics();
393
+ try {
394
+ frameGraphics.lineStyle({ width: borderWidth, color: strokeColor, alpha: 1, alignment: 1 });
395
+ } catch (e) {
396
+ frameGraphics.lineStyle(borderWidth, strokeColor, 1);
397
+ }
398
+ frameGraphics.beginFill(fillColor, 1);
399
+ frameGraphics.drawRoundedRect(0, 0, width, height, cornerRadius);
400
+ frameGraphics.endFill();
401
+
402
+ const titleText = new PIXI.Text(title, {
403
+ fontFamily: 'Arial, sans-serif',
404
+ fontSize: 14,
405
+ fill: 0x333333,
406
+ fontWeight: 'bold'
407
+ });
408
+ titleText.anchor.set(0, 0);
409
+ titleText.x = 8;
410
+ titleText.y = 4;
411
+
412
+ host.ghostContainer.addChild(frameGraphics);
413
+ host.ghostContainer.addChild(titleText);
414
+
415
+ host.ghostContainer.pivot.x = width / 2;
416
+ host.ghostContainer.pivot.y = height / 2;
417
+
418
+ host.world.addChild(host.ghostContainer);
419
+ }
420
+
421
+ showShapeGhost() {
422
+ const host = this.host;
423
+ if (!host.pending || host.pending.type !== 'shape' || !host.world) return;
424
+
425
+ this.hideGhost();
426
+
427
+ host.ghostContainer = new PIXI.Container();
428
+ host.ghostContainer.alpha = 0.6;
429
+
430
+ const kind = host.pending.properties?.kind || 'square';
431
+ const width = 100;
432
+ const height = 100;
433
+ const fillColor = 0x3b82f6;
434
+ const cornerRadius = host.pending.properties?.cornerRadius || 10;
435
+
436
+ const shapeGraphics = new PIXI.Graphics();
437
+ shapeGraphics.beginFill(fillColor, 0.8);
438
+
439
+ switch (kind) {
440
+ case 'circle': {
441
+ const r = Math.min(width, height) / 2;
442
+ shapeGraphics.drawCircle(width / 2, height / 2, r);
443
+ break;
444
+ }
445
+ case 'rounded': {
446
+ const r = cornerRadius || 10;
447
+ shapeGraphics.drawRoundedRect(0, 0, width, height, r);
448
+ break;
449
+ }
450
+ case 'triangle': {
451
+ shapeGraphics.moveTo(width / 2, 0);
452
+ shapeGraphics.lineTo(width, height);
453
+ shapeGraphics.lineTo(0, height);
454
+ shapeGraphics.lineTo(width / 2, 0);
455
+ break;
456
+ }
457
+ case 'diamond': {
458
+ shapeGraphics.moveTo(width / 2, 0);
459
+ shapeGraphics.lineTo(width, height / 2);
460
+ shapeGraphics.lineTo(width / 2, height);
461
+ shapeGraphics.lineTo(0, height / 2);
462
+ shapeGraphics.lineTo(width / 2, 0);
463
+ break;
464
+ }
465
+ case 'parallelogram': {
466
+ const skew = Math.min(width * 0.25, 20);
467
+ shapeGraphics.moveTo(skew, 0);
468
+ shapeGraphics.lineTo(width, 0);
469
+ shapeGraphics.lineTo(width - skew, height);
470
+ shapeGraphics.lineTo(0, height);
471
+ shapeGraphics.lineTo(skew, 0);
472
+ break;
473
+ }
474
+ case 'arrow': {
475
+ const shaftH = Math.max(6, height * 0.3);
476
+ const shaftY = (height - shaftH) / 2;
477
+ shapeGraphics.drawRect(0, shaftY, width * 0.6, shaftH);
478
+ shapeGraphics.moveTo(width * 0.6, 0);
479
+ shapeGraphics.lineTo(width, height / 2);
480
+ shapeGraphics.lineTo(width * 0.6, height);
481
+ shapeGraphics.lineTo(width * 0.6, 0);
482
+ break;
483
+ }
484
+ case 'square':
485
+ default: {
486
+ shapeGraphics.drawRect(0, 0, width, height);
487
+ break;
488
+ }
489
+ }
490
+ shapeGraphics.endFill();
491
+
492
+ const border = new PIXI.Graphics();
493
+ border.lineStyle(2, 0x007BFF, 0.6);
494
+ border.drawRect(-2, -2, width + 4, height + 4);
495
+
496
+ host.ghostContainer.addChild(border);
497
+ host.ghostContainer.addChild(shapeGraphics);
498
+
499
+ host.ghostContainer.pivot.x = width / 2;
500
+ host.ghostContainer.pivot.y = height / 2;
501
+
502
+ host.world.addChild(host.ghostContainer);
503
+ }
504
+ }
@@ -0,0 +1,20 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ export class PlacementCoordinateResolver {
4
+ constructor(host) {
5
+ this.host = host;
6
+ }
7
+
8
+ toWorld(x, y) {
9
+ if (!this.host.world) return { x, y };
10
+ const global = new PIXI.Point(x, y);
11
+ const local = this.host.world.toLocal(global);
12
+ return { x: local.x, y: local.y };
13
+ }
14
+
15
+ getWorldLayer() {
16
+ if (!this.host.app || !this.host.app.stage) return null;
17
+ const world = this.host.app.stage.getChildByName && this.host.app.stage.getChildByName('worldLayer');
18
+ return world || this.host.app.stage;
19
+ }
20
+ }
@@ -0,0 +1,91 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+
3
+ export class PlacementEventsBridge {
4
+ constructor(host) {
5
+ this.host = host;
6
+ this._attached = false;
7
+ }
8
+
9
+ attach() {
10
+ if (this._attached || !this.host.eventBus) return;
11
+ this._attached = true;
12
+
13
+ this.host.eventBus.on(Events.Place.Set, (cfg) => {
14
+ this.host.pending = cfg ? { ...cfg } : null;
15
+ if (this.host.app && this.host.app.view) {
16
+ const cur = this.host._getPendingCursor();
17
+ this.host.cursor = cur;
18
+ this.host.app.view.style.cursor = (cur === 'default') ? '' : cur;
19
+ }
20
+ this.host._updateCursorOverride();
21
+
22
+ if (this.host.pending && this.host.app && this.host.world) {
23
+ if (this.host.pending.type === 'note') {
24
+ this.host.showNoteGhost();
25
+ } else if (this.host.pending.type === 'emoji') {
26
+ this.host.showEmojiGhost();
27
+ } else if (this.host.pending.type === 'image') {
28
+ this.host.showImageUrlGhost();
29
+ } else if (this.host.pending.type === 'frame') {
30
+ this.host.showFrameGhost();
31
+ } else if (this.host.pending.type === 'frame-draw') {
32
+ this.host.startFrameDrawMode();
33
+ } else if (this.host.pending.type === 'shape') {
34
+ this.host.showShapeGhost();
35
+ }
36
+ if (this.host.pending.placeOnMouseUp && this.host.app && this.host.app.view) {
37
+ const onUp = (ev) => {
38
+ this.host.app.view.removeEventListener('mouseup', onUp);
39
+ if (!this.host.pending) return;
40
+ const worldPoint = this.host._toWorld(ev.x, ev.y);
41
+ const position = {
42
+ x: Math.round(worldPoint.x - (this.host.pending.size?.width ?? 100) / 2),
43
+ y: Math.round(worldPoint.y - (this.host.pending.size?.height ?? 100) / 2)
44
+ };
45
+ const props = { ...(this.host.pending.properties || {}) };
46
+ this.host.payloadFactory.emitGenericPlacement(this.host.pending.type, position, props);
47
+ this.host.pending = null;
48
+ this.host.hideGhost();
49
+ this.host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
50
+ };
51
+ this.host.app.view.addEventListener('mouseup', onUp, { once: true });
52
+ }
53
+ }
54
+ });
55
+
56
+ this.host.eventBus.on(Events.Tool.Activated, ({ tool }) => {
57
+ if (tool === 'select') {
58
+ this.host.sessionStore.clearSelectionState();
59
+ this.host.hideGhost();
60
+ this.host._updateCursorOverride();
61
+ }
62
+ });
63
+
64
+ this.host.eventBus.on(Events.Place.FileSelected, (fileData) => {
65
+ this.host.selectedFile = fileData;
66
+ this.host.selectedImage = null;
67
+
68
+ if (this.host.world) {
69
+ this.host.showFileGhost();
70
+ }
71
+ });
72
+
73
+ this.host.eventBus.on(Events.Place.FileCanceled, () => {
74
+ this.host.selectedFile = null;
75
+ this.host.hideGhost();
76
+ this.host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
77
+ });
78
+
79
+ this.host.eventBus.on(Events.Place.ImageSelected, (imageData) => {
80
+ this.host.selectedImage = imageData;
81
+ this.host.selectedFile = null;
82
+ this.host.showImageGhost();
83
+ });
84
+
85
+ this.host.eventBus.on(Events.Place.ImageCanceled, () => {
86
+ this.host.selectedImage = null;
87
+ this.host.hideGhost();
88
+ this.host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
89
+ });
90
+ }
91
+ }