@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,114 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * Класс объекта «Рисунок» (карандаш/маркер)
5
+ * Хранит точки и настраивает отрисовку с учётом режима, толщины, цвета.
6
+ */
7
+ export class DrawingObject {
8
+ /**
9
+ * @param {Object} objectData
10
+ * - properties.mode: 'pencil' | 'marker'
11
+ * - properties.strokeColor: number
12
+ * - properties.strokeWidth: number
13
+ * - properties.points: Array<{x:number,y:number}>
14
+ * - width/height: габариты для первичного масштаба (base)
15
+ */
16
+ constructor(objectData = {}) {
17
+ this.objectData = objectData;
18
+ this.mode = objectData.properties?.mode || 'pencil';
19
+ this.color = objectData.properties?.strokeColor ?? 0x111827;
20
+ this.strokeWidth = objectData.properties?.strokeWidth ?? 2;
21
+ this.points = Array.isArray(objectData.properties?.points) ? objectData.properties.points : [];
22
+
23
+ // Базовые размеры для последующего масштабирования
24
+ this.baseWidth = objectData.properties?.baseWidth || objectData.width || 1;
25
+ this.baseHeight = objectData.properties?.baseHeight || objectData.height || 1;
26
+
27
+ this.graphics = new PIXI.Graphics();
28
+ this._draw(this.points, this.color, this.strokeWidth, this.mode);
29
+
30
+ // Сохраняем мета для hit-test/resize
31
+ this.graphics._mb = {
32
+ ...(this.graphics._mb || {}),
33
+ type: 'drawing',
34
+ properties: {
35
+ mode: this.mode,
36
+ strokeColor: this.color,
37
+ strokeWidth: this.strokeWidth,
38
+ points: this.points,
39
+ baseWidth: this.baseWidth,
40
+ baseHeight: this.baseHeight
41
+ }
42
+ };
43
+ }
44
+
45
+ getPixi() {
46
+ return this.graphics;
47
+ }
48
+
49
+ /** Обновить визуал без изменения точек */
50
+ setStyle({ mode, strokeColor, strokeWidth } = {}) {
51
+ if (mode) this.mode = mode;
52
+ if (typeof strokeColor === 'number') this.color = strokeColor;
53
+ if (typeof strokeWidth === 'number') this.strokeWidth = strokeWidth;
54
+ this._redrawPreserveTransform(this.points);
55
+ }
56
+
57
+ /** Обновить точки (после дорисовки) */
58
+ setPoints(points) {
59
+ this.points = Array.isArray(points) ? points : [];
60
+ this._redrawPreserveTransform(this.points);
61
+ }
62
+
63
+ /** Изменение габаритов — масштабируем визуал относительно базовых размеров */
64
+ updateSize(size) {
65
+ if (!size) return;
66
+ const w = Math.max(1, size.width || 1);
67
+ const h = Math.max(1, size.height || 1);
68
+ const scaleX = w / (this.baseWidth || 1);
69
+ const scaleY = h / (this.baseHeight || 1);
70
+ const scaled = this.points.map(p => ({ x: p.x * scaleX, y: p.y * scaleY }));
71
+ this._redrawPreserveTransform(scaled);
72
+ }
73
+
74
+ _redrawPreserveTransform(points) {
75
+ const g = this.graphics;
76
+ const x = g.x;
77
+ const y = g.y;
78
+ const rot = g.rotation || 0;
79
+ const pivotX = g.pivot?.x || 0;
80
+ const pivotY = g.pivot?.y || 0;
81
+ this._draw(points, this.color, this.strokeWidth, this.mode);
82
+ g.pivot.set(pivotX, pivotY);
83
+ g.x = x;
84
+ g.y = y;
85
+ g.rotation = rot;
86
+ }
87
+
88
+ _draw(points, color, strokeWidth, mode) {
89
+ const g = this.graphics;
90
+ g.clear();
91
+ const isMarker = mode === 'marker';
92
+ const lineWidth = isMarker ? strokeWidth * 2 : strokeWidth;
93
+ const alpha = isMarker ? 0.6 : 1;
94
+ g.lineStyle({ width: lineWidth, color, alpha, cap: 'round', join: 'round', miterLimit: 2, alignment: 0.5 });
95
+ g.blendMode = isMarker ? PIXI.BLEND_MODES.LIGHTEN : PIXI.BLEND_MODES.NORMAL;
96
+ if (!points || points.length === 0) return;
97
+ if (points.length < 3) {
98
+ g.moveTo(points[0].x, points[0].y);
99
+ for (let i = 1; i < points.length; i++) g.lineTo(points[i].x, points[i].y);
100
+ } else {
101
+ g.moveTo(points[0].x, points[0].y);
102
+ for (let i = 1; i < points.length - 1; i++) {
103
+ const cx = points[i].x, cy = points[i].y;
104
+ const nx = points[i + 1].x, ny = points[i + 1].y;
105
+ const mx = (cx + nx) / 2, my = (cy + ny) / 2;
106
+ g.quadraticCurveTo(cx, cy, mx, my);
107
+ }
108
+ const pen = points[points.length - 2];
109
+ const last = points[points.length - 1];
110
+ g.quadraticCurveTo(pen.x, pen.y, last.x, last.y);
111
+ }
112
+ }
113
+ }
114
+
@@ -0,0 +1,98 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * Класс объекта «Эмоджи»
5
+ * Текстовый смайл с корректным масштабированием под заданные размеры
6
+ */
7
+ export class EmojiObject {
8
+ /**
9
+ * @param {Object} objectData
10
+ * - properties.content: строка-эмоджи
11
+ * - properties.fontSize: базовый размер шрифта
12
+ * - width/height: целевые размеры (при создании/ресайзе)
13
+ */
14
+ constructor(objectData = {}) {
15
+ this.objectData = objectData;
16
+ this.content = objectData.properties?.content || '🙂';
17
+ this.baseFontSize = objectData.properties?.fontSize || 48;
18
+
19
+ const style = new PIXI.TextStyle({
20
+ fontFamily: 'Segoe UI Emoji, Apple Color Emoji, Noto Color Emoji, Arial',
21
+ fontSize: this.baseFontSize
22
+ });
23
+
24
+ this.text = new PIXI.Text(this.content, style);
25
+ // Важный момент: якорь в левом верхнем углу, чтобы позиция соответствовала state.position
26
+ if (typeof this.text.anchor?.set === 'function') {
27
+ this.text.anchor.set(0, 0);
28
+ }
29
+
30
+ // Базовые размеры исходного глифа для дальнейшего масштабирования
31
+ const bounds = this.text.getLocalBounds();
32
+ this.baseW = Math.max(1, bounds.width || 1);
33
+ this.baseH = Math.max(1, bounds.height || 1);
34
+
35
+ // Если заданы целевые габариты — приводим к ним равномерным масштабом
36
+ const targetW = objectData.width || this.baseW;
37
+ const targetH = objectData.height || this.baseH;
38
+ this._applyUniformScaleToFit(targetW, targetH);
39
+
40
+ // Метаданные для движка
41
+ this.text._mb = {
42
+ ...(this.text._mb || {}),
43
+ type: 'emoji',
44
+ properties: {
45
+ content: this.content,
46
+ fontSize: this.baseFontSize,
47
+ baseW: this.baseW,
48
+ baseH: this.baseH
49
+ }
50
+ };
51
+ }
52
+
53
+ getPixi() {
54
+ return this.text;
55
+ }
56
+
57
+ setContent(content) {
58
+ this.content = content;
59
+ this.text.text = content;
60
+ const b = this.text.getLocalBounds();
61
+ this.baseW = Math.max(1, b.width || 1);
62
+ this.baseH = Math.max(1, b.height || 1);
63
+ }
64
+
65
+ setFontSize(fontSize) {
66
+ this.baseFontSize = fontSize;
67
+ this.text.style = new PIXI.TextStyle({
68
+ fontFamily: 'Segoe UI Emoji, Apple Color Emoji, Noto Color Emoji, Arial',
69
+ fontSize: this.baseFontSize
70
+ });
71
+ const b = this.text.getLocalBounds();
72
+ this.baseW = Math.max(1, b.width || 1);
73
+ this.baseH = Math.max(1, b.height || 1);
74
+ }
75
+
76
+ /** Масштабирование под указанные габариты без сдвига позиции */
77
+ updateSize(size) {
78
+ if (!size) return;
79
+ const t = this.text;
80
+ const prev = { x: t.x, y: t.y, rot: t.rotation, px: t.pivot?.x || 0, py: t.pivot?.y || 0 };
81
+ const w = Math.max(1, size.width || 1);
82
+ const h = Math.max(1, size.height || 1);
83
+ this._applyUniformScaleToFit(w, h);
84
+ t.pivot.set(prev.px, prev.py);
85
+ t.x = prev.x;
86
+ t.y = prev.y;
87
+ t.rotation = prev.rot;
88
+ }
89
+
90
+ _applyUniformScaleToFit(targetW, targetH) {
91
+ const sx = targetW / (this.baseW || 1);
92
+ const sy = targetH / (this.baseH || 1);
93
+ const s = Math.min(sx, sy);
94
+ this.text.scale.set(s, s);
95
+ }
96
+ }
97
+
98
+
@@ -0,0 +1,318 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * FileObject — объект файла, отображает иконку с названием и расширением
5
+ * Свойства (properties):
6
+ * - fileName: string — имя файла с расширением
7
+ * - fileSize: number — размер файла в байтах (опционально)
8
+ * - mimeType: string — MIME тип файла (опционально)
9
+ * - content: ArrayBuffer | Blob — содержимое файла (опционально)
10
+ */
11
+ export class FileObject {
12
+ constructor(objectData = {}) {
13
+ this.objectData = objectData;
14
+
15
+ // Размеры объекта файла
16
+ this.width = objectData.width || objectData.properties?.width || 120;
17
+ this.height = objectData.height || objectData.properties?.height || 140;
18
+
19
+ // Свойства файла
20
+ const props = objectData.properties || {};
21
+ this.fileName = props.fileName || 'Untitled';
22
+ this.fileSize = props.fileSize || 0;
23
+ this.mimeType = props.mimeType || 'application/octet-stream';
24
+ this.content = props.content || null;
25
+ this.isDeleted = props.isDeleted || false; // Флаг удаленного файла
26
+
27
+ // Создаем контейнер для файла
28
+ this.container = new PIXI.Container();
29
+
30
+ // Включаем интерактивность для контейнера
31
+ this.container.eventMode = 'static';
32
+ this.container.interactiveChildren = true;
33
+
34
+ // Графика фона и иконки
35
+ this.graphics = new PIXI.Graphics();
36
+ this.container.addChild(this.graphics);
37
+
38
+ // Текст имени файла
39
+ this.fileNameText = new PIXI.Text(this.fileName, {
40
+ fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
41
+ fontSize: 12,
42
+ fill: 0x333333,
43
+ align: 'center',
44
+ wordWrap: true,
45
+ wordWrapWidth: this.width - 8,
46
+ lineHeight: 14,
47
+ resolution: (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1
48
+ });
49
+
50
+ // Текст размера файла (если есть)
51
+ this.fileSizeText = null;
52
+ if (this.fileSize > 0) {
53
+ this.fileSizeText = new PIXI.Text(this._formatFileSize(this.fileSize), {
54
+ fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
55
+ fontSize: 10,
56
+ fill: 0x666666,
57
+ align: 'center',
58
+ resolution: (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1
59
+ });
60
+ }
61
+
62
+ this._redraw();
63
+ this.container.addChild(this.fileNameText);
64
+ if (this.fileSizeText) {
65
+ this.container.addChild(this.fileSizeText);
66
+ }
67
+ this._updateTextPosition();
68
+
69
+ // Метаданные
70
+ this.container._mb = {
71
+ ...(this.container._mb || {}),
72
+ type: 'file',
73
+ instance: this,
74
+ properties: {
75
+ fileName: this.fileName,
76
+ fileSize: this.fileSize,
77
+ mimeType: this.mimeType,
78
+ content: this.content,
79
+ ...objectData.properties
80
+ }
81
+ };
82
+ }
83
+
84
+ getPixi() {
85
+ return this.container;
86
+ }
87
+
88
+ updateSize(size) {
89
+ if (!size) return;
90
+ this.width = Math.max(80, size.width || this.width);
91
+ this.height = Math.max(100, size.height || this.height);
92
+
93
+ this._redraw();
94
+ this._updateTextPosition();
95
+
96
+ // Обновляем hit area
97
+ this.container.hitArea = new PIXI.Rectangle(0, 0, this.width, this.height);
98
+ this.container.containsPoint = (point) => {
99
+ const bounds = this.container.getBounds();
100
+ return point.x >= bounds.x &&
101
+ point.x <= bounds.x + bounds.width &&
102
+ point.y >= bounds.y &&
103
+ point.y <= bounds.y + bounds.height;
104
+ };
105
+ }
106
+
107
+ setFileName(fileName) {
108
+ this.fileName = fileName || 'Untitled';
109
+ this.fileNameText.text = this.fileName;
110
+ this._updateTextPosition();
111
+ if (this.container && this.container._mb) {
112
+ this.container._mb.properties = {
113
+ ...(this.container._mb.properties || {}),
114
+ fileName: this.fileName
115
+ };
116
+ }
117
+ this._redraw();
118
+ }
119
+
120
+ /**
121
+ * Скрывает текст названия файла (используется во время редактирования)
122
+ */
123
+ hideText() {
124
+ if (this.fileNameText) {
125
+ this.fileNameText.visible = false;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Показывает текст названия файла (используется после завершения редактирования)
131
+ */
132
+ showText() {
133
+ if (this.fileNameText) {
134
+ this.fileNameText.visible = true;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Получает текущее название файла
140
+ */
141
+ getFileName() {
142
+ return this.fileName;
143
+ }
144
+
145
+ _redraw() {
146
+ const g = this.graphics;
147
+ const w = this.width;
148
+ const h = this.height;
149
+
150
+ g.clear();
151
+
152
+ // Тень
153
+ g.beginFill(0x000000, 0.1);
154
+ g.drawRoundedRect(2, 2, w, h, 8);
155
+ g.endFill();
156
+
157
+ // Основной фон (блеклый для удаленных файлов)
158
+ const bgColor = this.isDeleted ? 0xF1F3F4 : 0xF8F9FA;
159
+ const borderColor = this.isDeleted ? 0xDADCE0 : 0xDEE2E6;
160
+ g.beginFill(bgColor, 1);
161
+ g.lineStyle(2, borderColor, 1);
162
+ g.drawRoundedRect(0, 0, w, h, 8);
163
+ g.endFill();
164
+
165
+ // Иконка файла в верхней части
166
+ const iconSize = Math.min(48, w * 0.4);
167
+ const iconX = (w - iconSize) / 2;
168
+ const iconY = 16;
169
+
170
+ // Определяем цвет иконки по расширению файла
171
+ const extension = this._getFileExtension();
172
+ const iconColor = this.isDeleted ? 0x6B7280 : this._getIconColor(extension); // Серый цвет для удаленных файлов
173
+
174
+ // Рисуем иконку файла
175
+ this._drawFileIcon(g, iconX, iconY, iconSize, iconColor, extension);
176
+
177
+ // Устанавливаем hit area
178
+ this.container.hitArea = new PIXI.Rectangle(0, 0, w, h);
179
+
180
+ this.container.containsPoint = (point) => {
181
+ const bounds = this.container.getBounds();
182
+ return point.x >= bounds.x &&
183
+ point.x <= bounds.x + bounds.width &&
184
+ point.y >= bounds.y &&
185
+ point.y <= bounds.y + bounds.height;
186
+ };
187
+ }
188
+
189
+ _drawFileIcon(graphics, x, y, size, color, extension) {
190
+ const g = graphics;
191
+
192
+ // Основная часть файла
193
+ g.beginFill(color, 1);
194
+ g.lineStyle(1, color, 1);
195
+ g.drawRoundedRect(x, y, size * 0.8, size, 4);
196
+ g.endFill();
197
+
198
+ // Загнутый уголок
199
+ const cornerSize = size * 0.25;
200
+ g.beginFill(0xFFFFFF, 0.8);
201
+ g.moveTo(x + size * 0.8 - cornerSize, y);
202
+ g.lineTo(x + size * 0.8, y);
203
+ g.lineTo(x + size * 0.8, y + cornerSize);
204
+ g.lineTo(x + size * 0.8 - cornerSize, y);
205
+ g.endFill();
206
+
207
+ // Текст расширения на иконке
208
+ if (extension && extension.length <= 4) {
209
+ const extensionText = new PIXI.Text(extension.toUpperCase(), {
210
+ fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
211
+ fontSize: Math.max(8, size * 0.2),
212
+ fill: 0xFFFFFF,
213
+ align: 'center',
214
+ fontWeight: 'bold'
215
+ });
216
+ extensionText.anchor.set(0.5, 0.5);
217
+ extensionText.x = x + size * 0.4;
218
+ extensionText.y = y + size * 0.7;
219
+ this.container.addChild(extensionText);
220
+ }
221
+ }
222
+
223
+ _updateTextPosition() {
224
+ if (!this.fileNameText) return;
225
+
226
+ // Обновляем стиль текста
227
+ this.fileNameText.style.wordWrapWidth = this.width - 8;
228
+ this.fileNameText.updateText();
229
+
230
+ // Позиционируем название файла
231
+ this.fileNameText.anchor.set(0.5, 0);
232
+ this.fileNameText.x = this.width / 2;
233
+ this.fileNameText.y = this.height - 40;
234
+
235
+ // Позиционируем размер файла
236
+ if (this.fileSizeText) {
237
+ this.fileSizeText.anchor.set(0.5, 0);
238
+ this.fileSizeText.x = this.width / 2;
239
+ this.fileSizeText.y = this.height - 20;
240
+ }
241
+ }
242
+
243
+ _getFileExtension() {
244
+ const parts = this.fileName.split('.');
245
+ return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
246
+ }
247
+
248
+ _getIconColor(extension) {
249
+ const colorMap = {
250
+ // Документы
251
+ 'pdf': 0xDC2626,
252
+ 'doc': 0x2563EB,
253
+ 'docx': 0x2563EB,
254
+ 'txt': 0x6B7280,
255
+ 'rtf': 0x6B7280,
256
+
257
+ // Изображения
258
+ 'jpg': 0x10B981,
259
+ 'jpeg': 0x10B981,
260
+ 'png': 0x10B981,
261
+ 'gif': 0x10B981,
262
+ 'svg': 0x8B5CF6,
263
+ 'bmp': 0x10B981,
264
+ 'webp': 0x10B981,
265
+
266
+ // Архивы
267
+ 'zip': 0xF59E0B,
268
+ 'rar': 0xF59E0B,
269
+ '7z': 0xF59E0B,
270
+ 'tar': 0xF59E0B,
271
+ 'gz': 0xF59E0B,
272
+
273
+ // Видео
274
+ 'mp4': 0xEF4444,
275
+ 'avi': 0xEF4444,
276
+ 'mov': 0xEF4444,
277
+ 'wmv': 0xEF4444,
278
+ 'flv': 0xEF4444,
279
+
280
+ // Аудио
281
+ 'mp3': 0x8B5CF6,
282
+ 'wav': 0x8B5CF6,
283
+ 'flac': 0x8B5CF6,
284
+ 'aac': 0x8B5CF6,
285
+
286
+ // Код
287
+ 'js': 0xF7DF1E,
288
+ 'html': 0xE34F26,
289
+ 'css': 0x1572B6,
290
+ 'json': 0x000000,
291
+ 'xml': 0xFF6600,
292
+ 'php': 0x777BB4,
293
+ 'py': 0x3776AB,
294
+ 'java': 0xED8B00,
295
+ 'cpp': 0x00599C,
296
+ 'c': 0x00599C,
297
+
298
+ // Таблицы
299
+ 'xls': 0x217346,
300
+ 'xlsx': 0x217346,
301
+ 'csv': 0x217346,
302
+
303
+ // Презентации
304
+ 'ppt': 0xD24726,
305
+ 'pptx': 0xD24726
306
+ };
307
+
308
+ return colorMap[extension] || 0x6B7280; // Серый по умолчанию
309
+ }
310
+
311
+ _formatFileSize(bytes) {
312
+ if (bytes === 0) return '0 B';
313
+ const k = 1024;
314
+ const sizes = ['B', 'KB', 'MB', 'GB'];
315
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
316
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
317
+ }
318
+ }
@@ -0,0 +1,127 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * Класс объекта «Фрейм» (контейнерная прямоугольная область)
5
+ * Отвечает за создание PIXI-графики, изменение размеров и изменение заливки.
6
+ */
7
+ export class FrameObject {
8
+ /**
9
+ * @param {Object} objectData Полные данные объекта из состояния
10
+ */
11
+ constructor(objectData) {
12
+ this.objectData = objectData || {};
13
+ this.width = this.objectData.width || 100;
14
+ this.height = this.objectData.height || 100;
15
+ this.borderWidth = 2;
16
+ // Используем backgroundColor из данных объекта, если есть, иначе белый
17
+ this.fillColor = this.objectData.backgroundColor || this.objectData.properties?.backgroundColor || 0xFFFFFF;
18
+ this.strokeColor = this.objectData.borderColor || 0x333333;
19
+ this.title = this.objectData.title || this.objectData.properties?.title || 'Новый';
20
+
21
+ // Создаем контейнер для фрейма и заголовка
22
+ this.container = new PIXI.Container();
23
+
24
+ // Графика для прямоугольника фрейма
25
+ this.graphics = new PIXI.Graphics();
26
+ this.container.addChild(this.graphics);
27
+
28
+ // Текст заголовка
29
+ this.titleText = new PIXI.Text(this.title, {
30
+ fontFamily: 'Arial, sans-serif',
31
+ fontSize: 14,
32
+ fill: 0x333333,
33
+ fontWeight: 'bold'
34
+ });
35
+ this.titleText.anchor.set(0, 1); // Левый нижний угол текста
36
+ this.titleText.y = -5; // Немного выше фрейма
37
+ this.container.addChild(this.titleText);
38
+
39
+ this._draw(this.width, this.height, this.fillColor);
40
+ }
41
+
42
+ /**
43
+ * Возвращает PIXI-объект
44
+ */
45
+ getPixi() {
46
+ return this.container;
47
+ }
48
+
49
+ /**
50
+ * Установить цвет заливки фрейма (без изменения размеров)
51
+ * @param {number} color Цвет заливки (hex)
52
+ */
53
+ setFill(color) {
54
+ if (typeof color === 'number') {
55
+ this.fillColor = color;
56
+ }
57
+ this._redrawPreserveTransform(this.width, this.height, this.fillColor);
58
+ }
59
+
60
+ /**
61
+ * Установить заголовок фрейма
62
+ * @param {string} title Новый заголовок
63
+ */
64
+ setTitle(title) {
65
+ this.title = title || 'Новый';
66
+ if (this.titleText) {
67
+ this.titleText.text = this.title;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Установить цвет фона фрейма
73
+ * @param {number} backgroundColor Цвет фона (hex)
74
+ */
75
+ setBackgroundColor(backgroundColor) {
76
+ if (typeof backgroundColor === 'number') {
77
+ this.fillColor = backgroundColor;
78
+ this._redrawPreserveTransform(this.width, this.height, this.fillColor);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Обновить размер фрейма
84
+ * @param {{width:number,height:number}} size
85
+ */
86
+ updateSize(size) {
87
+ if (!size) return;
88
+ const w = Math.max(0, size.width || 0);
89
+ const h = Math.max(0, size.height || 0);
90
+ this.width = w;
91
+ this.height = h;
92
+ this._redrawPreserveTransform(w, h, this.fillColor);
93
+ }
94
+
95
+ /**
96
+ * Перерисовать с сохранением трансформаций (позиция, pivot, rotation)
97
+ */
98
+ _redrawPreserveTransform(width, height, color) {
99
+ const container = this.container;
100
+ const x = container.x;
101
+ const y = container.y;
102
+ const rot = container.rotation || 0;
103
+ const pivotX = container.pivot?.x || 0;
104
+ const pivotY = container.pivot?.y || 0;
105
+
106
+ this._draw(width, height, color);
107
+
108
+ container.pivot.set(pivotX, pivotY);
109
+ container.x = x;
110
+ container.y = y;
111
+ container.rotation = rot;
112
+ }
113
+
114
+ /**
115
+ * Базовая отрисовка
116
+ */
117
+ _draw(width, height, color) {
118
+ const g = this.graphics;
119
+ g.clear();
120
+ g.lineStyle(this.borderWidth, this.strokeColor, 1);
121
+ g.beginFill(typeof color === 'number' ? color : 0xFFFFFF, 1);
122
+ const halfBorder = this.borderWidth / 2;
123
+ g.drawRect(halfBorder, halfBorder, Math.max(0, width - this.borderWidth), Math.max(0, height - this.borderWidth));
124
+ g.endFill();
125
+ }
126
+ }
127
+