@sequent-org/moodboard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/package.json +44 -0
  2. package/src/assets/icons/README.md +105 -0
  3. package/src/assets/icons/attachments.svg +3 -0
  4. package/src/assets/icons/clear.svg +5 -0
  5. package/src/assets/icons/comments.svg +3 -0
  6. package/src/assets/icons/emoji.svg +6 -0
  7. package/src/assets/icons/frame.svg +3 -0
  8. package/src/assets/icons/image.svg +3 -0
  9. package/src/assets/icons/note.svg +3 -0
  10. package/src/assets/icons/pan.svg +3 -0
  11. package/src/assets/icons/pencil.svg +3 -0
  12. package/src/assets/icons/redo.svg +3 -0
  13. package/src/assets/icons/select.svg +9 -0
  14. package/src/assets/icons/shapes.svg +3 -0
  15. package/src/assets/icons/text-add.svg +3 -0
  16. package/src/assets/icons/topbar/README.md +39 -0
  17. package/src/assets/icons/topbar/grid-cross.svg +6 -0
  18. package/src/assets/icons/topbar/grid-dot.svg +3 -0
  19. package/src/assets/icons/topbar/grid-line.svg +3 -0
  20. package/src/assets/icons/topbar/grid-off.svg +3 -0
  21. package/src/assets/icons/topbar/paint.svg +3 -0
  22. package/src/assets/icons/undo.svg +3 -0
  23. package/src/core/ApiClient.js +309 -0
  24. package/src/core/EventBus.js +42 -0
  25. package/src/core/HistoryManager.js +261 -0
  26. package/src/core/KeyboardManager.js +710 -0
  27. package/src/core/PixiEngine.js +439 -0
  28. package/src/core/SaveManager.js +381 -0
  29. package/src/core/StateManager.js +64 -0
  30. package/src/core/commands/BaseCommand.js +68 -0
  31. package/src/core/commands/CopyObjectCommand.js +44 -0
  32. package/src/core/commands/CreateObjectCommand.js +46 -0
  33. package/src/core/commands/DeleteObjectCommand.js +146 -0
  34. package/src/core/commands/EditFileNameCommand.js +107 -0
  35. package/src/core/commands/GroupMoveCommand.js +47 -0
  36. package/src/core/commands/GroupReorderZCommand.js +74 -0
  37. package/src/core/commands/GroupResizeCommand.js +37 -0
  38. package/src/core/commands/GroupRotateCommand.js +41 -0
  39. package/src/core/commands/MoveObjectCommand.js +89 -0
  40. package/src/core/commands/PasteObjectCommand.js +103 -0
  41. package/src/core/commands/ReorderZCommand.js +45 -0
  42. package/src/core/commands/ResizeObjectCommand.js +135 -0
  43. package/src/core/commands/RotateObjectCommand.js +70 -0
  44. package/src/core/commands/index.js +14 -0
  45. package/src/core/events/Events.js +147 -0
  46. package/src/core/index.js +1632 -0
  47. package/src/core/rendering/GeometryUtils.js +89 -0
  48. package/src/core/rendering/HitTestManager.js +186 -0
  49. package/src/core/rendering/LayerManager.js +137 -0
  50. package/src/core/rendering/ObjectRenderer.js +363 -0
  51. package/src/core/rendering/PixiRenderer.js +140 -0
  52. package/src/core/rendering/index.js +9 -0
  53. package/src/grid/BaseGrid.js +164 -0
  54. package/src/grid/CrossGrid.js +75 -0
  55. package/src/grid/DotGrid.js +148 -0
  56. package/src/grid/GridFactory.js +173 -0
  57. package/src/grid/LineGrid.js +115 -0
  58. package/src/index.js +2 -0
  59. package/src/moodboard/ActionHandler.js +114 -0
  60. package/src/moodboard/DataManager.js +114 -0
  61. package/src/moodboard/MoodBoard.js +359 -0
  62. package/src/moodboard/WorkspaceManager.js +103 -0
  63. package/src/objects/BaseObject.js +1 -0
  64. package/src/objects/CommentObject.js +115 -0
  65. package/src/objects/DrawingObject.js +114 -0
  66. package/src/objects/EmojiObject.js +98 -0
  67. package/src/objects/FileObject.js +318 -0
  68. package/src/objects/FrameObject.js +127 -0
  69. package/src/objects/ImageObject.js +72 -0
  70. package/src/objects/NoteObject.js +227 -0
  71. package/src/objects/ObjectFactory.js +61 -0
  72. package/src/objects/ShapeObject.js +134 -0
  73. package/src/objects/StampObject.js +0 -0
  74. package/src/objects/StickerObject.js +0 -0
  75. package/src/objects/TextObject.js +123 -0
  76. package/src/services/BoardService.js +85 -0
  77. package/src/services/FileUploadService.js +398 -0
  78. package/src/services/FrameService.js +138 -0
  79. package/src/services/ImageUploadService.js +246 -0
  80. package/src/services/ZOrderManager.js +50 -0
  81. package/src/services/ZoomPanController.js +78 -0
  82. package/src/src.7z +0 -0
  83. package/src/src.zip +0 -0
  84. package/src/src2.zip +0 -0
  85. package/src/tools/AlignmentGuides.js +326 -0
  86. package/src/tools/BaseTool.js +257 -0
  87. package/src/tools/ResizeHandles.js +381 -0
  88. package/src/tools/ToolManager.js +580 -0
  89. package/src/tools/board-tools/PanTool.js +43 -0
  90. package/src/tools/board-tools/ZoomTool.js +393 -0
  91. package/src/tools/object-tools/DrawingTool.js +404 -0
  92. package/src/tools/object-tools/PlacementTool.js +1005 -0
  93. package/src/tools/object-tools/SelectTool.js +2183 -0
  94. package/src/tools/object-tools/TextTool.js +416 -0
  95. package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
  96. package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
  97. package/src/tools/object-tools/selection/GroupDragController.js +61 -0
  98. package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
  99. package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
  100. package/src/tools/object-tools/selection/HandlesSync.js +96 -0
  101. package/src/tools/object-tools/selection/ResizeController.js +68 -0
  102. package/src/tools/object-tools/selection/RotateController.js +58 -0
  103. package/src/tools/object-tools/selection/SelectionModel.js +42 -0
  104. package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
  105. package/src/ui/CommentPopover.js +187 -0
  106. package/src/ui/ContextMenu.js +340 -0
  107. package/src/ui/FilePropertiesPanel.js +298 -0
  108. package/src/ui/FramePropertiesPanel.js +462 -0
  109. package/src/ui/HtmlHandlesLayer.js +778 -0
  110. package/src/ui/HtmlTextLayer.js +279 -0
  111. package/src/ui/MapPanel.js +290 -0
  112. package/src/ui/NotePropertiesPanel.js +502 -0
  113. package/src/ui/SaveStatus.js +250 -0
  114. package/src/ui/TextPropertiesPanel.js +911 -0
  115. package/src/ui/Toolbar.js +1118 -0
  116. package/src/ui/Topbar.js +220 -0
  117. package/src/ui/ZoomPanel.js +116 -0
  118. package/src/ui/styles/workspace.css +854 -0
  119. package/src/utils/colors.js +0 -0
  120. package/src/utils/geometry.js +0 -0
  121. package/src/utils/iconLoader.js +270 -0
  122. package/src/utils/objectIdGenerator.js +17 -0
  123. package/src/utils/topbarIconLoader.js +114 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Сервис для загрузки и управления изображениями на сервере
