@sequent-org/moodboard 1.0.24 → 1.1.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 +1 -1
- package/src/assets/icons/rotate-icon.svg +3 -0
- package/src/core/PixiEngine.js +32 -0
- package/src/core/commands/CopyObjectCommand.js +20 -9
- package/src/core/commands/PasteObjectCommand.js +26 -15
- package/src/core/index.js +522 -26
- package/src/objects/DrawingObject.js +16 -7
- package/src/objects/FileObject.js +25 -11
- package/src/objects/FrameObject.js +37 -9
- package/src/objects/NoteObject.js +32 -17
- package/src/objects/ShapeObject.js +9 -8
- package/src/objects/TextObject.js +2 -20
- package/src/services/FrameService.js +95 -17
- package/src/tools/object-tools/PlacementTool.js +192 -51
- package/src/tools/object-tools/SelectTool.js +215 -44
- package/src/tools/object-tools/selection/BoxSelectController.js +5 -0
- package/src/ui/FilePropertiesPanel.js +9 -2
- package/src/ui/FramePropertiesPanel.js +177 -34
- package/src/ui/HtmlHandlesLayer.js +145 -89
- package/src/ui/HtmlTextLayer.js +9 -1
- package/src/ui/NotePropertiesPanel.js +13 -6
- package/src/ui/Toolbar.js +118 -15
- package/src/ui/styles/workspace.css +71 -2
|
@@ -36,6 +36,8 @@ export class PlacementTool extends BaseTool {
|
|
|
36
36
|
this.showEmojiGhost();
|
|
37
37
|
} else if (this.pending.type === 'frame') {
|
|
38
38
|
this.showFrameGhost();
|
|
39
|
+
} else if (this.pending.type === 'frame-draw') {
|
|
40
|
+
this.startFrameDrawMode();
|
|
39
41
|
} else if (this.pending.type === 'shape') {
|
|
40
42
|
this.showShapeGhost();
|
|
41
43
|
}
|
|
@@ -143,22 +145,50 @@ export class PlacementTool extends BaseTool {
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
if (!this.pending) return;
|
|
148
|
+
// Если включен режим рисования фрейма — инициируем рамку
|
|
149
|
+
if (this.pending.type === 'frame-draw') {
|
|
150
|
+
const start = this._toWorld(event.x, event.y);
|
|
151
|
+
this._frameDrawState = { startX: start.x, startY: start.y, graphics: null };
|
|
152
|
+
if (this.world) {
|
|
153
|
+
const g = new PIXI.Graphics();
|
|
154
|
+
g.zIndex = 3000;
|
|
155
|
+
this.world.addChild(g);
|
|
156
|
+
this._frameDrawState.graphics = g;
|
|
157
|
+
}
|
|
158
|
+
// Вешаем временные обработчики движения/отпускания
|
|
159
|
+
this._onFrameDrawMoveBound = (ev) => this._onFrameDrawMove(ev);
|
|
160
|
+
this._onFrameDrawUpBound = (ev) => this._onFrameDrawUp(ev);
|
|
161
|
+
this.app.view.addEventListener('mousemove', this._onFrameDrawMoveBound);
|
|
162
|
+
this.app.view.addEventListener('mouseup', this._onFrameDrawUpBound, { once: true });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
146
165
|
|
|
147
166
|
const worldPoint = this._toWorld(event.x, event.y);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
167
|
+
// Базовая позиция (может быть переопределена для конкретных типов)
|
|
168
|
+
let position = {
|
|
169
|
+
x: Math.round(worldPoint.x - (this.pending.size?.width ?? 100) / 2),
|
|
170
|
+
y: Math.round(worldPoint.y - (this.pending.size?.height ?? 100) / 2)
|
|
171
|
+
};
|
|
151
172
|
|
|
152
|
-
|
|
173
|
+
let props = this.pending.properties || {};
|
|
153
174
|
const isTextWithEditing = this.pending.type === 'text' && props.editOnCreate;
|
|
154
175
|
const isImage = this.pending.type === 'image';
|
|
155
176
|
const isFile = this.pending.type === 'file';
|
|
156
177
|
const presetSize = {
|
|
157
|
-
width: (this.pending.size && this.pending.size.width) ? this.pending.size.width : 200,
|
|
158
|
-
height: (this.pending.size && this.pending.size.height) ? this.pending.size.height : 150,
|
|
178
|
+
width: (this.pending.size && this.pending.size.width) ? this.pending.size.width : (props.width || 200),
|
|
179
|
+
height: (this.pending.size && this.pending.size.height) ? this.pending.size.height : (props.height || 150),
|
|
159
180
|
};
|
|
160
181
|
|
|
161
182
|
if (isTextWithEditing) {
|
|
183
|
+
// Для текста используем те же размеры, что и у "призрака",
|
|
184
|
+
// чтобы позиция совпадала пиксель-в-пиксель
|
|
185
|
+
const fontSize = props.fontSize || 18;
|
|
186
|
+
const ghostWidth = 120;
|
|
187
|
+
const ghostHeight = fontSize + 20;
|
|
188
|
+
position = {
|
|
189
|
+
x: Math.round(worldPoint.x - ghostWidth / 2),
|
|
190
|
+
y: Math.round(worldPoint.y - ghostHeight / 2)
|
|
191
|
+
};
|
|
162
192
|
// Слушаем событие создания объекта, чтобы получить его ID
|
|
163
193
|
const handleObjectCreated = (objectData) => {
|
|
164
194
|
if (objectData.type === 'text') {
|
|
@@ -202,6 +232,20 @@ export class PlacementTool extends BaseTool {
|
|
|
202
232
|
backgroundColor: 'transparent' // Дефолтный фон (прозрачный)
|
|
203
233
|
}
|
|
204
234
|
});
|
|
235
|
+
} else if (this.pending.type === 'frame') {
|
|
236
|
+
// Для фрейма центр привязываем к курсору так же, как у призрака
|
|
237
|
+
const width = props.width || presetSize.width || 200;
|
|
238
|
+
const height = props.height || presetSize.height || 300;
|
|
239
|
+
position = {
|
|
240
|
+
x: Math.round(worldPoint.x - width / 2),
|
|
241
|
+
y: Math.round(worldPoint.y - height / 2)
|
|
242
|
+
};
|
|
243
|
+
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
244
|
+
type: 'frame',
|
|
245
|
+
id: 'frame',
|
|
246
|
+
position,
|
|
247
|
+
properties: { ...props, width, height }
|
|
248
|
+
});
|
|
205
249
|
} else if (isImage && props.selectFileOnPlace) {
|
|
206
250
|
const input = document.createElement('input');
|
|
207
251
|
input.type = 'file';
|
|
@@ -314,6 +358,16 @@ export class PlacementTool extends BaseTool {
|
|
|
314
358
|
}, { once: true });
|
|
315
359
|
input.click();
|
|
316
360
|
} else {
|
|
361
|
+
// Для записки: выставляем фактические габариты и центрируем по курсору
|
|
362
|
+
if (this.pending.type === 'note') {
|
|
363
|
+
const noteW = (typeof props.width === 'number') ? props.width : 160;
|
|
364
|
+
const noteH = (typeof props.height === 'number') ? props.height : 100;
|
|
365
|
+
props = { ...props, width: noteW, height: noteH };
|
|
366
|
+
position = {
|
|
367
|
+
x: Math.round(worldPoint.x - noteW / 2),
|
|
368
|
+
y: Math.round(worldPoint.y - noteH / 2)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
317
371
|
// Обычное размещение через общий канал
|
|
318
372
|
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
319
373
|
type: this.pending.type,
|
|
@@ -331,6 +385,57 @@ export class PlacementTool extends BaseTool {
|
|
|
331
385
|
}
|
|
332
386
|
}
|
|
333
387
|
|
|
388
|
+
startFrameDrawMode() {
|
|
389
|
+
// Курсор при рисовании фрейма
|
|
390
|
+
if (this.app && this.app.view) this.app.view.style.cursor = 'crosshair';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_onFrameDrawMove(event) {
|
|
394
|
+
if (!this._frameDrawState || !this._frameDrawState.graphics) return;
|
|
395
|
+
const p = this._toWorld(event.offsetX, event.offsetY);
|
|
396
|
+
const x = Math.min(this._frameDrawState.startX, p.x);
|
|
397
|
+
const y = Math.min(this._frameDrawState.startY, p.y);
|
|
398
|
+
const w = Math.abs(p.x - this._frameDrawState.startX);
|
|
399
|
+
const h = Math.abs(p.y - this._frameDrawState.startY);
|
|
400
|
+
const g = this._frameDrawState.graphics;
|
|
401
|
+
g.clear();
|
|
402
|
+
g.lineStyle(1, 0x3B82F6, 1);
|
|
403
|
+
g.beginFill(0x3B82F6, 0.08);
|
|
404
|
+
g.drawRect(x, y, w, h);
|
|
405
|
+
g.endFill();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_onFrameDrawUp(event) {
|
|
409
|
+
const g = this._frameDrawState?.graphics;
|
|
410
|
+
if (!this._frameDrawState || !g) return;
|
|
411
|
+
const p = this._toWorld(event.offsetX, event.offsetY);
|
|
412
|
+
const x = Math.min(this._frameDrawState.startX, p.x);
|
|
413
|
+
const y = Math.min(this._frameDrawState.startY, p.y);
|
|
414
|
+
const w = Math.abs(p.x - this._frameDrawState.startX);
|
|
415
|
+
const h = Math.abs(p.y - this._frameDrawState.startY);
|
|
416
|
+
// Удаляем временную графику
|
|
417
|
+
if (g.parent) g.parent.removeChild(g);
|
|
418
|
+
g.destroy();
|
|
419
|
+
this._frameDrawState = null;
|
|
420
|
+
// Создаем фрейм, если размер достаточный
|
|
421
|
+
if (w >= 2 && h >= 2) {
|
|
422
|
+
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
423
|
+
type: 'frame',
|
|
424
|
+
id: 'frame',
|
|
425
|
+
position: { x, y },
|
|
426
|
+
properties: { width: Math.round(w), height: Math.round(h), title: 'Произвольный', lockedAspect: false, isArbitrary: true }
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
// Сбрасываем pending и выходим из режима place → select
|
|
430
|
+
this.pending = null;
|
|
431
|
+
this.hideGhost();
|
|
432
|
+
if (this.app && this.app.view) {
|
|
433
|
+
this.app.view.removeEventListener('mousemove', this._onFrameDrawMoveBound);
|
|
434
|
+
this.app.view.style.cursor = '';
|
|
435
|
+
}
|
|
436
|
+
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
437
|
+
}
|
|
438
|
+
|
|
334
439
|
_toWorld(x, y) {
|
|
335
440
|
if (!this.world) return { x, y };
|
|
336
441
|
const global = new PIXI.Point(x, y);
|
|
@@ -366,41 +471,57 @@ export class PlacementTool extends BaseTool {
|
|
|
366
471
|
this.ghostContainer = new PIXI.Container();
|
|
367
472
|
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
368
473
|
|
|
369
|
-
//
|
|
370
|
-
const graphics = new PIXI.Graphics();
|
|
474
|
+
// Размеры
|
|
371
475
|
const width = this.selectedFile.properties.width || 120;
|
|
372
476
|
const height = this.selectedFile.properties.height || 140;
|
|
373
|
-
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
477
|
+
|
|
478
|
+
// Размытая тень (как у FileObject)
|
|
479
|
+
const shadow = new PIXI.Graphics();
|
|
480
|
+
try {
|
|
481
|
+
shadow.filters = [new PIXI.filters.BlurFilter(6)];
|
|
482
|
+
} catch (e) {}
|
|
483
|
+
shadow.beginFill(0x000000, 1);
|
|
484
|
+
shadow.drawRect(0, 0, width, height);
|
|
485
|
+
shadow.endFill();
|
|
486
|
+
shadow.x = 2;
|
|
487
|
+
shadow.y = 3;
|
|
488
|
+
shadow.alpha = 0.18;
|
|
489
|
+
|
|
490
|
+
// Белый прямоугольник без рамки
|
|
491
|
+
const background = new PIXI.Graphics();
|
|
492
|
+
background.beginFill(0xFFFFFF, 1);
|
|
493
|
+
background.drawRect(0, 0, width, height);
|
|
494
|
+
background.endFill();
|
|
495
|
+
|
|
496
|
+
// Иконка-заглушка файла наверху
|
|
497
|
+
const icon = new PIXI.Graphics();
|
|
498
|
+
const iconSize = Math.min(48, width * 0.4);
|
|
499
|
+
const iconX = (width - iconSize) / 2;
|
|
500
|
+
const iconY = 16;
|
|
501
|
+
icon.beginFill(0x6B7280, 1);
|
|
502
|
+
icon.drawRect(iconX, iconY, iconSize * 0.8, iconSize);
|
|
503
|
+
icon.endFill();
|
|
504
|
+
|
|
385
505
|
// Текст названия файла
|
|
386
506
|
const fileName = this.selectedFile.fileName || 'File';
|
|
387
507
|
const displayName = fileName.length > 15 ? fileName.substring(0, 12) + '...' : fileName;
|
|
388
|
-
|
|
389
508
|
const nameText = new PIXI.Text(displayName, {
|
|
390
|
-
fontFamily: 'Arial, sans-serif',
|
|
509
|
+
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
|
|
391
510
|
fontSize: 12,
|
|
392
|
-
fill:
|
|
511
|
+
fill: 0x333333,
|
|
393
512
|
align: 'center',
|
|
394
513
|
wordWrap: true,
|
|
395
|
-
wordWrapWidth: width -
|
|
514
|
+
wordWrapWidth: width - 8
|
|
396
515
|
});
|
|
397
|
-
|
|
398
516
|
nameText.x = (width - nameText.width) / 2;
|
|
399
|
-
nameText.y = height
|
|
400
|
-
|
|
401
|
-
|
|
517
|
+
nameText.y = height - 40;
|
|
518
|
+
|
|
519
|
+
// Добавляем в контейнер в правильном порядке
|
|
520
|
+
this.ghostContainer.addChild(shadow);
|
|
521
|
+
this.ghostContainer.addChild(background);
|
|
522
|
+
this.ghostContainer.addChild(icon);
|
|
402
523
|
this.ghostContainer.addChild(nameText);
|
|
403
|
-
|
|
524
|
+
|
|
404
525
|
// Центрируем контейнер относительно курсора
|
|
405
526
|
this.ghostContainer.pivot.x = width / 2;
|
|
406
527
|
this.ghostContainer.pivot.y = height / 2;
|
|
@@ -651,43 +772,63 @@ export class PlacementTool extends BaseTool {
|
|
|
651
772
|
this.ghostContainer = new PIXI.Container();
|
|
652
773
|
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
653
774
|
|
|
654
|
-
// Размеры
|
|
775
|
+
// Размеры и стили, синхронизированные с NoteObject
|
|
655
776
|
const width = this.pending.properties?.width || 160;
|
|
656
777
|
const height = this.pending.properties?.height || 100;
|
|
657
778
|
const fontSize = this.pending.properties?.fontSize || 16;
|
|
658
779
|
const content = this.pending.properties?.content || 'Новая записка';
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
780
|
+
const backgroundColor = (typeof this.pending.properties?.backgroundColor === 'number')
|
|
781
|
+
? this.pending.properties.backgroundColor
|
|
782
|
+
: 0xFFF9C4;
|
|
783
|
+
const borderColor = (typeof this.pending.properties?.borderColor === 'number')
|
|
784
|
+
? this.pending.properties.borderColor
|
|
785
|
+
: 0xF9A825;
|
|
786
|
+
const textColor = (typeof this.pending.properties?.textColor === 'number')
|
|
787
|
+
? this.pending.properties.textColor
|
|
788
|
+
: 0x1A1A1A;
|
|
789
|
+
|
|
790
|
+
// Тень (размытая) под запиской
|
|
668
791
|
const shadow = new PIXI.Graphics();
|
|
669
|
-
|
|
670
|
-
|
|
792
|
+
try {
|
|
793
|
+
shadow.filters = [new PIXI.filters.BlurFilter(6)];
|
|
794
|
+
} catch (e) {}
|
|
795
|
+
shadow.beginFill(0x000000, 1);
|
|
796
|
+
shadow.drawRect(0, 0, width, height);
|
|
671
797
|
shadow.endFill();
|
|
672
|
-
|
|
673
|
-
|
|
798
|
+
shadow.x = 2;
|
|
799
|
+
shadow.y = 3;
|
|
800
|
+
shadow.alpha = 0.18;
|
|
801
|
+
|
|
802
|
+
// Основной фон записки (прямоугольник без рамки)
|
|
803
|
+
const background = new PIXI.Graphics();
|
|
804
|
+
background.beginFill(backgroundColor, 1);
|
|
805
|
+
background.drawRect(0, 0, width, height);
|
|
806
|
+
background.endFill();
|
|
807
|
+
|
|
808
|
+
// Прямоугольная шапка сверху, цвет рамки
|
|
809
|
+
const header = new PIXI.Graphics();
|
|
810
|
+
header.beginFill(borderColor, 1);
|
|
811
|
+
header.drawRect(0, 0, width, 8);
|
|
812
|
+
header.endFill();
|
|
813
|
+
|
|
814
|
+
// Текст записки, выровнен как в NoteObject (центр по X, отступ сверху)
|
|
674
815
|
const noteText = new PIXI.Text(content, {
|
|
675
816
|
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
|
|
676
817
|
fontSize: fontSize,
|
|
677
|
-
fill:
|
|
818
|
+
fill: textColor,
|
|
678
819
|
align: 'center',
|
|
679
820
|
wordWrap: true,
|
|
680
|
-
wordWrapWidth: width - 16,
|
|
821
|
+
wordWrapWidth: width - 16,
|
|
681
822
|
lineHeight: fontSize * 1.2
|
|
682
823
|
});
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
noteText.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
// Добавляем элементы в правильном порядке
|
|
824
|
+
noteText.anchor.set(0.5, 0);
|
|
825
|
+
noteText.x = Math.round(width / 2);
|
|
826
|
+
noteText.y = 20; // как в NoteObject topMargin
|
|
827
|
+
|
|
828
|
+
// Порядок добавления: тень → фон → шапка → текст
|
|
689
829
|
this.ghostContainer.addChild(shadow);
|
|
690
830
|
this.ghostContainer.addChild(background);
|
|
831
|
+
this.ghostContainer.addChild(header);
|
|
691
832
|
this.ghostContainer.addChild(noteText);
|
|
692
833
|
|
|
693
834
|
// Центрируем контейнер относительно курсора
|