@sequent-org/moodboard 1.0.23 → 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 +74 -4
|
@@ -4,7 +4,7 @@ import { ResizeHandles } from '../ResizeHandles.js';
|
|
|
4
4
|
import * as PIXI from 'pixi.js';
|
|
5
5
|
import { Events } from '../../core/events/Events.js';
|
|
6
6
|
import { SelectionModel } from './selection/SelectionModel.js';
|
|
7
|
-
import { HandlesSync } from './selection/HandlesSync.js';
|
|
7
|
+
// import { HandlesSync } from './selection/HandlesSync.js';
|
|
8
8
|
import { SimpleDragController } from './selection/SimpleDragController.js';
|
|
9
9
|
import { ResizeController } from './selection/ResizeController.js';
|
|
10
10
|
import { RotateController } from './selection/RotateController.js';
|
|
@@ -149,12 +149,10 @@ export class SelectTool extends BaseTool {
|
|
|
149
149
|
// Инициализируем систему ручек изменения размера
|
|
150
150
|
if (!this.resizeHandles && app) {
|
|
151
151
|
this.resizeHandles = new ResizeHandles(app);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
emit: (event, payload) => this.emit(event, payload)
|
|
157
|
-
});
|
|
152
|
+
// Полностью отключаем синхронизацию старых PIXI-ручек
|
|
153
|
+
if (this.resizeHandles && typeof this.resizeHandles.hideHandles === 'function') {
|
|
154
|
+
this.resizeHandles.hideHandles();
|
|
155
|
+
}
|
|
158
156
|
this._dragCtrl = new SimpleDragController({
|
|
159
157
|
emit: (event, payload) => this.emit(event, payload)
|
|
160
158
|
});
|
|
@@ -283,17 +281,77 @@ export class SelectTool extends BaseTool {
|
|
|
283
281
|
this.startBoxSelect(event);
|
|
284
282
|
}
|
|
285
283
|
} else if (hitResult.type === 'object') {
|
|
286
|
-
//
|
|
284
|
+
// Особая логика для фреймов: если у фрейма есть дети и клик внутри внутренней области (без 20px рамки),
|
|
285
|
+
// то не начинаем drag фрейма, а запускаем box-select для выбора объектов внутри
|
|
286
|
+
const req = { objectId: hitResult.object, pixiObject: null };
|
|
287
|
+
this.emit(Events.Tool.GetObjectPixi, req);
|
|
288
|
+
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
289
|
+
if (mbType === 'frame') {
|
|
290
|
+
// Получаем данные фрейма и его экранные границы
|
|
291
|
+
const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
|
|
292
|
+
const frameObj = objects.find(o => o.id === hitResult.object);
|
|
293
|
+
const hasChildren = !!(objects && objects.some(o => o.properties && o.properties.frameId === hitResult.object));
|
|
294
|
+
if (req.pixiObject && hasChildren && frameObj) {
|
|
295
|
+
const bounds = req.pixiObject.getBounds(); // экранные координаты
|
|
296
|
+
const inner = { x: bounds.x + 20, y: bounds.y + 20, width: Math.max(0, bounds.width - 40), height: Math.max(0, bounds.height - 40) };
|
|
297
|
+
const insideInner = this.isPointInBounds({ x: event.x, y: event.y }, inner);
|
|
298
|
+
// Если клик внутри внутренней области — запускаем box-select и выходим
|
|
299
|
+
if (insideInner) {
|
|
300
|
+
// Запускаем рамку выделения вместо drag фрейма
|
|
301
|
+
this.startBoxSelect(event);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Если клик на 20px рамке — позволяем перетягивать фрейм. Но запрещаем box-select от рамки.
|
|
305
|
+
// Здесь ничего не делаем: ниже пойдёт обычная логика handleObjectSelect
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Обычная логика: начинаем drag выбранного объекта
|
|
287
309
|
this.handleObjectSelect(hitResult.object, event);
|
|
288
310
|
} else {
|
|
289
311
|
// Клик по пустому месту — если есть одиночное выделение, разрешаем drag за пределами объекта в пределах рамки
|
|
290
312
|
if (this.selection.size() === 1) {
|
|
291
313
|
const selId = this.selection.toArray()[0];
|
|
314
|
+
// Если выбран фрейм с детьми и клик внутри внутренней области — не начинаем drag, а box-select
|
|
315
|
+
const req = { objectId: selId, pixiObject: null };
|
|
316
|
+
this.emit(Events.Tool.GetObjectPixi, req);
|
|
317
|
+
const isFrame = !!(req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type === 'frame');
|
|
318
|
+
if (isFrame) {
|
|
319
|
+
const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
|
|
320
|
+
const frameObj = objects.find(o => o.id === selId);
|
|
321
|
+
const hasChildren = !!(objects && objects.some(o => o.properties && o.properties.frameId === selId));
|
|
322
|
+
if (frameObj && hasChildren) {
|
|
323
|
+
const b = { x: frameObj.position.x, y: frameObj.position.y, width: frameObj.width || 0, height: frameObj.height || 0 };
|
|
324
|
+
const inner = { x: b.x + 20, y: b.y + 20, width: Math.max(0, b.width - 40), height: Math.max(0, b.height - 40) };
|
|
325
|
+
const insideInner = this.isPointInBounds({ x: event.x, y: event.y }, inner);
|
|
326
|
+
if (insideInner) {
|
|
327
|
+
this.startBoxSelect(event);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Обычная логика: если клик внутри рамки выбранного — начинаем drag
|
|
292
333
|
const boundsReq = { objects: [] };
|
|
293
334
|
this.emit(Events.Tool.GetAllObjects, boundsReq);
|
|
294
335
|
const map = new Map(boundsReq.objects.map(o => [o.id, o.bounds]));
|
|
295
336
|
const b = map.get(selId);
|
|
296
337
|
if (b && this.isPointInBounds({ x: event.x, y: event.y }, b)) {
|
|
338
|
+
// Для фрейма c детьми: отфильтруем клики внутри внутренней области (box-select)
|
|
339
|
+
const req2 = { objectId: selId, pixiObject: null };
|
|
340
|
+
this.emit(Events.Tool.GetObjectPixi, req2);
|
|
341
|
+
const isFrame2 = !!(req2.pixiObject && req2.pixiObject._mb && req2.pixiObject._mb.type === 'frame');
|
|
342
|
+
if (isFrame2) {
|
|
343
|
+
const os = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
|
|
344
|
+
const fr = os.find(o => o.id === selId);
|
|
345
|
+
const hasChildren2 = !!(os && os.some(o => o.properties && o.properties.frameId === selId));
|
|
346
|
+
if (req2.pixiObject && fr && hasChildren2) {
|
|
347
|
+
const bounds2 = req2.pixiObject.getBounds();
|
|
348
|
+
const inner2 = { x: bounds2.x + 20, y: bounds2.y + 20, width: Math.max(0, bounds2.width - 40), height: Math.max(0, bounds2.height - 40) };
|
|
349
|
+
if (this.isPointInBounds({ x: event.x, y: event.y }, inner2)) {
|
|
350
|
+
this.startBoxSelect(event);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
297
355
|
// Старт перетаскивания как если бы кликнули по объекту
|
|
298
356
|
this.startDrag(selId, event);
|
|
299
357
|
return;
|
|
@@ -614,12 +672,23 @@ export class SelectTool extends BaseTool {
|
|
|
614
672
|
startDrag(objectId, event) {
|
|
615
673
|
this.isDragging = true;
|
|
616
674
|
this.dragTarget = objectId;
|
|
675
|
+
// Сообщаем HtmlHandlesLayer о начале перетаскивания одиночного объекта
|
|
676
|
+
this.emit(Events.Tool.DragStart, { object: objectId });
|
|
617
677
|
|
|
618
678
|
// Получаем текущую позицию объекта
|
|
619
679
|
const objectData = { objectId, position: null };
|
|
620
680
|
this.emit(Events.Tool.GetObjectPosition, objectData);
|
|
621
681
|
// Нормализуем координаты в мировые (worldLayer), чтобы убрать влияние зума
|
|
622
682
|
const w = this._toWorld(event.x, event.y);
|
|
683
|
+
// Запоминаем смещение точки захвата курсора относительно левого-верхнего угла объекта (в мировых координатах)
|
|
684
|
+
if (objectData.position) {
|
|
685
|
+
this._dragGrabOffset = {
|
|
686
|
+
x: w.x - objectData.position.x,
|
|
687
|
+
y: w.y - objectData.position.y
|
|
688
|
+
};
|
|
689
|
+
} else {
|
|
690
|
+
this._dragGrabOffset = null;
|
|
691
|
+
}
|
|
623
692
|
const worldEvent = { ...event, x: w.x, y: w.y };
|
|
624
693
|
if (this._dragCtrl) this._dragCtrl.start(objectId, worldEvent);
|
|
625
694
|
}
|
|
@@ -639,13 +708,14 @@ export class SelectTool extends BaseTool {
|
|
|
639
708
|
this.isAltCloneMode = true;
|
|
640
709
|
this.cloneSourceId = this.dragTarget;
|
|
641
710
|
this.clonePending = true;
|
|
642
|
-
//
|
|
643
|
-
const
|
|
644
|
-
this.
|
|
645
|
-
|
|
711
|
+
// Создаём дубликат так, чтобы курсор захватывал ту же точку объекта
|
|
712
|
+
const wpos = this._toWorld(event.x, event.y);
|
|
713
|
+
const targetTopLeft = this._dragGrabOffset
|
|
714
|
+
? { x: wpos.x - this._dragGrabOffset.x, y: wpos.y - this._dragGrabOffset.y }
|
|
715
|
+
: { x: wpos.x, y: wpos.y };
|
|
646
716
|
this.emit(Events.Tool.DuplicateRequest, {
|
|
647
717
|
originalId: this.cloneSourceId,
|
|
648
|
-
position:
|
|
718
|
+
position: targetTopLeft
|
|
649
719
|
});
|
|
650
720
|
// Не сбрасываем dragTarget, чтобы исходник продолжал двигаться до появления копии
|
|
651
721
|
// Визуально это ок: копия появится и захватит drag в onDuplicateReady
|
|
@@ -657,6 +727,13 @@ export class SelectTool extends BaseTool {
|
|
|
657
727
|
const w = this._toWorld(event.x, event.y);
|
|
658
728
|
this._dragCtrl.update({ ...event, x: w.x, y: w.y });
|
|
659
729
|
}
|
|
730
|
+
// Сообщаем о процессе перетаскивания (с текущей позицией объекта)
|
|
731
|
+
if (this.isDragging && this.dragTarget) {
|
|
732
|
+
// Передаём текущий центр PIXI для точного расчёта dx/dy службами
|
|
733
|
+
const pix = this.getPixiObject(this.dragTarget);
|
|
734
|
+
const center = pix ? { x: pix.x, y: pix.y } : null;
|
|
735
|
+
this.emit(Events.Tool.DragUpdate, { object: this.dragTarget, pixiCenter: center });
|
|
736
|
+
}
|
|
660
737
|
|
|
661
738
|
// Обновляем ручки во время перетаскивания
|
|
662
739
|
if (this.resizeHandles && this.selection.has(this.dragTarget)) {
|
|
@@ -678,6 +755,8 @@ export class SelectTool extends BaseTool {
|
|
|
678
755
|
this.groupCloneMap = null;
|
|
679
756
|
} else if (this.dragTarget) {
|
|
680
757
|
if (this._dragCtrl) this._dragCtrl.end();
|
|
758
|
+
// Сообщаем о завершении перетаскивания одиночного объекта
|
|
759
|
+
this.emit(Events.Tool.DragEnd, { object: this.dragTarget });
|
|
681
760
|
}
|
|
682
761
|
|
|
683
762
|
this.isDragging = false;
|
|
@@ -763,7 +842,8 @@ export class SelectTool extends BaseTool {
|
|
|
763
842
|
this.groupBoundsGraphics.position.set(gb.x, gb.y);
|
|
764
843
|
}
|
|
765
844
|
if (this.resizeHandles) {
|
|
766
|
-
|
|
845
|
+
// Отключаем старые PIXI-ручки
|
|
846
|
+
this.resizeHandles.hideHandles();
|
|
767
847
|
}
|
|
768
848
|
return;
|
|
769
849
|
}
|
|
@@ -868,7 +948,7 @@ export class SelectTool extends BaseTool {
|
|
|
868
948
|
this.groupBoundsGraphics.pivot.set(0, 0);
|
|
869
949
|
this.groupBoundsGraphics.position.set(gb.x, gb.y);
|
|
870
950
|
}
|
|
871
|
-
if (this.resizeHandles) this.resizeHandles.
|
|
951
|
+
if (this.resizeHandles) this.resizeHandles.hideHandles();
|
|
872
952
|
return;
|
|
873
953
|
}
|
|
874
954
|
if (this._rotateCtrl) this._rotateCtrl.end();
|
|
@@ -1044,7 +1124,7 @@ export class SelectTool extends BaseTool {
|
|
|
1044
1124
|
this.isDragging = false; // отключаем одиночный drag, если был
|
|
1045
1125
|
this.ensureGroupBoundsGraphics(gb);
|
|
1046
1126
|
if (this.groupBoundsGraphics && this.resizeHandles) {
|
|
1047
|
-
this.resizeHandles.
|
|
1127
|
+
this.resizeHandles.hideHandles();
|
|
1048
1128
|
}
|
|
1049
1129
|
if (this._groupDragCtrl) {
|
|
1050
1130
|
const w = this._toWorld(event.x, event.y);
|
|
@@ -1562,22 +1642,27 @@ export class SelectTool extends BaseTool {
|
|
|
1562
1642
|
left: '0px',
|
|
1563
1643
|
top: '0px',
|
|
1564
1644
|
border: 'none',
|
|
1565
|
-
padding: '
|
|
1645
|
+
padding: '0',
|
|
1566
1646
|
fontSize: `${fontSize}px`,
|
|
1567
1647
|
fontFamily: 'Arial, sans-serif',
|
|
1568
|
-
lineHeight:
|
|
1648
|
+
lineHeight: `${fontSize}px`,
|
|
1569
1649
|
color: '#111', // Для записок делаем текст черным для лучшей видимости
|
|
1570
1650
|
background: 'white',
|
|
1571
1651
|
outline: 'none',
|
|
1572
1652
|
resize: 'none',
|
|
1573
1653
|
minWidth: '240px', // Для заметок уменьшаем минимальную ширину
|
|
1574
|
-
minHeight:
|
|
1654
|
+
minHeight: `${fontSize}px`,
|
|
1575
1655
|
width: '280px', // Для заметок уменьшаем начальную ширину
|
|
1576
|
-
height:
|
|
1577
|
-
boxSizing: '
|
|
1656
|
+
height: `${fontSize}px`,
|
|
1657
|
+
boxSizing: 'content-box',
|
|
1658
|
+
overflow: 'hidden',
|
|
1578
1659
|
// Повыше чёткость текста в CSS
|
|
1579
1660
|
WebkitFontSmoothing: 'antialiased',
|
|
1580
1661
|
MozOsxFontSmoothing: 'grayscale',
|
|
1662
|
+
// Улучшенное перенесение слов, как в Miro
|
|
1663
|
+
overflowWrap: 'anywhere',
|
|
1664
|
+
wordBreak: 'break-word',
|
|
1665
|
+
margin: '0',
|
|
1581
1666
|
});
|
|
1582
1667
|
|
|
1583
1668
|
wrapper.appendChild(textarea);
|
|
@@ -1727,8 +1812,22 @@ export class SelectTool extends BaseTool {
|
|
|
1727
1812
|
const initialHpx = initialSize ? Math.max(1, (initialSize.height || 0) * s / viewRes) : null;
|
|
1728
1813
|
|
|
1729
1814
|
// Определяем минимальные границы для всех типов объектов
|
|
1730
|
-
let minWBound = initialWpx ||
|
|
1731
|
-
let minHBound =
|
|
1815
|
+
let minWBound = initialWpx || 120; // базово близко к призраку
|
|
1816
|
+
let minHBound = fontSize; // высота по шрифту, чтобы не было пустого пространства
|
|
1817
|
+
|
|
1818
|
+
// Если создаём новый текст — выставляем стартовый размер, как у призрака, но чуть шире
|
|
1819
|
+
if (create && !isNote) {
|
|
1820
|
+
const ghostWidth = 120;
|
|
1821
|
+
const startWidth = Math.round(ghostWidth * 1.33); // чуть длиннее призрака (~160px)
|
|
1822
|
+
const startHeight = fontSize; // высота равна высоте шрифта
|
|
1823
|
+
textarea.style.width = `${startWidth}px`;
|
|
1824
|
+
textarea.style.height = `${startHeight}px`;
|
|
1825
|
+
wrapper.style.width = `${startWidth}px`;
|
|
1826
|
+
wrapper.style.height = `${startHeight}px`;
|
|
1827
|
+
// Зафиксируем минимальные границы, чтобы авторазмер не схлопывал пустое поле
|
|
1828
|
+
minWBound = startWidth;
|
|
1829
|
+
minHBound = startHeight;
|
|
1830
|
+
}
|
|
1732
1831
|
|
|
1733
1832
|
// Для записок размеры уже установлены выше, пропускаем эту логику
|
|
1734
1833
|
if (!isNote) {
|
|
@@ -1742,24 +1841,38 @@ export class SelectTool extends BaseTool {
|
|
|
1742
1841
|
}
|
|
1743
1842
|
}
|
|
1744
1843
|
// Автоподгон
|
|
1844
|
+
const MAX_AUTO_WIDTH = 360; // Поведение как в Miro: авто-ширина до порога, далее перенос строк
|
|
1745
1845
|
const autoSize = () => {
|
|
1746
1846
|
if (isNote) {
|
|
1747
1847
|
// Для заметок используем фиксированные размеры, вычисленные выше
|
|
1748
|
-
// Не вызываем autoSize, чтобы сохранить точное позиционирование
|
|
1749
1848
|
return;
|
|
1750
1849
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
textarea.style.height
|
|
1754
|
-
textarea.style.width = '
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
textarea.
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1850
|
+
// Сначала измеряем естественную ширину без ограничений
|
|
1851
|
+
const prevWidth = textarea.style.width;
|
|
1852
|
+
const prevHeight = textarea.style.height;
|
|
1853
|
+
textarea.style.width = 'auto';
|
|
1854
|
+
textarea.style.height = 'auto';
|
|
1855
|
+
|
|
1856
|
+
// Желаемая ширина: не уже минимальной и не шире максимальной авто-ширины
|
|
1857
|
+
const naturalW = textarea.scrollWidth + 1;
|
|
1858
|
+
const targetW = Math.min(MAX_AUTO_WIDTH, Math.max(minWBound, naturalW));
|
|
1859
|
+
textarea.style.width = `${targetW}px`;
|
|
1860
|
+
wrapper.style.width = `${targetW}px`;
|
|
1861
|
+
|
|
1862
|
+
// Высота по содержимому при установленной ширине
|
|
1863
|
+
textarea.style.height = 'auto';
|
|
1864
|
+
// Коррекция высоты: для одной строки принудительно равна line-height,
|
|
1865
|
+
// для нескольких строк используем scrollHeight с небольшим вычетом браузерного запаса
|
|
1866
|
+
const adjust = 2;
|
|
1867
|
+
const computed = (typeof window !== 'undefined') ? window.getComputedStyle(textarea) : null;
|
|
1868
|
+
const lineH = computed ? parseFloat(computed.lineHeight) : (fontSize || 18);
|
|
1869
|
+
const rawH = textarea.scrollHeight;
|
|
1870
|
+
const lines = lineH > 0 ? Math.max(1, Math.round(rawH / lineH)) : 1;
|
|
1871
|
+
const targetH = lines <= 1
|
|
1872
|
+
? lineH
|
|
1873
|
+
: Math.max(minHBound, Math.max(1, rawH - adjust));
|
|
1874
|
+
textarea.style.height = `${targetH}px`;
|
|
1875
|
+
wrapper.style.height = `${targetH}px`;
|
|
1763
1876
|
};
|
|
1764
1877
|
|
|
1765
1878
|
// Вызываем autoSize только для обычного текста
|
|
@@ -1828,6 +1941,7 @@ export class SelectTool extends BaseTool {
|
|
|
1828
1941
|
// handles.forEach(h => h.addEventListener('mousedown', onHandleDown));
|
|
1829
1942
|
}
|
|
1830
1943
|
// Завершение
|
|
1944
|
+
const isNewCreation = !!create;
|
|
1831
1945
|
const finalize = (commit) => {
|
|
1832
1946
|
console.log('🔧 SelectTool: finalize called with commit:', commit, 'objectId:', objectId, 'objectType:', this.textEditor.objectType);
|
|
1833
1947
|
const value = textarea.value.trim();
|
|
@@ -1837,8 +1951,8 @@ export class SelectTool extends BaseTool {
|
|
|
1837
1951
|
const currentObjectType = this.textEditor.objectType;
|
|
1838
1952
|
console.log('🔧 SelectTool: finalize - saved objectType:', currentObjectType);
|
|
1839
1953
|
|
|
1840
|
-
// Показываем статичный текст
|
|
1841
|
-
if (objectId) {
|
|
1954
|
+
// Показываем статичный текст только если не отменяем создание нового пустого
|
|
1955
|
+
if (objectId && (commitValue || !isNewCreation)) {
|
|
1842
1956
|
// Проверяем, что HTML-элемент существует перед попыткой показать текст
|
|
1843
1957
|
if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
|
|
1844
1958
|
const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
|
|
@@ -1852,10 +1966,42 @@ export class SelectTool extends BaseTool {
|
|
|
1852
1966
|
}
|
|
1853
1967
|
}
|
|
1854
1968
|
|
|
1969
|
+
// Перед скрытием — если редактировался существующий текст, обновим его размер под текущий редактор
|
|
1970
|
+
if (objectId && (currentObjectType === 'text' || currentObjectType === 'simple-text')) {
|
|
1971
|
+
try {
|
|
1972
|
+
const worldLayerRef = this.textEditor.world || (this.app?.stage);
|
|
1973
|
+
const s = worldLayerRef?.scale?.x || 1;
|
|
1974
|
+
const viewResLocal = (this.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
|
|
1975
|
+
const wPx = Math.max(1, wrapper.offsetWidth);
|
|
1976
|
+
const hPx = Math.max(1, wrapper.offsetHeight);
|
|
1977
|
+
const newW = Math.max(1, Math.round(wPx * viewResLocal / s));
|
|
1978
|
+
const newH = Math.max(1, Math.round(hPx * viewResLocal / s));
|
|
1979
|
+
// Получим старые размеры для команды
|
|
1980
|
+
const sizeReq = { objectId, size: null };
|
|
1981
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeReq);
|
|
1982
|
+
const oldSize = sizeReq.size || { width: newW, height: newH };
|
|
1983
|
+
// Позиция в state хранится как левый-верх
|
|
1984
|
+
const posReq = { objectId, position: null };
|
|
1985
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posReq);
|
|
1986
|
+
const oldPos = posReq.position || { x: position.x, y: position.y };
|
|
1987
|
+
const newSize = { width: newW, height: newH };
|
|
1988
|
+
// Во время ResizeUpdate ядро обновит и PIXI, и state
|
|
1989
|
+
this.eventBus.emit(Events.Tool.ResizeUpdate, { object: objectId, size: newSize, position: oldPos });
|
|
1990
|
+
// Зафиксируем изменение одной командой
|
|
1991
|
+
this.eventBus.emit(Events.Tool.ResizeEnd, { object: objectId, oldSize: oldSize, newSize: newSize, oldPosition: oldPos, newPosition: oldPos });
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
console.warn('⚠️ Не удалось применить размеры после редактирования текста:', err);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1855
1997
|
wrapper.remove();
|
|
1856
1998
|
this.textEditor = { active: false, objectId: null, textarea: null, wrapper: null, world: null, position: null, properties: null, objectType: 'text' };
|
|
1857
1999
|
this.eventBus.emit(Events.UI.TextEditEnd, { objectId: objectId || null });
|
|
1858
2000
|
if (!commitValue) {
|
|
2001
|
+
// Если это было создание нового текста и оно отменено — удаляем пустой объект
|
|
2002
|
+
if (isNewCreation && objectId) {
|
|
2003
|
+
this.eventBus.emit(Events.Tool.ObjectsDelete, { objects: [objectId] });
|
|
2004
|
+
}
|
|
1859
2005
|
console.log('🔧 SelectTool: finalize - no commit, returning');
|
|
1860
2006
|
return;
|
|
1861
2007
|
}
|
|
@@ -1863,11 +2009,18 @@ export class SelectTool extends BaseTool {
|
|
|
1863
2009
|
console.log('🔧 SelectTool: finalize - creating new object');
|
|
1864
2010
|
// Создаем объект с правильным типом
|
|
1865
2011
|
const objectType = currentObjectType || 'text';
|
|
2012
|
+
// Конвертируем размеры редактора (px) в мировые единицы
|
|
2013
|
+
const worldLayerRef = this.textEditor.world || (this.app?.stage);
|
|
2014
|
+
const s = worldLayerRef?.scale?.x || 1;
|
|
2015
|
+
const wPx = Math.max(1, wrapper.offsetWidth);
|
|
2016
|
+
const hPx = Math.max(1, wrapper.offsetHeight);
|
|
2017
|
+
const wWorld = Math.max(1, Math.round(wPx * viewRes / s));
|
|
2018
|
+
const hWorld = Math.max(1, Math.round(hPx * viewRes / s));
|
|
1866
2019
|
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
1867
2020
|
type: objectType,
|
|
1868
2021
|
id: objectType,
|
|
1869
2022
|
position: { x: position.x, y: position.y },
|
|
1870
|
-
properties: { content: value, fontSize }
|
|
2023
|
+
properties: { content: value, fontSize, width: wWorld, height: hWorld }
|
|
1871
2024
|
});
|
|
1872
2025
|
} else {
|
|
1873
2026
|
// Обновление существующего: используем команду обновления содержимого
|
|
@@ -1905,12 +2058,10 @@ export class SelectTool extends BaseTool {
|
|
|
1905
2058
|
}
|
|
1906
2059
|
};
|
|
1907
2060
|
textarea.addEventListener('blur', (e) => {
|
|
1908
|
-
// Не закрываем новый пустой текст по потере фокуса — чтобы поле не исчезало сразу
|
|
1909
|
-
const isNew = objectId == null;
|
|
1910
2061
|
const value = (textarea.value || '').trim();
|
|
1911
|
-
if (
|
|
1912
|
-
//
|
|
1913
|
-
|
|
2062
|
+
if (isNewCreation && value.length === 0) {
|
|
2063
|
+
// Клик вне поля при пустом значении — отменяем и удаляем созданный объект
|
|
2064
|
+
finalize(false);
|
|
1914
2065
|
return;
|
|
1915
2066
|
}
|
|
1916
2067
|
finalize(true);
|
|
@@ -2267,4 +2418,24 @@ export class SelectTool extends BaseTool {
|
|
|
2267
2418
|
super.destroy();
|
|
2268
2419
|
}
|
|
2269
2420
|
|
|
2421
|
+
onDuplicateReady(newObjectId) {
|
|
2422
|
+
this.clonePending = false;
|
|
2423
|
+
|
|
2424
|
+
// Переключаем выделение на новый объект
|
|
2425
|
+
this.clearSelection();
|
|
2426
|
+
this.addToSelection(newObjectId);
|
|
2427
|
+
|
|
2428
|
+
// Завершаем drag исходного объекта и переключаем контроллер на новый объект
|
|
2429
|
+
if (this._dragCtrl) this._dragCtrl.end();
|
|
2430
|
+
this.dragTarget = newObjectId;
|
|
2431
|
+
this.isDragging = true;
|
|
2432
|
+
// Стартуем drag нового объекта под текущим курсором (в мировых координатах)
|
|
2433
|
+
const w = this._toWorld(this.currentX, this.currentY);
|
|
2434
|
+
if (this._dragCtrl) this._dragCtrl.start(newObjectId, { x: w.x, y: w.y });
|
|
2435
|
+
// Мгновенно обновляем позицию под курсор
|
|
2436
|
+
this.updateDrag({ x: this.currentX, y: this.currentY });
|
|
2437
|
+
// Обновляем ручки
|
|
2438
|
+
this.updateResizeHandles();
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2270
2441
|
}
|
|
@@ -55,6 +55,9 @@ export class BoxSelectController {
|
|
|
55
55
|
this.emit('get:all:objects', request);
|
|
56
56
|
const matched = [];
|
|
57
57
|
for (const item of request.objects) {
|
|
58
|
+
const meta = item.pixi && item.pixi._mb;
|
|
59
|
+
// Исключаем фреймы из выделения рамкой — их можно выбрать только кликом по захватной рамке
|
|
60
|
+
if (meta && meta.type === 'frame') continue;
|
|
58
61
|
if (this.rectIntersectsRect(box, item.bounds)) matched.push(item.id);
|
|
59
62
|
}
|
|
60
63
|
let newSelection;
|
|
@@ -84,6 +87,8 @@ export class BoxSelectController {
|
|
|
84
87
|
this.emit('get:all:objects', request);
|
|
85
88
|
const matched = [];
|
|
86
89
|
for (const item of request.objects) {
|
|
90
|
+
const meta = item.pixi && item.pixi._mb;
|
|
91
|
+
if (meta && meta.type === 'frame') continue;
|
|
87
92
|
if (this.rectIntersectsRect(box, item.bounds)) matched.push(item.id);
|
|
88
93
|
}
|
|
89
94
|
if (matched.length > 0) {
|
|
@@ -28,9 +28,13 @@ export class FilePropertiesPanel {
|
|
|
28
28
|
if (this.currentId && objectId === this.currentId) this.hide();
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
// Обновляем позицию
|
|
31
|
+
// Обновляем позицию / скрываем во время перетаскивания
|
|
32
|
+
this.eventBus.on(Events.Tool.DragStart, () => this.hide());
|
|
32
33
|
this.eventBus.on(Events.Tool.DragUpdate, () => this.reposition());
|
|
34
|
+
this.eventBus.on(Events.Tool.DragEnd, () => this.updateFromSelection());
|
|
33
35
|
this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.reposition());
|
|
36
|
+
this.eventBus.on(Events.Tool.GroupDragStart, () => this.hide());
|
|
37
|
+
this.eventBus.on(Events.Tool.GroupDragEnd, () => this.updateFromSelection());
|
|
34
38
|
this.eventBus.on(Events.Tool.ResizeUpdate, () => this.reposition());
|
|
35
39
|
this.eventBus.on(Events.Tool.RotateUpdate, () => this.reposition());
|
|
36
40
|
|
|
@@ -256,7 +260,10 @@ export class FilePropertiesPanel {
|
|
|
256
260
|
|
|
257
261
|
// Показываем/скрываем кнопку скачивания в зависимости от наличия fileId
|
|
258
262
|
if (this.downloadButton) {
|
|
259
|
-
|
|
263
|
+
// Всегда показываем кнопку, даже без fileId
|
|
264
|
+
this.downloadButton.style.display = 'flex';
|
|
265
|
+
this.downloadButton.disabled = !hasFileId;
|
|
266
|
+
this.downloadButton.title = hasFileId ? 'Скачать файл' : 'Файл недоступен для скачивания';
|
|
260
267
|
}
|
|
261
268
|
}
|
|
262
269
|
}
|