@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.
@@ -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
- const halfW = (this.pending.size?.width ?? 100) / 2;
149
- const halfH = (this.pending.size?.height ?? 100) / 2;
150
- const position = { x: Math.round(worldPoint.x - halfW), y: Math.round(worldPoint.y - halfH) };
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
- const props = this.pending.properties || {};
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
- // Создаем визуальное представление файла (аналогично FileObject)
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
- graphics.beginFill(0xF8F9FA, 0.8);
376
- graphics.lineStyle(2, 0xDEE2E6, 0.8);
377
- graphics.drawRoundedRect(0, 0, width, height, 8);
378
- graphics.endFill();
379
-
380
- // Иконка файла (простой прямоугольник)
381
- graphics.beginFill(0x6C757D, 0.6);
382
- graphics.drawRoundedRect(width * 0.2, height * 0.15, width * 0.6, height * 0.3, 4);
383
- graphics.endFill();
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: 0x495057,
511
+ fill: 0x333333,
393
512
  align: 'center',
394
513
  wordWrap: true,
395
- wordWrapWidth: width - 10
514
+ wordWrapWidth: width - 8
396
515
  });
397
-
398
516
  nameText.x = (width - nameText.width) / 2;
399
- nameText.y = height * 0.55;
400
-
401
- this.ghostContainer.addChild(graphics);
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
- // Размеры призрака записки (из настроек NoteObject)
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
- // Фон записки (как в NoteObject)
661
- const background = new PIXI.Graphics();
662
- background.beginFill(0xFFF9C4, 0.8); // Светло-желтый с прозрачностью
663
- background.lineStyle(2, 0xF9A825, 0.8); // Золотистая граница
664
- background.drawRoundedRect(0, 0, width, height, 8);
665
- background.endFill();
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
- shadow.beginFill(0x000000, 0.1);
670
- shadow.drawRoundedRect(2, 2, width, height, 8);
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: 0x1A1A1A, // Темный цвет как в NoteObject
818
+ fill: textColor,
678
819
  align: 'center',
679
820
  wordWrap: true,
680
- wordWrapWidth: width - 16, // Отступы по 8px с каждой стороны
821
+ wordWrapWidth: width - 16,
681
822
  lineHeight: fontSize * 1.2
682
823
  });
683
-
684
- // Центрируем текст в записке
685
- noteText.x = (width - noteText.width) / 2;
686
- noteText.y = (height - noteText.height) / 2;
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
  // Центрируем контейнер относительно курсора