@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,72 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ImageObject — отображение загруженного изображения как спрайт
|
|
5
|
+
*/
|
|
6
|
+
export class ImageObject {
|
|
7
|
+
constructor(objectData = {}) {
|
|
8
|
+
this.objectData = objectData;
|
|
9
|
+
let src = objectData.properties?.src || objectData.src;
|
|
10
|
+
// Не используем устаревшие blob: URL — они недолговечны и приводят к ERR_FILE_NOT_FOUND
|
|
11
|
+
if (typeof src === 'string' && src.startsWith('blob:')) {
|
|
12
|
+
src = null;
|
|
13
|
+
}
|
|
14
|
+
this.width = objectData.width || objectData.properties?.width || 200;
|
|
15
|
+
this.height = objectData.height || objectData.properties?.height || 150;
|
|
16
|
+
const texture = src ? PIXI.Texture.from(src) : PIXI.Texture.WHITE;
|
|
17
|
+
this.sprite = new PIXI.Sprite(texture);
|
|
18
|
+
this.sprite.anchor.set(0.5, 0.5); // центр для совместимости с позиционированием по центру
|
|
19
|
+
if (!src) {
|
|
20
|
+
this.sprite.tint = 0xcccccc;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fitToSize = () => {
|
|
24
|
+
const texW = this.sprite.texture.width || 1;
|
|
25
|
+
const texH = this.sprite.texture.height || 1;
|
|
26
|
+
const sx = this.width / texW;
|
|
27
|
+
const sy = this.height / texH;
|
|
28
|
+
this.sprite.scale.set(sx, sy);
|
|
29
|
+
// Обновим метаданные базовых размеров
|
|
30
|
+
this.sprite._mb = {
|
|
31
|
+
...(this.sprite._mb || {}),
|
|
32
|
+
type: 'image',
|
|
33
|
+
properties: { src, baseW: texW, baseH: texH }
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const onError = () => {
|
|
38
|
+
// Фолбек на плейсхолдер без спама ошибками
|
|
39
|
+
this.sprite.texture = PIXI.Texture.WHITE;
|
|
40
|
+
this.sprite.tint = 0xcccccc;
|
|
41
|
+
fitToSize();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (this.sprite.texture.baseTexture) {
|
|
45
|
+
this.sprite.texture.baseTexture.once('error', onError);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.sprite.texture.baseTexture?.valid) {
|
|
49
|
+
fitToSize();
|
|
50
|
+
} else if (this.sprite.texture.baseTexture) {
|
|
51
|
+
this.sprite.texture.baseTexture.once('loaded', fitToSize);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getPixi() {
|
|
56
|
+
return this.sprite;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updateSize(size) {
|
|
60
|
+
if (!size) return;
|
|
61
|
+
const w = Math.max(1, size.width || 1);
|
|
62
|
+
const h = Math.max(1, size.height || 1);
|
|
63
|
+
const apply = () => {
|
|
64
|
+
const texW = this.sprite.texture.width || 1;
|
|
65
|
+
const texH = this.sprite.texture.height || 1;
|
|
66
|
+
this.sprite.scale.set(w / texW, h / texH);
|
|
67
|
+
};
|
|
68
|
+
if (this.sprite.texture.baseTexture?.valid) apply(); else this.sprite.texture.baseTexture?.once('loaded', apply);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NoteObject — объект записки, стилизованный как стикер
|
|
5
|
+
* Свойства (properties):
|
|
6
|
+
* - content: string — содержимое записки
|
|
7
|
+
* - fontSize: number — размер шрифта (по умолчанию 14)
|
|
8
|
+
* - backgroundColor: number — цвет фона записки (по умолчанию желтоватый)
|
|
9
|
+
* - borderColor: number — цвет границы (по умолчанию темнее фона)
|
|
10
|
+
* - textColor: number — цвет текста (по умолчанию темный)
|
|
11
|
+
*/
|
|
12
|
+
export class NoteObject {
|
|
13
|
+
constructor(objectData = {}) {
|
|
14
|
+
this.objectData = objectData;
|
|
15
|
+
|
|
16
|
+
// Размеры записки
|
|
17
|
+
this.width = objectData.width || objectData.properties?.width || 160;
|
|
18
|
+
this.height = objectData.height || objectData.properties?.height || 100;
|
|
19
|
+
|
|
20
|
+
// Свойства записки
|
|
21
|
+
const props = objectData.properties || {};
|
|
22
|
+
this.content = props.content || '';
|
|
23
|
+
this.fontSize = props.fontSize || 16;
|
|
24
|
+
this.backgroundColor = (typeof props.backgroundColor === 'number') ? props.backgroundColor : 0xFFF9C4; // Светло-желтый
|
|
25
|
+
this.borderColor = (typeof props.borderColor === 'number') ? props.borderColor : 0xF9A825; // Золотистый
|
|
26
|
+
this.textColor = (typeof props.textColor === 'number') ? props.textColor : 0x1A1A1A; // Почти черный для лучшей контрастности
|
|
27
|
+
|
|
28
|
+
// Создаем контейнер для записки
|
|
29
|
+
this.container = new PIXI.Container();
|
|
30
|
+
|
|
31
|
+
// Включаем интерактивность для контейнера (PixiJS v7.2.0+)
|
|
32
|
+
this.container.eventMode = 'static';
|
|
33
|
+
this.container.interactiveChildren = true;
|
|
34
|
+
|
|
35
|
+
// Графика фона
|
|
36
|
+
this.graphics = new PIXI.Graphics();
|
|
37
|
+
this.container.addChild(this.graphics);
|
|
38
|
+
|
|
39
|
+
// Текст записки
|
|
40
|
+
this.textField = new PIXI.Text(this.content, {
|
|
41
|
+
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
|
|
42
|
+
fontSize: this.fontSize,
|
|
43
|
+
fill: this.textColor,
|
|
44
|
+
align: 'center',
|
|
45
|
+
wordWrap: true,
|
|
46
|
+
wordWrapWidth: this.width - 16, // Отступы по 8px с каждой стороны
|
|
47
|
+
lineHeight: this.fontSize * 1.2,
|
|
48
|
+
resolution: (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this._redraw(); // Сначала рисуем фон
|
|
52
|
+
this.container.addChild(this.textField); // Затем добавляем текст поверх
|
|
53
|
+
this._updateTextPosition();
|
|
54
|
+
|
|
55
|
+
// Отладочная информация
|
|
56
|
+
console.log('NoteObject created with content:', this.content);
|
|
57
|
+
|
|
58
|
+
// Метаданные
|
|
59
|
+
this.container._mb = {
|
|
60
|
+
...(this.container._mb || {}),
|
|
61
|
+
type: 'note',
|
|
62
|
+
instance: this, // Ссылка на сам объект для вызова методов
|
|
63
|
+
properties: {
|
|
64
|
+
content: this.content,
|
|
65
|
+
fontSize: this.fontSize,
|
|
66
|
+
backgroundColor: this.backgroundColor,
|
|
67
|
+
borderColor: this.borderColor,
|
|
68
|
+
textColor: this.textColor,
|
|
69
|
+
...objectData.properties
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
this._redraw();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getPixi() {
|
|
77
|
+
return this.container;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
updateSize(size) {
|
|
81
|
+
if (!size) return;
|
|
82
|
+
this.width = Math.max(80, size.width || this.width);
|
|
83
|
+
this.height = Math.max(60, size.height || this.height);
|
|
84
|
+
|
|
85
|
+
this._redraw();
|
|
86
|
+
this._updateTextPosition();
|
|
87
|
+
|
|
88
|
+
// Обновляем hit area и containsPoint
|
|
89
|
+
this.container.hitArea = new PIXI.Rectangle(0, 0, this.width, this.height);
|
|
90
|
+
this.container.containsPoint = (point) => {
|
|
91
|
+
const bounds = this.container.getBounds();
|
|
92
|
+
return point.x >= bounds.x &&
|
|
93
|
+
point.x <= bounds.x + bounds.width &&
|
|
94
|
+
point.y >= bounds.y &&
|
|
95
|
+
point.y <= bounds.y + bounds.height;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setContent(content) {
|
|
100
|
+
this.content = content || '';
|
|
101
|
+
this.textField.text = this.content;
|
|
102
|
+
this._updateTextPosition();
|
|
103
|
+
if (this.container && this.container._mb) {
|
|
104
|
+
this.container._mb.properties = {
|
|
105
|
+
...(this.container._mb.properties || {}),
|
|
106
|
+
content: this.content
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
console.log('NoteObject setContent called:', this.content);
|
|
110
|
+
// Перерисовываем фон после обновления содержимого
|
|
111
|
+
console.log('NoteObject: calling _redraw() to restore background');
|
|
112
|
+
this._redraw();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Alias для совместимости с TextObject
|
|
116
|
+
setText(content) {
|
|
117
|
+
this.setContent(content);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Скрывает текст записки (используется во время редактирования)
|
|
122
|
+
*/
|
|
123
|
+
hideText() {
|
|
124
|
+
if (this.textField) {
|
|
125
|
+
this.textField.visible = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Показывает текст записки (используется после завершения редактирования)
|
|
131
|
+
*/
|
|
132
|
+
showText() {
|
|
133
|
+
if (this.textField) {
|
|
134
|
+
this.textField.visible = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setStyle({ fontSize, backgroundColor, borderColor, textColor } = {}) {
|
|
139
|
+
if (typeof fontSize === 'number') {
|
|
140
|
+
this.fontSize = fontSize;
|
|
141
|
+
this.textField.style.fontSize = fontSize;
|
|
142
|
+
this.textField.style.lineHeight = fontSize * 1.2;
|
|
143
|
+
}
|
|
144
|
+
if (typeof backgroundColor === 'number') this.backgroundColor = backgroundColor;
|
|
145
|
+
if (typeof borderColor === 'number') this.borderColor = borderColor;
|
|
146
|
+
if (typeof textColor === 'number') {
|
|
147
|
+
this.textColor = textColor;
|
|
148
|
+
this.textField.style.fill = textColor;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.container && this.container._mb) {
|
|
152
|
+
this.container._mb.properties = {
|
|
153
|
+
...(this.container._mb.properties || {}),
|
|
154
|
+
fontSize: this.fontSize,
|
|
155
|
+
backgroundColor: this.backgroundColor,
|
|
156
|
+
borderColor: this.borderColor,
|
|
157
|
+
textColor: this.textColor
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this._redraw();
|
|
162
|
+
this._updateTextPosition();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_redraw() {
|
|
166
|
+
const g = this.graphics;
|
|
167
|
+
const w = this.width;
|
|
168
|
+
const h = this.height;
|
|
169
|
+
|
|
170
|
+
g.clear();
|
|
171
|
+
|
|
172
|
+
// Тень записки (эффект приподнятости)
|
|
173
|
+
g.beginFill(0x000000, 0.1);
|
|
174
|
+
g.drawRoundedRect(2, 2, w, h, 4);
|
|
175
|
+
g.endFill();
|
|
176
|
+
|
|
177
|
+
// Основной фон записки
|
|
178
|
+
g.beginFill(this.backgroundColor, 1);
|
|
179
|
+
g.lineStyle(1, this.borderColor, 1);
|
|
180
|
+
g.drawRoundedRect(0, 0, w, h, 4);
|
|
181
|
+
g.endFill();
|
|
182
|
+
|
|
183
|
+
// Небольшая полоска сверху для эффекта стикера
|
|
184
|
+
g.beginFill(this.borderColor, 0.3);
|
|
185
|
+
g.drawRoundedRect(0, 0, w, 8, 4);
|
|
186
|
+
g.endFill();
|
|
187
|
+
|
|
188
|
+
// Линии на записке (эффект бумаги)
|
|
189
|
+
g.lineStyle(0.5, this.borderColor, 0.2);
|
|
190
|
+
const lineSpacing = Math.max(16, this.fontSize + 4);
|
|
191
|
+
for (let y = 24; y < h - 8; y += lineSpacing) {
|
|
192
|
+
g.moveTo(8, y);
|
|
193
|
+
g.lineTo(w - 8, y);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Устанавливаем hit area для контейнера
|
|
197
|
+
this.container.hitArea = new PIXI.Rectangle(0, 0, w, h);
|
|
198
|
+
|
|
199
|
+
// Переопределяем containsPoint для правильного hit testing
|
|
200
|
+
this.container.containsPoint = (point) => {
|
|
201
|
+
const bounds = this.container.getBounds();
|
|
202
|
+
return point.x >= bounds.x &&
|
|
203
|
+
point.x <= bounds.x + bounds.width &&
|
|
204
|
+
point.y >= bounds.y &&
|
|
205
|
+
point.y <= bounds.y + bounds.height;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_updateTextPosition() {
|
|
210
|
+
if (!this.textField) return;
|
|
211
|
+
|
|
212
|
+
// Обновляем стиль текста
|
|
213
|
+
this.textField.style.wordWrapWidth = this.width - 16;
|
|
214
|
+
|
|
215
|
+
// Ждем, пока PIXI пересчитает размеры текста
|
|
216
|
+
this.textField.updateText();
|
|
217
|
+
|
|
218
|
+
// Центрируем текст по горизонтали
|
|
219
|
+
const centerX = this.width / 2;
|
|
220
|
+
const topMargin = 20; // Отступ от верха (ниже полоски)
|
|
221
|
+
|
|
222
|
+
// Используем anchor для центрирования
|
|
223
|
+
this.textField.anchor.set(0.5, 0);
|
|
224
|
+
this.textField.x = centerX;
|
|
225
|
+
this.textField.y = topMargin;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { FrameObject } from './FrameObject.js';
|
|
2
|
+
import { ShapeObject } from './ShapeObject.js';
|
|
3
|
+
import { DrawingObject } from './DrawingObject.js';
|
|
4
|
+
import { TextObject } from './TextObject.js';
|
|
5
|
+
import { EmojiObject } from './EmojiObject.js';
|
|
6
|
+
import { ImageObject } from './ImageObject.js';
|
|
7
|
+
import { CommentObject } from './CommentObject.js';
|
|
8
|
+
import { NoteObject } from './NoteObject.js';
|
|
9
|
+
import { FileObject } from './FileObject.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Фабрика объектов холста
|
|
13
|
+
* Назначение: централизованно создавать инстансы по типу объекта
|
|
14
|
+
*/
|
|
15
|
+
export class ObjectFactory {
|
|
16
|
+
static registry = new Map([
|
|
17
|
+
['frame', FrameObject],
|
|
18
|
+
['shape', ShapeObject],
|
|
19
|
+
['drawing', DrawingObject],
|
|
20
|
+
['text', TextObject],
|
|
21
|
+
['simple-text', TextObject],
|
|
22
|
+
['emoji', EmojiObject],
|
|
23
|
+
['image', ImageObject],
|
|
24
|
+
['comment', CommentObject],
|
|
25
|
+
['note', NoteObject],
|
|
26
|
+
['file', FileObject]
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Зарегистрировать новый тип объекта
|
|
31
|
+
* @param {string} type
|
|
32
|
+
* @param {class} clazz
|
|
33
|
+
*/
|
|
34
|
+
static register(type, clazz) {
|
|
35
|
+
if (!type || !clazz) return;
|
|
36
|
+
this.registry.set(type, clazz);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Создать инстанс объекта по типу
|
|
41
|
+
* @param {string} type
|
|
42
|
+
* @param {Object} objectData
|
|
43
|
+
* @returns {any|null}
|
|
44
|
+
*/
|
|
45
|
+
static create(type, objectData = {}) {
|
|
46
|
+
const Ctor = this.registry.get(type);
|
|
47
|
+
if (!Ctor) return null;
|
|
48
|
+
try {
|
|
49
|
+
return new Ctor(objectData);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error(`ObjectFactory: failed to create instance for type "${type}"`, e);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static has(type) {
|
|
57
|
+
return this.registry.has(type);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Класс объекта «Фигура»
|
|
5
|
+
* Отвечает за создание и перерисовку фигур разных типов с сохранением формы при ресайзе.
|
|
6
|
+
*/
|
|
7
|
+
export class ShapeObject {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} objectData Полные данные объекта из состояния
|
|
10
|
+
* - width, height
|
|
11
|
+
* - color
|
|
12
|
+
* - properties.kind: 'square' | 'rounded' | 'circle' | 'triangle' | 'diamond' | 'parallelogram' | 'arrow'
|
|
13
|
+
* - properties.cornerRadius?: number (для rounded)
|
|
14
|
+
*/
|
|
15
|
+
constructor(objectData = {}) {
|
|
16
|
+
this.objectData = objectData;
|
|
17
|
+
this.width = objectData.width || 100;
|
|
18
|
+
this.height = objectData.height || 100;
|
|
19
|
+
this.fillColor = objectData.color ?? 0x3b82f6;
|
|
20
|
+
const props = objectData.properties || {};
|
|
21
|
+
this.kind = props.kind || 'square';
|
|
22
|
+
this.cornerRadius = props.cornerRadius || 10;
|
|
23
|
+
|
|
24
|
+
this.graphics = new PIXI.Graphics();
|
|
25
|
+
this._draw(this.width, this.height, this.fillColor, this.kind, this.cornerRadius);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Возвращает PIXI-объект */
|
|
29
|
+
getPixi() {
|
|
30
|
+
return this.graphics;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Установить цвет заливки */
|
|
34
|
+
setColor(color) {
|
|
35
|
+
if (typeof color === 'number') {
|
|
36
|
+
this.fillColor = color;
|
|
37
|
+
this._redrawPreserveTransform(this.width, this.height, this.fillColor, this.kind, this.cornerRadius);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Обновить свойства фигуры (тип, радиус скругления) */
|
|
42
|
+
setProperties({ kind, cornerRadius } = {}) {
|
|
43
|
+
if (kind) this.kind = kind;
|
|
44
|
+
if (typeof cornerRadius === 'number') this.cornerRadius = cornerRadius;
|
|
45
|
+
this._redrawPreserveTransform(this.width, this.height, this.fillColor, this.kind, this.cornerRadius);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Обновить размер фигуры */
|
|
49
|
+
updateSize(size) {
|
|
50
|
+
if (!size) return;
|
|
51
|
+
const w = Math.max(0, size.width || 0);
|
|
52
|
+
const h = Math.max(0, size.height || 0);
|
|
53
|
+
this.width = w;
|
|
54
|
+
this.height = h;
|
|
55
|
+
this._redrawPreserveTransform(w, h, this.fillColor, this.kind, this.cornerRadius);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Перерисовать с сохранением трансформаций */
|
|
59
|
+
_redrawPreserveTransform(width, height, color, kind, cornerRadius) {
|
|
60
|
+
const g = this.graphics;
|
|
61
|
+
const x = g.x;
|
|
62
|
+
const y = g.y;
|
|
63
|
+
const rot = g.rotation || 0;
|
|
64
|
+
const pivotX = g.pivot?.x || 0;
|
|
65
|
+
const pivotY = g.pivot?.y || 0;
|
|
66
|
+
|
|
67
|
+
this._draw(width, height, color, kind, cornerRadius);
|
|
68
|
+
|
|
69
|
+
g.pivot.set(pivotX, pivotY);
|
|
70
|
+
g.x = x;
|
|
71
|
+
g.y = y;
|
|
72
|
+
g.rotation = rot;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Непосредственная отрисовка фигуры */
|
|
76
|
+
_draw(w, h, color, kind, cornerRadius) {
|
|
77
|
+
const g = this.graphics;
|
|
78
|
+
g.clear();
|
|
79
|
+
g.beginFill(color, 1);
|
|
80
|
+
switch (kind) {
|
|
81
|
+
case 'circle': {
|
|
82
|
+
const r = Math.min(w, h) / 2;
|
|
83
|
+
g.drawCircle(w / 2, h / 2, r);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case 'rounded': {
|
|
87
|
+
const r = cornerRadius || 10;
|
|
88
|
+
g.drawRoundedRect(0, 0, w, h, r);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case 'triangle': {
|
|
92
|
+
g.moveTo(w / 2, 0);
|
|
93
|
+
g.lineTo(w, h);
|
|
94
|
+
g.lineTo(0, h);
|
|
95
|
+
g.lineTo(w / 2, 0);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'diamond': {
|
|
99
|
+
g.moveTo(w / 2, 0);
|
|
100
|
+
g.lineTo(w, h / 2);
|
|
101
|
+
g.lineTo(w / 2, h);
|
|
102
|
+
g.lineTo(0, h / 2);
|
|
103
|
+
g.lineTo(w / 2, 0);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'parallelogram': {
|
|
107
|
+
const skew = Math.min(w * 0.25, 20);
|
|
108
|
+
g.moveTo(skew, 0);
|
|
109
|
+
g.lineTo(w, 0);
|
|
110
|
+
g.lineTo(w - skew, h);
|
|
111
|
+
g.lineTo(0, h);
|
|
112
|
+
g.lineTo(skew, 0);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'arrow': {
|
|
116
|
+
const shaftH = Math.max(6, h * 0.3);
|
|
117
|
+
const shaftY = (h - shaftH) / 2;
|
|
118
|
+
g.drawRect(0, shaftY, w * 0.6, shaftH);
|
|
119
|
+
g.moveTo(w * 0.6, 0);
|
|
120
|
+
g.lineTo(w, h / 2);
|
|
121
|
+
g.lineTo(w * 0.6, h);
|
|
122
|
+
g.lineTo(w * 0.6, 0);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'square':
|
|
126
|
+
default: {
|
|
127
|
+
g.drawRect(0, 0, w, h);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
g.endFill();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Класс объекта «Текст» — PIXI-объект служит только для хит-тестов/манипуляций.
|
|
5
|
+
* Визуальный рендер текста выполняет HtmlTextLayer.
|
|
6
|
+
*/
|
|
7
|
+
export class TextObject {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} objectData
|
|
10
|
+
* - properties.content: string
|
|
11
|
+
* - properties.fontSize: number
|
|
12
|
+
* - width/height: габариты текстового блока
|
|
13
|
+
*/
|
|
14
|
+
constructor(objectData = {}) {
|
|
15
|
+
this.objectData = objectData;
|
|
16
|
+
this.content = objectData.content || objectData.properties?.content || '';
|
|
17
|
+
this.fontSize = objectData.fontSize || objectData.properties?.fontSize || 16;
|
|
18
|
+
|
|
19
|
+
// Создаем невидимый прямоугольник для хит-теста
|
|
20
|
+
const w = Math.max(1, objectData.width || 160);
|
|
21
|
+
const h = Math.max(1, objectData.height || 36);
|
|
22
|
+
this.rect = new PIXI.Graphics();
|
|
23
|
+
this._drawRect(w, h);
|
|
24
|
+
|
|
25
|
+
// Метаданные типа
|
|
26
|
+
this.rect._mb = {
|
|
27
|
+
...(this.rect._mb || {}),
|
|
28
|
+
type: 'text',
|
|
29
|
+
instance: this, // Ссылка на сам объект для вызова методов
|
|
30
|
+
properties: {
|
|
31
|
+
content: this.content,
|
|
32
|
+
fontSize: this.fontSize,
|
|
33
|
+
baseW: w,
|
|
34
|
+
baseH: h
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_drawRect(width, height) {
|
|
40
|
+
const g = this.rect;
|
|
41
|
+
g.clear();
|
|
42
|
+
// Едва заметная заливка для стабильного containsPoint (почти прозрачная)
|
|
43
|
+
g.beginFill(0x000000, 0.001);
|
|
44
|
+
g.drawRect(0, 0, width, height);
|
|
45
|
+
g.endFill();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getPixi() {
|
|
49
|
+
return this.rect;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setText(content) {
|
|
53
|
+
this.content = content;
|
|
54
|
+
if (this.rect && this.rect._mb) {
|
|
55
|
+
this.rect._mb.properties = {
|
|
56
|
+
...(this.rect._mb.properties || {}),
|
|
57
|
+
content: content
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setStyle({ fontSize } = {}) {
|
|
63
|
+
if (typeof fontSize === 'number') this.fontSize = fontSize;
|
|
64
|
+
if (this.rect && this.rect._mb) {
|
|
65
|
+
this.rect._mb.properties = {
|
|
66
|
+
...(this.rect._mb.properties || {}),
|
|
67
|
+
fontSize: this.fontSize
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Обновление габаритов хит-бокса */
|
|
73
|
+
updateSize(size) {
|
|
74
|
+
if (!size) return;
|
|
75
|
+
const w = Math.max(1, size.width || 1);
|
|
76
|
+
const h = Math.max(1, size.height || 1);
|
|
77
|
+
const t = this.rect;
|
|
78
|
+
const prevCenter = { x: t.x, y: t.y };
|
|
79
|
+
const prevRot = t.rotation || 0;
|
|
80
|
+
this._drawRect(w, h);
|
|
81
|
+
// Центрируем pivot по новым размерам и восстанавливаем центр в мире
|
|
82
|
+
t.pivot.set(w / 2, h / 2);
|
|
83
|
+
t.x = prevCenter.x;
|
|
84
|
+
t.y = prevCenter.y;
|
|
85
|
+
t.rotation = prevRot;
|
|
86
|
+
// Не обновляем baseW/baseH — они служат опорой для масштабирования шрифта в HtmlTextLayer
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Скрывает текст (для совместимости с NoteObject)
|
|
91
|
+
* Для TextObject фактическая логика скрытия/показа обрабатывается в HtmlTextLayer
|
|
92
|
+
*/
|
|
93
|
+
hideText() {
|
|
94
|
+
// Для TextObject визуализация происходит в HtmlTextLayer
|
|
95
|
+
// Эмитим событие для скрытия текста
|
|
96
|
+
if (this.rect && this.rect._mb && this.rect._mb.objectId) {
|
|
97
|
+
// Используем EventBus через core, если доступен
|
|
98
|
+
if (window.moodboard && window.moodboard.eventBus) {
|
|
99
|
+
window.moodboard.eventBus.emit('tool:hide:object:text', {
|
|
100
|
+
objectId: this.rect._mb.objectId
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Показывает текст (для совместимости с NoteObject)
|
|
108
|
+
* Для TextObject фактическая логика скрытия/показа обрабатывается в HtmlTextLayer
|
|
109
|
+
*/
|
|
110
|
+
showText() {
|
|
111
|
+
// Для TextObject визуализация происходит в HtmlTextLayer
|
|
112
|
+
// Эмитим событие для показа текста
|
|
113
|
+
if (this.rect && this.rect._mb && this.rect._mb.objectId) {
|
|
114
|
+
// Используем EventBus через core, если доступен
|
|
115
|
+
if (window.moodboard && window.moodboard.eventBus) {
|
|
116
|
+
window.moodboard.eventBus.emit('tool:show:object:text', {
|
|
117
|
+
objectId: this.rect._mb.objectId
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|