@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
|
@@ -28,8 +28,12 @@ export class FramePropertiesPanel {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// Обновляем позицию при любых изменениях (как в TextPropertiesPanel)
|
|
31
|
+
this.eventBus.on(Events.Tool.DragStart, () => this.hide());
|
|
31
32
|
this.eventBus.on(Events.Tool.DragUpdate, () => this.reposition());
|
|
33
|
+
this.eventBus.on(Events.Tool.DragEnd, () => this.updateFromSelection());
|
|
32
34
|
this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.reposition());
|
|
35
|
+
this.eventBus.on(Events.Tool.GroupDragStart, () => this.hide());
|
|
36
|
+
this.eventBus.on(Events.Tool.GroupDragEnd, () => this.updateFromSelection());
|
|
33
37
|
this.eventBus.on(Events.Tool.ResizeUpdate, () => this.reposition());
|
|
34
38
|
this.eventBus.on(Events.Tool.RotateUpdate, () => this.reposition());
|
|
35
39
|
|
|
@@ -88,6 +92,7 @@ export class FramePropertiesPanel {
|
|
|
88
92
|
|
|
89
93
|
// Обновляем контролы в соответствии с текущими свойствами объекта
|
|
90
94
|
this._updateControlsFromObject();
|
|
95
|
+
this._syncTypeFromObject();
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
hide() {
|
|
@@ -104,16 +109,16 @@ export class FramePropertiesPanel {
|
|
|
104
109
|
position: 'absolute',
|
|
105
110
|
display: 'none',
|
|
106
111
|
alignItems: 'center',
|
|
107
|
-
gap: '
|
|
108
|
-
padding: '
|
|
112
|
+
gap: '4px',
|
|
113
|
+
padding: '4px 6px',
|
|
109
114
|
backgroundColor: 'white',
|
|
110
|
-
border: '1px solid #
|
|
111
|
-
borderRadius: '
|
|
112
|
-
boxShadow: '0 2px
|
|
113
|
-
fontSize: '
|
|
115
|
+
border: '1px solid #e5e7eb',
|
|
116
|
+
borderRadius: '6px',
|
|
117
|
+
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.12)',
|
|
118
|
+
fontSize: '12px',
|
|
114
119
|
fontFamily: 'Arial, sans-serif',
|
|
115
|
-
minWidth: '
|
|
116
|
-
height: '
|
|
120
|
+
minWidth: '160px',
|
|
121
|
+
height: 'auto',
|
|
117
122
|
zIndex: '10000'
|
|
118
123
|
});
|
|
119
124
|
|
|
@@ -158,25 +163,25 @@ export class FramePropertiesPanel {
|
|
|
158
163
|
const { x, y } = posData.position;
|
|
159
164
|
const { width, height } = sizeData.size;
|
|
160
165
|
|
|
161
|
-
// Позиционируем панель
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
// Позиционируем панель над фреймом, по центру
|
|
167
|
+
const panelRect = this.panel.getBoundingClientRect();
|
|
168
|
+
const panelW = Math.max(1, panelRect.width || 280);
|
|
169
|
+
const panelH = Math.max(1, panelRect.height || 60);
|
|
170
|
+
let panelX = x + (width / 2) - (panelW / 2);
|
|
171
|
+
let panelY = y - panelH - 40; // отступ 40px над фреймом
|
|
172
|
+
|
|
173
|
+
// Если панель уходит за верх, переносим ниже фрейма
|
|
174
|
+
if (panelY < 0) {
|
|
175
|
+
panelY = y + height + 40;
|
|
176
|
+
}
|
|
172
177
|
|
|
173
|
-
console.log('🖼️ FramePropertiesPanel: Positioning above frame:', {
|
|
178
|
+
console.log('🖼️ FramePropertiesPanel: Positioning above frame:', {
|
|
174
179
|
frameX: x, frameY: y, frameWidth: width, frameHeight: height,
|
|
175
180
|
panelX, panelY
|
|
176
181
|
});
|
|
177
182
|
|
|
178
|
-
this.panel.style.left = `${panelX}px`;
|
|
179
|
-
this.panel.style.top = `${panelY}px`;
|
|
183
|
+
this.panel.style.left = `${Math.round(panelX)}px`;
|
|
184
|
+
this.panel.style.top = `${Math.round(panelY)}px`;
|
|
180
185
|
|
|
181
186
|
console.log('🖼️ FramePropertiesPanel: Panel CSS applied:', {
|
|
182
187
|
left: this.panel.style.left,
|
|
@@ -190,16 +195,17 @@ export class FramePropertiesPanel {
|
|
|
190
195
|
Object.assign(titleContainer.style, {
|
|
191
196
|
display: 'flex',
|
|
192
197
|
alignItems: 'center',
|
|
193
|
-
gap: '
|
|
194
|
-
padding: '
|
|
198
|
+
gap: '4px',
|
|
199
|
+
padding: '4px 6px'
|
|
195
200
|
});
|
|
196
201
|
|
|
197
202
|
// Лейбл
|
|
198
203
|
const titleLabel = document.createElement('span');
|
|
199
204
|
titleLabel.textContent = 'Название:';
|
|
200
|
-
titleLabel.style.fontSize = '
|
|
205
|
+
titleLabel.style.fontSize = '11px';
|
|
201
206
|
titleLabel.style.color = '#666';
|
|
202
|
-
titleLabel.style.
|
|
207
|
+
titleLabel.style.width = '56px';
|
|
208
|
+
titleLabel.style.textAlign = 'right';
|
|
203
209
|
|
|
204
210
|
// Поле ввода для названия
|
|
205
211
|
const titleInput = document.createElement('input');
|
|
@@ -207,11 +213,12 @@ export class FramePropertiesPanel {
|
|
|
207
213
|
titleInput.placeholder = 'Название фрейма';
|
|
208
214
|
Object.assign(titleInput.style, {
|
|
209
215
|
flex: '1',
|
|
210
|
-
padding: '4px
|
|
216
|
+
padding: '2px 4px',
|
|
211
217
|
border: '1px solid #ddd',
|
|
212
218
|
borderRadius: '4px',
|
|
213
219
|
fontSize: '12px',
|
|
214
|
-
outline: 'none'
|
|
220
|
+
outline: 'none',
|
|
221
|
+
height: '22px'
|
|
215
222
|
});
|
|
216
223
|
|
|
217
224
|
// Обработчик изменения названия
|
|
@@ -221,6 +228,14 @@ export class FramePropertiesPanel {
|
|
|
221
228
|
}
|
|
222
229
|
});
|
|
223
230
|
|
|
231
|
+
// Блокируем Delete/Backspace от всплытия, чтобы не удалялся весь фрейм при фокусе в поле
|
|
232
|
+
titleInput.addEventListener('keydown', (e) => {
|
|
233
|
+
const key = e.key;
|
|
234
|
+
if (key === 'Backspace' || key === 'Delete') {
|
|
235
|
+
e.stopPropagation();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
224
239
|
// Обработчик Enter для подтверждения
|
|
225
240
|
titleInput.addEventListener('keypress', (e) => {
|
|
226
241
|
if (e.key === 'Enter') {
|
|
@@ -239,22 +254,23 @@ export class FramePropertiesPanel {
|
|
|
239
254
|
Object.assign(colorContainer.style, {
|
|
240
255
|
display: 'flex',
|
|
241
256
|
alignItems: 'center',
|
|
242
|
-
gap: '
|
|
243
|
-
padding: '
|
|
257
|
+
gap: '4px',
|
|
258
|
+
padding: '4px 6px'
|
|
244
259
|
});
|
|
245
260
|
|
|
246
261
|
// Лейбл для цвета
|
|
247
262
|
const colorLabel = document.createElement('span');
|
|
248
263
|
colorLabel.textContent = 'Фон:';
|
|
249
|
-
colorLabel.style.fontSize = '
|
|
264
|
+
colorLabel.style.fontSize = '11px';
|
|
250
265
|
colorLabel.style.color = '#666';
|
|
251
|
-
colorLabel.style.
|
|
266
|
+
colorLabel.style.width = '56px';
|
|
267
|
+
colorLabel.style.textAlign = 'right';
|
|
252
268
|
|
|
253
269
|
// Кнопка выбора цвета
|
|
254
270
|
const colorButton = document.createElement('button');
|
|
255
271
|
Object.assign(colorButton.style, {
|
|
256
|
-
width: '
|
|
257
|
-
height: '
|
|
272
|
+
width: '20px',
|
|
273
|
+
height: '20px',
|
|
258
274
|
border: '1px solid #ccc',
|
|
259
275
|
borderRadius: '4px',
|
|
260
276
|
cursor: 'pointer',
|
|
@@ -274,8 +290,60 @@ export class FramePropertiesPanel {
|
|
|
274
290
|
colorContainer.appendChild(colorLabel);
|
|
275
291
|
colorContainer.appendChild(colorButton);
|
|
276
292
|
|
|
293
|
+
// Контейнер для типа фрейма
|
|
294
|
+
const typeContainer = document.createElement('div');
|
|
295
|
+
Object.assign(typeContainer.style, {
|
|
296
|
+
display: 'flex',
|
|
297
|
+
alignItems: 'center',
|
|
298
|
+
gap: '4px',
|
|
299
|
+
padding: '4px 6px'
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const typeLabel = document.createElement('span');
|
|
303
|
+
typeLabel.textContent = 'Тип:';
|
|
304
|
+
typeLabel.style.fontSize = '11px';
|
|
305
|
+
typeLabel.style.color = '#666';
|
|
306
|
+
typeLabel.style.width = '56px';
|
|
307
|
+
typeLabel.style.textAlign = 'right';
|
|
308
|
+
|
|
309
|
+
const typeSelect = document.createElement('select');
|
|
310
|
+
Object.assign(typeSelect.style, {
|
|
311
|
+
flex: '1',
|
|
312
|
+
padding: '2px 4px',
|
|
313
|
+
border: '1px solid #ddd',
|
|
314
|
+
borderRadius: '4px',
|
|
315
|
+
fontSize: '12px',
|
|
316
|
+
outline: 'none',
|
|
317
|
+
height: '22px',
|
|
318
|
+
maxWidth: '100%'
|
|
319
|
+
});
|
|
320
|
+
const options = [
|
|
321
|
+
{ value: 'custom', label: 'Произвольный' },
|
|
322
|
+
{ value: 'a4', label: 'A4' },
|
|
323
|
+
{ value: '1x1', label: '1:1' },
|
|
324
|
+
{ value: '4x3', label: '4:3' },
|
|
325
|
+
{ value: '16x9', label: '16:9' }
|
|
326
|
+
];
|
|
327
|
+
options.forEach(opt => {
|
|
328
|
+
const o = document.createElement('option');
|
|
329
|
+
o.value = opt.value;
|
|
330
|
+
o.textContent = opt.label;
|
|
331
|
+
typeSelect.appendChild(o);
|
|
332
|
+
});
|
|
333
|
+
this.frameTypeSelect = typeSelect;
|
|
334
|
+
|
|
335
|
+
typeSelect.addEventListener('change', () => {
|
|
336
|
+
if (!this.currentId) return;
|
|
337
|
+
const v = typeSelect.value;
|
|
338
|
+
this._applyFrameType(v);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
typeContainer.appendChild(typeLabel);
|
|
342
|
+
typeContainer.appendChild(typeSelect);
|
|
343
|
+
|
|
277
344
|
panel.appendChild(titleContainer);
|
|
278
345
|
panel.appendChild(colorContainer);
|
|
346
|
+
panel.appendChild(typeContainer);
|
|
279
347
|
|
|
280
348
|
// Создаем палитру цветов (скрытую)
|
|
281
349
|
this._createColorPalette(panel);
|
|
@@ -446,6 +514,81 @@ export class FramePropertiesPanel {
|
|
|
446
514
|
this.colorButton.title = `Цвет фона: ${hexColor}`;
|
|
447
515
|
}
|
|
448
516
|
|
|
517
|
+
_syncTypeFromObject() {
|
|
518
|
+
if (!this.frameTypeSelect || !this.currentId) return;
|
|
519
|
+
const objectData = this.core.getObjectData(this.currentId);
|
|
520
|
+
const t = (objectData && objectData.properties && objectData.properties.type) || 'custom';
|
|
521
|
+
this.frameTypeSelect.value = t;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_applyFrameType(typeValue) {
|
|
525
|
+
if (!this.currentId) return;
|
|
526
|
+
|
|
527
|
+
// 1) Обновляем тип и временно отключаем фиксацию пропорций на время программного ресайза
|
|
528
|
+
const willLockAfter = typeValue !== 'custom';
|
|
529
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
530
|
+
objectId: this.currentId,
|
|
531
|
+
updates: { properties: { type: typeValue, lockedAspect: false } }
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// 2) Для пресетов меняем размеры под аспект, сохраняя центр
|
|
535
|
+
if (!willLockAfter) return; // Произвольный: без изменения размеров
|
|
536
|
+
|
|
537
|
+
// Аспект по типу
|
|
538
|
+
const aspectMap = {
|
|
539
|
+
'a4': 210 / 297,
|
|
540
|
+
'1x1': 1,
|
|
541
|
+
'4x3': 4 / 3,
|
|
542
|
+
'16x9': 16 / 9
|
|
543
|
+
};
|
|
544
|
+
const aspect = aspectMap[typeValue] || 1;
|
|
545
|
+
|
|
546
|
+
// Текущие позиция и размер
|
|
547
|
+
const posData = { objectId: this.currentId, position: null };
|
|
548
|
+
const sizeData = { objectId: this.currentId, size: null };
|
|
549
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
550
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
551
|
+
if (!posData.position || !sizeData.size) return;
|
|
552
|
+
|
|
553
|
+
const oldX = posData.position.x;
|
|
554
|
+
const oldY = posData.position.y;
|
|
555
|
+
const oldW = Math.max(1, sizeData.size.width);
|
|
556
|
+
const oldH = Math.max(1, sizeData.size.height);
|
|
557
|
+
const cx = oldX + oldW / 2;
|
|
558
|
+
const cy = oldY + oldH / 2;
|
|
559
|
+
|
|
560
|
+
// Сохраняем визуальный масштаб: подбираем размеры с тем же приблизительным "площадью"
|
|
561
|
+
const area = oldW * oldH;
|
|
562
|
+
let newW = Math.max(1, Math.round(Math.sqrt(area * aspect)));
|
|
563
|
+
let newH = Math.max(1, Math.round(newW / aspect));
|
|
564
|
+
|
|
565
|
+
const newX = Math.round(cx - newW / 2);
|
|
566
|
+
const newY = Math.round(cy - newH / 2);
|
|
567
|
+
|
|
568
|
+
// Применяем через события resize для согласованности с историей/ядром
|
|
569
|
+
this.eventBus.emit(Events.Tool.ResizeUpdate, {
|
|
570
|
+
object: this.currentId,
|
|
571
|
+
size: { width: newW, height: newH },
|
|
572
|
+
position: { x: newX, y: newY }
|
|
573
|
+
});
|
|
574
|
+
this.eventBus.emit(Events.Tool.ResizeEnd, {
|
|
575
|
+
object: this.currentId,
|
|
576
|
+
oldSize: { width: oldW, height: oldH },
|
|
577
|
+
newSize: { width: newW, height: newH },
|
|
578
|
+
oldPosition: { x: oldX, y: oldY },
|
|
579
|
+
newPosition: { x: newX, y: newY }
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Обновим UI сразу
|
|
583
|
+
this._syncTypeFromObject();
|
|
584
|
+
|
|
585
|
+
// 3) Включаем обратно фиксацию пропорций (для пресетов)
|
|
586
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
587
|
+
objectId: this.currentId,
|
|
588
|
+
updates: { properties: { lockedAspect: true } }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
449
592
|
destroy() {
|
|
450
593
|
// Удаляем обработчик клика по документу
|
|
451
594
|
document.removeEventListener('click', this._documentClickHandler.bind(this));
|