@sequent-org/moodboard 1.2.118 → 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 +7 -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 -1765
  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 -976
  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,190 @@
1
+ import { Events } from '../events/Events.js';
2
+
3
+ export class KeyboardClipboardImagePaste {
4
+ constructor(eventBus, core = null) {
5
+ this.eventBus = eventBus;
6
+ this.core = core;
7
+ }
8
+
9
+ async handleImageUpload(dataUrl, fileName) {
10
+ try {
11
+ if (this.core && this.core.imageUploadService) {
12
+ const uploadResult = await this.core.imageUploadService.uploadFromDataUrl(dataUrl, fileName);
13
+ this.eventBus.emit(Events.UI.PasteImage, {
14
+ src: uploadResult.url,
15
+ name: uploadResult.name,
16
+ imageId: uploadResult.imageId || uploadResult.id
17
+ });
18
+ } else {
19
+ this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
20
+ }
21
+ } catch (error) {
22
+ console.error('Ошибка загрузки изображения:', error);
23
+ this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
24
+ }
25
+ }
26
+
27
+ async handleImageFileUpload(file, fileName) {
28
+ try {
29
+ if (this.core && this.core.imageUploadService) {
30
+ const uploadResult = await this.core.imageUploadService.uploadImage(file, fileName);
31
+ this.eventBus.emit(Events.UI.PasteImage, {
32
+ src: uploadResult.url,
33
+ name: uploadResult.name,
34
+ imageId: uploadResult.imageId || uploadResult.id
35
+ });
36
+ } else {
37
+ const reader = new FileReader();
38
+ reader.onload = () => {
39
+ this.eventBus.emit(Events.UI.PasteImage, {
40
+ src: reader.result,
41
+ name: fileName
42
+ });
43
+ };
44
+ reader.readAsDataURL(file);
45
+ }
46
+ } catch (error) {
47
+ console.error('Ошибка загрузки файла изображения:', error);
48
+ try {
49
+ const reader = new FileReader();
50
+ reader.onload = () => {
51
+ this.eventBus.emit(Events.UI.PasteImage, {
52
+ src: reader.result,
53
+ name: fileName
54
+ });
55
+ };
56
+ reader.readAsDataURL(file);
57
+ } catch (fallbackError) {
58
+ console.error('Критическая ошибка при чтении файла:', fallbackError);
59
+ }
60
+ }
61
+ }
62
+
63
+ createPasteHandler() {
64
+ return async (e) => {
65
+ try {
66
+ const cd = e.clipboardData;
67
+ if (!cd) return;
68
+ let handled = false;
69
+
70
+ const items = cd.items ? Array.from(cd.items) : [];
71
+ const imageItem = items.find(i => i.type && i.type.startsWith('image/'));
72
+ if (imageItem) {
73
+ e.preventDefault();
74
+ const file = imageItem.getAsFile();
75
+ if (file) {
76
+ await this.handleImageFileUpload(file, file.name || 'clipboard-image.png');
77
+ handled = true;
78
+ }
79
+ }
80
+ if (handled) return;
81
+
82
+ const files = cd.files ? Array.from(cd.files) : [];
83
+ const imgFile = files.find(f => f.type && f.type.startsWith('image/'));
84
+ if (imgFile) {
85
+ e.preventDefault();
86
+ await this.handleImageFileUpload(imgFile, imgFile.name || 'clipboard-image.png');
87
+ return;
88
+ }
89
+
90
+ const html = cd.getData && cd.getData('text/html');
91
+ if (html && html.includes('<img')) {
92
+ const match = html.match(/<img[^>]*src\s*=\s*"([^"]+)"/i);
93
+ if (match && match[1]) {
94
+ const srcInHtml = match[1];
95
+ if (/^data:image\//i.test(srcInHtml)) {
96
+ e.preventDefault();
97
+ this.handleImageUpload(srcInHtml, 'clipboard-image.png');
98
+ return;
99
+ }
100
+ if (/^https?:\/\//i.test(srcInHtml)) {
101
+ e.preventDefault();
102
+ try {
103
+ const resp = await fetch(srcInHtml, { mode: 'cors' });
104
+ const blob = await resp.blob();
105
+ const dataUrl = await this._blobToDataUrl(blob);
106
+ this.handleImageUpload(dataUrl, srcInHtml.split('/').pop() || 'image');
107
+ } catch (_) {
108
+ this.eventBus.emit(Events.UI.PasteImage, {
109
+ src: srcInHtml,
110
+ name: srcInHtml.split('/').pop() || 'image'
111
+ });
112
+ }
113
+ return;
114
+ }
115
+ if (/^blob:/i.test(srcInHtml)) {
116
+ try {
117
+ if (navigator.clipboard && navigator.clipboard.read) {
118
+ const itemsFromAPI = await navigator.clipboard.read();
119
+ for (const item of itemsFromAPI) {
120
+ const imgType = (item.types || []).find(t => t.startsWith('image/'));
121
+ if (!imgType) continue;
122
+ const blob = await item.getType(imgType);
123
+ const dataUrl = await this._blobToDataUrl(blob);
124
+ e.preventDefault();
125
+ this.handleImageUpload(dataUrl, `clipboard.${imgType.split('/')[1] || 'png'}`);
126
+ return;
127
+ }
128
+ }
129
+ } catch (_) {}
130
+ }
131
+ }
132
+ }
133
+
134
+ const text = cd.getData && cd.getData('text/plain');
135
+ if (text) {
136
+ const trimmed = text.trim();
137
+ const isDataUrl = /^data:image\//i.test(trimmed);
138
+ const isHttpUrl = /^https?:\/\//i.test(trimmed);
139
+ const looksLikeImage = /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(trimmed);
140
+ if (isDataUrl) {
141
+ e.preventDefault();
142
+ this.handleImageUpload(trimmed, 'clipboard-image.png');
143
+ return;
144
+ }
145
+ if (isHttpUrl && looksLikeImage) {
146
+ e.preventDefault();
147
+ try {
148
+ const resp = await fetch(trimmed, { mode: 'cors' });
149
+ const blob = await resp.blob();
150
+ const dataUrl = await this._blobToDataUrl(blob);
151
+ this.handleImageUpload(dataUrl, trimmed.split('/').pop() || 'image');
152
+ return;
153
+ } catch (_) {
154
+ this.eventBus.emit(Events.UI.PasteImage, {
155
+ src: trimmed,
156
+ name: trimmed.split('/').pop() || 'image'
157
+ });
158
+ return;
159
+ }
160
+ }
161
+ }
162
+
163
+ try {
164
+ if (!handled && navigator.clipboard && navigator.clipboard.read) {
165
+ const itemsFromAPI = await navigator.clipboard.read();
166
+ for (const item of itemsFromAPI) {
167
+ const imgType = (item.types || []).find(t => t.startsWith('image/'));
168
+ if (!imgType) continue;
169
+ const blob = await item.getType(imgType);
170
+ const dataUrl = await this._blobToDataUrl(blob);
171
+ e.preventDefault();
172
+ this.handleImageUpload(dataUrl, `clipboard.${imgType.split('/')[1] || 'png'}`);
173
+ return;
174
+ }
175
+ }
176
+ } catch (_) {}
177
+ } catch (err) {
178
+ console.error('Error in paste handler:', err);
179
+ }
180
+ };
181
+ }
182
+
183
+ _blobToDataUrl(blob) {
184
+ return new Promise((resolve) => {
185
+ const reader = new FileReader();
186
+ reader.onload = () => resolve(reader.result);
187
+ reader.readAsDataURL(blob);
188
+ });
189
+ }
190
+ }
@@ -0,0 +1,35 @@
1
+ export function isInputElement(element) {
2
+ if (!element || !element.tagName) {
3
+ return false;
4
+ }
5
+
6
+ const inputTags = ['input', 'textarea', 'select'];
7
+ const isInput = inputTags.includes(element.tagName.toLowerCase());
8
+ const isContentEditable = element.contentEditable === 'true';
9
+
10
+ return isInput || isContentEditable;
11
+ }
12
+
13
+ export function isTextEditorActive(doc = document) {
14
+ const activeElement = doc.activeElement;
15
+
16
+ if (activeElement && (
17
+ activeElement.tagName === 'INPUT' ||
18
+ activeElement.tagName === 'TEXTAREA' ||
19
+ activeElement.contentEditable === 'true'
20
+ )) {
21
+ return true;
22
+ }
23
+
24
+ const fileNameEditor = doc.querySelector('.moodboard-file-name-editor');
25
+ if (fileNameEditor && fileNameEditor.style.display !== 'none') {
26
+ return true;
27
+ }
28
+
29
+ const textEditor = doc.querySelector('.moodboard-text-editor');
30
+ if (textEditor && textEditor.style.display !== 'none') {
31
+ return true;
32
+ }
33
+
34
+ return false;
35
+ }
@@ -0,0 +1,92 @@
1
+ import { Events } from '../events/Events.js';
2
+
3
+ export class KeyboardEventRouter {
4
+ constructor(eventBus, shortcuts, isInputElementGuard) {
5
+ this.eventBus = eventBus;
6
+ this.shortcuts = shortcuts;
7
+ this.isInputElementGuard = isInputElementGuard;
8
+ }
9
+
10
+ handleKeyDown(event) {
11
+ if (this.isInputElementGuard(event.target)) {
12
+ return;
13
+ }
14
+
15
+ const combination = this.eventToShortcut(event);
16
+ const handlers = this.shortcuts.get(combination);
17
+
18
+ if (handlers && handlers.length > 0) {
19
+ handlers.forEach(({ handler, preventDefault, stopPropagation }) => {
20
+ if (preventDefault) event.preventDefault();
21
+ if (stopPropagation) event.stopPropagation();
22
+
23
+ handler(event);
24
+ });
25
+ }
26
+ }
27
+
28
+ handleKeyUp(event) {
29
+ const combination = this.eventToShortcut(event, 'keyup');
30
+
31
+ this.eventBus.emit(Events.Keyboard.KeyUp, {
32
+ key: event.key,
33
+ code: event.code,
34
+ combination,
35
+ originalEvent: event
36
+ });
37
+ }
38
+
39
+ normalizeShortcut(combination) {
40
+ return combination
41
+ .toLowerCase()
42
+ .split('+')
43
+ .map(key => key.trim())
44
+ .sort((a, b) => {
45
+ const order = ['ctrl', 'alt', 'shift', 'meta'];
46
+ const aIndex = order.indexOf(a);
47
+ const bIndex = order.indexOf(b);
48
+
49
+ if (aIndex !== -1 && bIndex !== -1) {
50
+ return aIndex - bIndex;
51
+ }
52
+ if (aIndex !== -1) return -1;
53
+ if (bIndex !== -1) return 1;
54
+ return a.localeCompare(b);
55
+ })
56
+ .join('+');
57
+ }
58
+
59
+ eventToShortcut(event, eventType = 'keydown') {
60
+ const parts = [];
61
+
62
+ if (event.ctrlKey) parts.push('ctrl');
63
+ if (event.altKey) parts.push('alt');
64
+ if (event.shiftKey) parts.push('shift');
65
+ if (event.metaKey) parts.push('meta');
66
+
67
+ let key = event.key.toLowerCase();
68
+
69
+ const specialKeys = {
70
+ ' ': 'space',
71
+ 'enter': 'enter',
72
+ 'escape': 'escape',
73
+ 'backspace': 'backspace',
74
+ 'delete': 'delete',
75
+ 'tab': 'tab',
76
+ 'arrowup': 'arrowup',
77
+ 'arrowdown': 'arrowdown',
78
+ 'arrowleft': 'arrowleft',
79
+ 'arrowright': 'arrowright'
80
+ };
81
+
82
+ if (specialKeys[key]) {
83
+ key = specialKeys[key];
84
+ }
85
+
86
+ if (!['control', 'alt', 'shift', 'meta'].includes(key)) {
87
+ parts.push(key);
88
+ }
89
+
90
+ return parts.join('+');
91
+ }
92
+ }
@@ -0,0 +1,103 @@
1
+ import { Events } from '../events/Events.js';
2
+
3
+ export class KeyboardSelectionActions {
4
+ constructor(eventBus, isTextEditorActive) {
5
+ this.eventBus = eventBus;
6
+ this.isTextEditorActive = isTextEditorActive;
7
+ }
8
+
9
+ createHandler(actionId) {
10
+ switch (actionId) {
11
+ case 'undo':
12
+ return () => {
13
+ this.eventBus.emit(Events.Keyboard.Undo);
14
+ };
15
+ case 'redo':
16
+ return () => {
17
+ this.eventBus.emit(Events.Keyboard.Redo);
18
+ };
19
+ case 'select-all':
20
+ return () => {
21
+ this.eventBus.emit(Events.Keyboard.SelectAll);
22
+ };
23
+ case 'copy':
24
+ return () => {
25
+ this.eventBus.emit(Events.Keyboard.Copy);
26
+ };
27
+ case 'paste':
28
+ return () => {
29
+ this.eventBus.emit(Events.Keyboard.Paste);
30
+ };
31
+ case 'layer-bring-to-front':
32
+ return () => {
33
+ const data = { selection: [] };
34
+ this.eventBus.emit(Events.Tool.GetSelection, data);
35
+ const id = data.selection?.[0];
36
+ if (id) this.eventBus.emit(Events.UI.LayerBringToFront, { objectId: id });
37
+ };
38
+ case 'layer-bring-forward':
39
+ return () => {
40
+ const data = { selection: [] };
41
+ this.eventBus.emit(Events.Tool.GetSelection, data);
42
+ const id = data.selection?.[0];
43
+ if (id) this.eventBus.emit(Events.UI.LayerBringForward, { objectId: id });
44
+ };
45
+ case 'layer-send-to-back':
46
+ return () => {
47
+ const data = { selection: [] };
48
+ this.eventBus.emit(Events.Tool.GetSelection, data);
49
+ const id = data.selection?.[0];
50
+ if (id) this.eventBus.emit(Events.UI.LayerSendToBack, { objectId: id });
51
+ };
52
+ case 'layer-send-backward':
53
+ return () => {
54
+ const data = { selection: [] };
55
+ this.eventBus.emit(Events.Tool.GetSelection, data);
56
+ const id = data.selection?.[0];
57
+ if (id) this.eventBus.emit(Events.UI.LayerSendBackward, { objectId: id });
58
+ };
59
+ case 'delete':
60
+ return () => {
61
+ if (this.isTextEditorActive()) {
62
+ console.log('🔒 KeyboardManager: Текстовый редактор активен, пропускаем удаление объектов');
63
+ return;
64
+ }
65
+ this.eventBus.emit(Events.Keyboard.Delete);
66
+ };
67
+ case 'escape':
68
+ return () => {
69
+ this.eventBus.emit(Events.Keyboard.Escape);
70
+ };
71
+ case 'move-up':
72
+ return (event) => {
73
+ this.eventBus.emit(Events.Keyboard.Move, {
74
+ direction: 'up',
75
+ step: event.shiftKey ? 10 : 1
76
+ });
77
+ };
78
+ case 'move-down':
79
+ return (event) => {
80
+ this.eventBus.emit(Events.Keyboard.Move, {
81
+ direction: 'down',
82
+ step: event.shiftKey ? 10 : 1
83
+ });
84
+ };
85
+ case 'move-left':
86
+ return (event) => {
87
+ this.eventBus.emit(Events.Keyboard.Move, {
88
+ direction: 'left',
89
+ step: event.shiftKey ? 10 : 1
90
+ });
91
+ };
92
+ case 'move-right':
93
+ return (event) => {
94
+ this.eventBus.emit(Events.Keyboard.Move, {
95
+ direction: 'right',
96
+ step: event.shiftKey ? 10 : 1
97
+ });
98
+ };
99
+ default:
100
+ return null;
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,31 @@
1
+ export const DEFAULT_KEYBOARD_SHORTCUTS = [
2
+ { combination: 'ctrl+z', actionId: 'undo', description: 'Отменить действие', preventDefault: true },
3
+ { combination: 'ctrl+я', actionId: 'undo', description: 'Отменить действие (рус)', preventDefault: true },
4
+ { combination: 'ctrl+shift+z', actionId: 'redo', description: 'Повторить действие', preventDefault: true },
5
+ { combination: 'ctrl+shift+я', actionId: 'redo', description: 'Повторить действие (рус)', preventDefault: true },
6
+ { combination: 'ctrl+y', actionId: 'redo', description: 'Повторить действие (альтернативный)', preventDefault: true },
7
+ { combination: 'ctrl+н', actionId: 'redo', description: 'Повторить действие (рус альт)', preventDefault: true },
8
+ { combination: 'ctrl+a', actionId: 'select-all', description: 'Выделить все', preventDefault: true },
9
+ { combination: 'ctrl+ф', actionId: 'select-all', description: 'Выделить все (рус)', preventDefault: true },
10
+ { combination: 'ctrl+c', actionId: 'copy', description: 'Копировать', preventDefault: true },
11
+ { combination: 'ctrl+с', actionId: 'copy', description: 'Копировать (рус)', preventDefault: true },
12
+ { combination: 'ctrl+v', actionId: 'paste', description: 'Вставить', preventDefault: false },
13
+ { combination: 'ctrl+м', actionId: 'paste', description: 'Вставить (рус)', preventDefault: false },
14
+ { combination: ']', actionId: 'layer-bring-to-front', description: 'На передний план', preventDefault: true },
15
+ { combination: 'ctrl+]', actionId: 'layer-bring-forward', description: 'Перенести вперёд', preventDefault: true },
16
+ { combination: '[', actionId: 'layer-send-to-back', description: 'На задний план', preventDefault: true },
17
+ { combination: 'ctrl+[', actionId: 'layer-send-backward', description: 'Перенести назад', preventDefault: true },
18
+ { combination: 'delete', actionId: 'delete', description: 'Удалить объект', preventDefault: true },
19
+ { combination: 'backspace', actionId: 'delete', description: 'Удалить объект', preventDefault: true },
20
+ { combination: 'escape', actionId: 'escape', description: 'Отменить выделение', preventDefault: true },
21
+ { combination: 'v', actionId: 'tool-select', description: 'Выбрать инструмент выделения' },
22
+ { combination: 'м', actionId: 'tool-select', description: 'Выбрать инструмент выделения (рус)' },
23
+ { combination: 't', actionId: 'tool-text', description: 'Выбрать инструмент текста' },
24
+ { combination: 'е', actionId: 'tool-text', description: 'Выбрать инструмент текста (рус)' },
25
+ { combination: 'r', actionId: 'tool-frame', description: 'Выбрать инструмент рамки' },
26
+ { combination: 'к', actionId: 'tool-frame', description: 'Выбрать инструмент рамки (рус)' },
27
+ { combination: 'arrowup', actionId: 'move-up', description: 'Переместить вверх', preventDefault: true },
28
+ { combination: 'arrowdown', actionId: 'move-down', description: 'Переместить вниз', preventDefault: true },
29
+ { combination: 'arrowleft', actionId: 'move-left', description: 'Переместить влево', preventDefault: true },
30
+ { combination: 'arrowright', actionId: 'move-right', description: 'Переместить вправо', preventDefault: true },
31
+ ];
@@ -0,0 +1,26 @@
1
+ import { Events } from '../events/Events.js';
2
+
3
+ export class KeyboardToolSwitching {
4
+ constructor(eventBus) {
5
+ this.eventBus = eventBus;
6
+ }
7
+
8
+ createHandler(actionId) {
9
+ switch (actionId) {
10
+ case 'tool-select':
11
+ return () => {
12
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
13
+ };
14
+ case 'tool-text':
15
+ return () => {
16
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'text' });
17
+ };
18
+ case 'tool-frame':
19
+ return () => {
20
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'frame' });
21
+ };
22
+ default:
23
+ return null;
24
+ }
25
+ }
26
+ }
@@ -327,14 +327,10 @@ export class ObjectRenderer {
327
327
  */
328
328
  setFrameFill(objectId, width, height, fillColor = 0xFFFFFF) {
329
329
  const pixiObject = this.objects.get(objectId);
330
- if (!pixiObject || !(pixiObject instanceof PIXI.Graphics)) return;
331
-
330
+ if (!pixiObject) return;
332
331
  const meta = pixiObject._mb || {};
333
- if (meta.type !== 'frame') return;
334
-
335
- if (meta.instance) {
336
- meta.instance.setFill(fillColor);
337
- }
332
+ if (meta.type !== 'frame' || !meta.instance) return;
333
+ meta.instance.setFill(fillColor);
338
334
  }
339
335
 
340
336
  /**
@@ -17,6 +17,8 @@ export class BaseGrid {
17
17
  // Размеры области отрисовки
18
18
  this.width = options.width || 1920;
19
19
  this.height = options.height || 1080;
20
+ /** @type {{ left: number, top: number, right: number, bottom: number } | null} */
21
+ this.viewportBounds = null;
20
22
 
21
23
  // PIXI графика
22
24
  this.graphics = new PIXI.Graphics();
@@ -133,8 +135,32 @@ export class BaseGrid {
133
135
  resize(width, height) {
134
136
  this.width = width;
135
137
  this.height = height;
138
+ this.viewportBounds = null;
136
139
  this.updateVisual();
137
140
  }
141
+
142
+ /**
143
+ * Устанавливает видимую область для непрерывной отрисовки при паннинге.
144
+ * @param {number} left
145
+ * @param {number} top
146
+ * @param {number} right
147
+ * @param {number} bottom
148
+ */
149
+ setVisibleBounds(left, top, right, bottom) {
150
+ this.viewportBounds = { left, top, right, bottom };
151
+ this.updateVisual();
152
+ }
153
+
154
+ /**
155
+ * Возвращает границы отрисовки: viewportBounds или (0,0,width,height)
156
+ * @returns {{ left: number, top: number, right: number, bottom: number }}
157
+ */
158
+ getDrawBounds() {
159
+ if (this.viewportBounds) {
160
+ return this.viewportBounds;
161
+ }
162
+ return { left: 0, top: 0, right: this.width, bottom: this.height };
163
+ }
138
164
 
139
165
  /**
140
166
  * Возвращает PIXI объект для рендеринга
@@ -34,16 +34,17 @@ export class CrossGrid extends BaseGrid {
34
34
  }
35
35
 
36
36
  const hs = this.crossHalfSize;
37
-
38
- for (let x = 0; x <= this.width; x += this.size) {
39
- for (let y = 0; y <= this.height; y += this.size) {
40
- // Выравниваем к полу-пикселю для чётких 1px линий
37
+ const b = this.getDrawBounds();
38
+ const startX = Math.floor(b.left / this.size) * this.size;
39
+ const startY = Math.floor(b.top / this.size) * this.size;
40
+ const endX = Math.ceil(b.right / this.size) * this.size;
41
+ const endY = Math.ceil(b.bottom / this.size) * this.size;
42
+ for (let x = startX; x <= endX; x += this.size) {
43
+ for (let y = startY; y <= endY; y += this.size) {
41
44
  const px = Math.round(x) + 0.5;
42
45
  const py = Math.round(y) + 0.5;
43
- // Горизонтальная часть креста
44
46
  g.moveTo(px - hs, py);
45
47
  g.lineTo(px + hs, py);
46
- // Вертикальная часть креста
47
48
  g.moveTo(px, py - hs);
48
49
  g.lineTo(px, py + hs);
49
50
  }