@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,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
+