3
+ */
4
+ export class ImageUploadService {
5
+ constructor(apiClient) {
6
+ this.apiClient = apiClient;
7
+ this.uploadEndpoint = '/api/images/upload';
8
+ this.deleteEndpoint = '/api/images';
9
+ }
10
+
11
+ /**
12
+ * Загружает изображение на сервер
13
+ * @param {File|Blob} file - файл изображения
14
+ * @param {string} name - имя файла
15
+ * @returns {Promise<{id: string, url: string, width: number, height: number}>}
16
+ */
17
+ async uploadImage(file, name = null) {
18
+ try {
19
+ // Получаем размеры изображения перед загрузкой
20
+ const dimensions = await this._getImageDimensions(file);
21
+
22
+ // Создаем FormData для отправки файла
23
+ const formData = new FormData();
24
+ formData.append('image', file);
25
+ formData.append('name', name || file.name || 'image.png');
26
+ formData.append('width', dimensions.width.toString());
27
+ formData.append('height', dimensions.height.toString());
28
+
29
+ // Получаем CSRF токен
30
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
31
+
32
+ if (!csrfToken) {
33
+ throw new Error('CSRF токен не найден');
34
+ }
35
+
36
+ const response = await fetch(this.uploadEndpoint, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'X-CSRF-TOKEN': csrfToken,
40
+ 'X-Requested-With': 'XMLHttpRequest'
41
+ },
42
+ credentials: 'same-origin',
43
+ body: formData
44
+ });
45
+
46
+ if (!response.ok) {
47
+ const errorData = await response.json().catch(() => null);
48
+ throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
49
+ }
50
+
51
+ const result = await response.json();
52
+
53
+ if (!result.success) {
54
+ throw new Error(result.message || 'Ошибка загрузки изображения');
55
+ }
56
+
57
+ return {
58
+ id: result.data.id,
59
+ url: result.data.url,
60
+ width: result.data.width,
61
+ height: result.data.height,
62
+ name: result.data.name,
63
+ size: result.data.size
64
+ };
65
+
66
+ } catch (error) {
67
+ console.error('Ошибка загрузки изображения:', error);
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Загружает изображение из base64 DataURL
74
+ * @param {string} dataUrl - base64 DataURL
75
+ * @param {string} name - имя файла
76
+ * @returns {Promise<{id: string, url: string, width: number, height: number}>}
77
+ */
78
+ async uploadFromDataUrl(dataUrl, name = 'clipboard-image.png') {
79
+ const blob = await this._dataUrlToBlob(dataUrl);
80
+ return this.uploadImage(blob, name);
81
+ }
82
+
83
+ /**
84
+ * Удаляет изображение с сервера
85
+ * @param {string} imageId - ID изображения
86
+ */
87
+ async deleteImage(imageId) {
88
+ try {
89
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
90
+
91
+ const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
92
+ method: 'DELETE',
93
+ headers: {
94
+ 'X-CSRF-TOKEN': csrfToken,
95
+ 'X-Requested-With': 'XMLHttpRequest',
96
+ 'Accept': 'application/json'
97
+ },
98
+ credentials: 'same-origin'
99
+ });
100
+
101
+ if (!response.ok) {
102
+ const errorData = await response.json().catch(() => null);
103
+ throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
104
+ }
105
+
106
+ const result = await response.json();
107
+ return result.success;
108
+
109
+ } catch (error) {
110
+ console.error('Ошибка удаления изображения:', error);
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Очищает неиспользуемые изображения с сервера
117
+ * @returns {Promise<{deletedCount: number, errors: Array}>}
118
+ */
119
+ async cleanupUnusedImages() {
120
+ try {
121
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
122
+
123
+ const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'X-CSRF-TOKEN': csrfToken,
127
+ 'X-Requested-With': 'XMLHttpRequest',
128
+ 'Accept': 'application/json'
129
+ },
130
+ credentials: 'same-origin'
131
+ });
132
+
133
+ if (!response.ok) {
134
+ const errorData = await response.json().catch(() => null);
135
+ throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
136
+ }
137
+
138
+ const result = await response.json();
139
+
140
+ if (result.success) {
141
+ // Защитная проверка на существование result.data
142
+ const data = result.data || {};
143
+ return {
144
+ deletedCount: data.deleted_count || 0,
145
+ errors: data.errors || []
146
+ };
147
+ } else {
148
+ throw new Error(result.message || 'Ошибка очистки изображений');
149
+ }
150
+
151
+ } catch (error) {
152
+ console.error('Ошибка очистки неиспользуемых изображений:', error);
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Получает информацию об изображении
159
+ * @param {string} imageId - ID изображения
160
+ */
161
+ async getImageInfo(imageId) {
162
+ try {
163
+ const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
164
+ method: 'GET',
165
+ headers: {
166
+ 'Accept': 'application/json',
167
+ 'X-Requested-With': 'XMLHttpRequest'
168
+ },
169
+ credentials: 'same-origin'
170
+ });
171
+
172
+ if (!response.ok) {
173
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
174
+ }
175
+
176
+ const result = await response.json();
177
+ return result.data;
178
+
179
+ } catch (error) {
180
+ console.error('Ошибка получения информации об изображении:', error);
181
+ throw error;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Получает размеры изображения из файла или blob
187
+ * @private
188
+ */
189
+ _getImageDimensions(file) {
190
+ return new Promise((resolve, reject) => {
191
+ const img = new Image();
192
+ const url = URL.createObjectURL(file);
193
+
194
+ img.onload = () => {
195
+ resolve({
196
+ width: img.naturalWidth || img.width,
197
+ height: img.naturalHeight || img.height
198
+ });
199
+ URL.revokeObjectURL(url);
200
+ };
201
+
202
+ img.onerror = () => {
203
+ reject(new Error('Не удалось загрузить изображение для определения размеров'));
204
+ URL.revokeObjectURL(url);
205
+ };
206
+
207
+ img.src = url;
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Конвертирует DataURL в Blob
213
+ * @private
214
+ */
215
+ _dataUrlToBlob(dataUrl) {
216
+ return new Promise((resolve) => {
217
+ const arr = dataUrl.split(',');
218
+ const mime = arr[0].match(/:(.*?);/)[1];
219
+ const bstr = atob(arr[1]);
220
+ let n = bstr.length;
221
+ const u8arr = new Uint8Array(n);
222
+
223
+ while (n--) {
224
+ u8arr[n] = bstr.charCodeAt(n);
225
+ }
226
+
227
+ resolve(new Blob([u8arr], { type: mime }));
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Проверяет, является ли URL внешней ссылкой на изображение
233
+ */
234
+ static isExternalImageUrl(url) {
235
+ if (!url || typeof url !== 'string') return false;
236
+ return /^https?:\/\/.+\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
237
+ }
238
+
239
+ /**
240
+ * Проверяет, является ли строка base64 DataURL
241
+ */
242
+ static isDataUrl(str) {
243
+ if (!str || typeof str !== 'string') return false;
244
+ return /^data:image\/.+;base64,/.test(str);
245
+ }
246
+ }
@@ -0,0 +1,50 @@
1
+ import { Events } from '../core/events/Events.js';
2
+
3
+ export class ZOrderManager {
4
+ constructor(eventBus, pixi, state) {
5
+ this.eventBus = eventBus;
6
+ this.pixi = pixi;
7
+ this.state = state;
8
+ }
9
+
10
+ attach() {
11
+ const ensureFramesBottom = () => {
12
+ const arr = this.state.state.objects || [];
13
+ if (arr.length === 0) return;
14
+ const frames = [];
15
+ const others = [];
16
+ for (const o of arr) {
17
+ if (o?.type === 'frame') frames.push(o); else others.push(o);
18
+ }
19
+ const newOrder = [...frames, ...others];
20
+ let changed = false;
21
+ if (newOrder.length === arr.length) {
22
+ for (let i = 0; i < arr.length; i++) {
23
+ if (arr[i] !== newOrder[i]) { changed = true; break; }
24
+ }
25
+ }
26
+ if (!changed) return;
27
+ this.state.state.objects = newOrder;
28
+ const world = this.pixi?.worldLayer || this.pixi?.app?.stage;
29
+ if (world) world.sortableChildren = true;
30
+ let z = 0;
31
+ for (const o of this.state.state.objects || []) {
32
+ const pixi = this.pixi.objects.get(o.id);
33
+ if (!pixi) continue;
34
+ if (o.type === 'frame') {
35
+ pixi.zIndex = -100000;
36
+ } else {
37
+ pixi.zIndex = z++;
38
+ }
39
+ }
40
+ this.state.markDirty();
41
+ };
42
+
43
+ this.eventBus.on(Events.Object.Created, () => ensureFramesBottom());
44
+ this.eventBus.on(Events.Object.Deleted, () => ensureFramesBottom());
45
+ this.eventBus.on(Events.Object.Reordered, () => ensureFramesBottom());
46
+ this.eventBus.on(Events.UI.LayerGroupSendToBack, () => ensureFramesBottom());
47
+ }
48
+ }
49
+
50
+
@@ -0,0 +1,78 @@
1
+ import { Events } from '../core/events/Events.js';
2
+
3
+ export class ZoomPanController {
4
+ constructor(eventBus, pixi) {
5
+ this.eventBus = eventBus;
6
+ this.pixi = pixi;
7
+ }
8
+
9
+ attach() {
10
+ // Масштабирование колесом — глобально отрабатываем Ctrl+Wheel
11
+ this.eventBus.on(Events.Tool.WheelZoom, ({ x, y, delta }) => {
12
+ const factor = 1 + (-delta) * 0.0015;
13
+ const world = this.pixi.worldLayer || this.pixi.app.stage;
14
+ const oldScale = world.scale.x || 1;
15
+ const newScale = Math.max(0.1, Math.min(5, oldScale * factor));
16
+ if (newScale === oldScale) return;
17
+ // Вычисляем мировые координаты точки под курсором до изменения скейла
18
+ const worldX = (x - world.x) / oldScale;
19
+ const worldY = (y - world.y) / oldScale;
20
+ // Применяем новый скейл и пересчитываем позицию, чтобы точка под курсором осталась на месте
21
+ world.scale.set(newScale);
22
+ world.x = x - worldX * newScale;
23
+ world.y = y - worldY * newScale;
24
+ this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
25
+ });
26
+
27
+ // Кнопки зума из UI
28
+ this.eventBus.on(Events.UI.ZoomIn, () => {
29
+ const center = { x: this.pixi.app.view.clientWidth / 2, y: this.pixi.app.view.clientHeight / 2 };
30
+ this.eventBus.emit(Events.Tool.WheelZoom, { x: center.x, y: center.y, delta: -120 });
31
+ });
32
+ this.eventBus.on(Events.UI.ZoomOut, () => {
33
+ const center = { x: this.pixi.app.view.clientWidth / 2, y: this.pixi.app.view.clientHeight / 2 };
34
+ this.eventBus.emit(Events.Tool.WheelZoom, { x: center.x, y: center.y, delta: 120 });
35
+ });
36
+ this.eventBus.on(Events.UI.ZoomReset, () => {
37
+ const world = this.pixi.worldLayer || this.pixi.app.stage;
38
+ const centerX = this.pixi.app.view.clientWidth / 2;
39
+ const centerY = this.pixi.app.view.clientHeight / 2;
40
+ const oldScale = world.scale.x || 1;
41
+ const worldX = (centerX - world.x) / oldScale;
42
+ const worldY = (centerY - world.y) / oldScale;
43
+ world.scale.set(1);
44
+ world.x = centerX - worldX * 1;
45
+ world.y = centerY - worldY * 1;
46
+ this.eventBus.emit(Events.UI.ZoomPercent, { percentage: 100 });
47
+ });
48
+ this.eventBus.on(Events.UI.ZoomFit, () => {
49
+ const objs = (this.pixi?.objects ? Array.from(this.pixi.objects.values()) : []);
50
+ if (objs.length === 0) return;
51
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
52
+ for (const p of objs) {
53
+ const b = p.getBounds();
54
+ minX = Math.min(minX, b.x);
55
+ minY = Math.min(minY, b.y);
56
+ maxX = Math.max(maxX, b.x + b.width);
57
+ maxY = Math.max(maxY, b.y + b.height);
58
+ }
59
+ const bboxW = Math.max(1, maxX - minX);
60
+ const bboxH = Math.max(1, maxY - minY);
61
+ const viewW = this.pixi.app.view.clientWidth;
62
+ const viewH = this.pixi.app.view.clientHeight;
63
+ const padding = 40;
64
+ const scaleX = (viewW - padding) / bboxW;
65
+ const scaleY = (viewH - padding) / bboxH;
66
+ const newScale = Math.max(0.1, Math.min(5, Math.min(scaleX, scaleY)));
67
+ const world = this.pixi.worldLayer || this.pixi.app.stage;
68
+ const worldCenterX = minX + bboxW / 2;
69
+ const worldCenterY = minY + bboxH / 2;
70
+ world.scale.set(newScale);
71
+ world.x = viewW / 2 - worldCenterX * newScale;
72
+ world.y = viewH / 2 - worldCenterY * newScale;
73
+ this.eventBus.emit(Events.UI.ZoomPercent, { percentage: Math.round(newScale * 100) });
74
+ });
75
+ }
76
+ }
77
+
78
+
package/src/src.7z ADDED
Binary file
package/src/src.zip ADDED
Binary file
package/src/src2.zip ADDED
Binary file