@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.
- package/package.json +44 -0
- package/src/assets/icons/README.md +105 -0
- package/src/assets/icons/attachments.svg +3 -0
- package/src/assets/icons/clear.svg +5 -0
- package/src/assets/icons/comments.svg +3 -0
- package/src/assets/icons/emoji.svg +6 -0
- package/src/assets/icons/frame.svg +3 -0
- package/src/assets/icons/image.svg +3 -0
- package/src/assets/icons/note.svg +3 -0
- package/src/assets/icons/pan.svg +3 -0
- package/src/assets/icons/pencil.svg +3 -0
- package/src/assets/icons/redo.svg +3 -0
- package/src/assets/icons/select.svg +9 -0
- package/src/assets/icons/shapes.svg +3 -0
- package/src/assets/icons/text-add.svg +3 -0
- package/src/assets/icons/topbar/README.md +39 -0
- package/src/assets/icons/topbar/grid-cross.svg +6 -0
- package/src/assets/icons/topbar/grid-dot.svg +3 -0
- package/src/assets/icons/topbar/grid-line.svg +3 -0
- package/src/assets/icons/topbar/grid-off.svg +3 -0
- package/src/assets/icons/topbar/paint.svg +3 -0
- package/src/assets/icons/undo.svg +3 -0
- package/src/core/ApiClient.js +309 -0
- package/src/core/EventBus.js +42 -0
- package/src/core/HistoryManager.js +261 -0
- package/src/core/KeyboardManager.js +710 -0
- package/src/core/PixiEngine.js +439 -0
- package/src/core/SaveManager.js +381 -0
- package/src/core/StateManager.js +64 -0
- package/src/core/commands/BaseCommand.js +68 -0
- package/src/core/commands/CopyObjectCommand.js +44 -0
- package/src/core/commands/CreateObjectCommand.js +46 -0
- package/src/core/commands/DeleteObjectCommand.js +146 -0
- package/src/core/commands/EditFileNameCommand.js +107 -0
- package/src/core/commands/GroupMoveCommand.js +47 -0
- package/src/core/commands/GroupReorderZCommand.js +74 -0
- package/src/core/commands/GroupResizeCommand.js +37 -0
- package/src/core/commands/GroupRotateCommand.js +41 -0
- package/src/core/commands/MoveObjectCommand.js +89 -0
- package/src/core/commands/PasteObjectCommand.js +103 -0
- package/src/core/commands/ReorderZCommand.js +45 -0
- package/src/core/commands/ResizeObjectCommand.js +135 -0
- package/src/core/commands/RotateObjectCommand.js +70 -0
- package/src/core/commands/index.js +14 -0
- package/src/core/events/Events.js +147 -0
- package/src/core/index.js +1632 -0
- package/src/core/rendering/GeometryUtils.js +89 -0
- package/src/core/rendering/HitTestManager.js +186 -0
- package/src/core/rendering/LayerManager.js +137 -0
- package/src/core/rendering/ObjectRenderer.js +363 -0
- package/src/core/rendering/PixiRenderer.js +140 -0
- package/src/core/rendering/index.js +9 -0
- package/src/grid/BaseGrid.js +164 -0
- package/src/grid/CrossGrid.js +75 -0
- package/src/grid/DotGrid.js +148 -0
- package/src/grid/GridFactory.js +173 -0
- package/src/grid/LineGrid.js +115 -0
- package/src/index.js +2 -0
- package/src/moodboard/ActionHandler.js +114 -0
- package/src/moodboard/DataManager.js +114 -0
- package/src/moodboard/MoodBoard.js +359 -0
- package/src/moodboard/WorkspaceManager.js +103 -0
- package/src/objects/BaseObject.js +1 -0
- package/src/objects/CommentObject.js +115 -0
- package/src/objects/DrawingObject.js +114 -0
- package/src/objects/EmojiObject.js +98 -0
- package/src/objects/FileObject.js +318 -0
- package/src/objects/FrameObject.js +127 -0
- package/src/objects/ImageObject.js +72 -0
- package/src/objects/NoteObject.js +227 -0
- package/src/objects/ObjectFactory.js +61 -0
- package/src/objects/ShapeObject.js +134 -0
- package/src/objects/StampObject.js +0 -0
- package/src/objects/StickerObject.js +0 -0
- package/src/objects/TextObject.js +123 -0
- package/src/services/BoardService.js +85 -0
- package/src/services/FileUploadService.js +398 -0
- package/src/services/FrameService.js +138 -0
- package/src/services/ImageUploadService.js +246 -0
- package/src/services/ZOrderManager.js +50 -0
- package/src/services/ZoomPanController.js +78 -0
- package/src/src.7z +0 -0
- package/src/src.zip +0 -0
- package/src/src2.zip +0 -0
- package/src/tools/AlignmentGuides.js +326 -0
- package/src/tools/BaseTool.js +257 -0
- package/src/tools/ResizeHandles.js +381 -0
- package/src/tools/ToolManager.js +580 -0
- package/src/tools/board-tools/PanTool.js +43 -0
- package/src/tools/board-tools/ZoomTool.js +393 -0
- package/src/tools/object-tools/DrawingTool.js +404 -0
- package/src/tools/object-tools/PlacementTool.js +1005 -0
- package/src/tools/object-tools/SelectTool.js +2183 -0
- package/src/tools/object-tools/TextTool.js +416 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
- package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
- package/src/tools/object-tools/selection/GroupDragController.js +61 -0
- package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
- package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
- package/src/tools/object-tools/selection/HandlesSync.js +96 -0
- package/src/tools/object-tools/selection/ResizeController.js +68 -0
- package/src/tools/object-tools/selection/RotateController.js +58 -0
- package/src/tools/object-tools/selection/SelectionModel.js +42 -0
- package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
- package/src/ui/CommentPopover.js +187 -0
- package/src/ui/ContextMenu.js +340 -0
- package/src/ui/FilePropertiesPanel.js +298 -0
- package/src/ui/FramePropertiesPanel.js +462 -0
- package/src/ui/HtmlHandlesLayer.js +778 -0
- package/src/ui/HtmlTextLayer.js +279 -0
- package/src/ui/MapPanel.js +290 -0
- package/src/ui/NotePropertiesPanel.js +502 -0
- package/src/ui/SaveStatus.js +250 -0
- package/src/ui/TextPropertiesPanel.js +911 -0
- package/src/ui/Toolbar.js +1118 -0
- package/src/ui/Topbar.js +220 -0
- package/src/ui/ZoomPanel.js +116 -0
- package/src/ui/styles/workspace.css +854 -0
- package/src/utils/colors.js +0 -0
- package/src/utils/geometry.js +0 -0
- package/src/utils/iconLoader.js +270 -0
- package/src/utils/objectIdGenerator.js +17 -0
- 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
|
+
|