@sequent-org/moodboard 1.4.49 → 1.4.52
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/core/bootstrap/CoreInitializer.js +3 -0
- package/src/core/commands/ApplyCropCommand.js +90 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +9 -2
- package/src/core/flows/ObjectLifecycleFlow.js +29 -8
- package/src/objects/FrameObject.js +242 -21
- package/src/objects/ImageObject.js +23 -7
- package/src/services/CropController.js +242 -0
- package/src/services/FrameService.js +87 -9
- package/src/services/ImageAttachmentService.js +260 -0
- package/src/tools/manager/ToolManagerLifecycle.js +4 -0
- package/src/tools/object-tools/DrawingTool.js +1 -0
- package/src/tools/object-tools/placement/GhostController.js +2 -2
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +9 -1
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +13 -4
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +15 -10
- package/src/tools/object-tools/selection/TransformInteractionController.js +50 -0
- package/src/ui/CropOverlay.js +654 -0
- package/src/ui/FramePropertiesPanel.js +1184 -405
- package/src/ui/HtmlHandlesLayer.js +2 -0
- package/src/ui/ImagePropertiesPanel.js +85 -41
- package/src/ui/connectors/ConnectionAnchorsLayer.js +4 -0
- package/src/ui/handles/HandlesDomRenderer.js +9 -2
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/styles/panels.css +402 -27
- package/src/ui/styles/workspace.css +20 -0
- package/src/utils/applyCrop.js +92 -0
|
@@ -27,6 +27,7 @@ export class HtmlHandlesLayer {
|
|
|
27
27
|
this.handles = {};
|
|
28
28
|
this._drag = null;
|
|
29
29
|
this._handlesSuppressed = false; // скрывать ручки во время перетаскивания/трансформаций
|
|
30
|
+
this._cropMode = false; // полное подавление во время crop (ни рамка, ни ручки)
|
|
30
31
|
this._groupRotationPreview = null;
|
|
31
32
|
|
|
32
33
|
// Ссылки на обработчики, чтобы корректно отписаться при destroy()
|
|
@@ -96,6 +97,7 @@ export class HtmlHandlesLayer {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
update() {
|
|
100
|
+
if (this._cropMode) return;
|
|
99
101
|
// Дополнительная защита: если слой или core уже уничтожены,
|
|
100
102
|
// выходим, чтобы не получить ошибок при resize/смене DPR
|
|
101
103
|
if (!this.core || !this.core.pixi || !this.core.pixi.app || !this.layer) return;
|
|
@@ -2,6 +2,7 @@ import * as PIXI from 'pixi.js';
|
|
|
2
2
|
import { Events } from '../core/events/Events.js';
|
|
3
3
|
import { EditFileNameCommand } from '../core/commands/EditFileNameCommand.js';
|
|
4
4
|
import { applyRoundedMask } from '../utils/applyRoundedMask.js';
|
|
5
|
+
import { CropController } from '../services/CropController.js';
|
|
5
6
|
|
|
6
7
|
const ICONS = {
|
|
7
8
|
download: `<svg width="24" height="24" fill="none" viewBox="0 0 24 24" type="download"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.75 14.75V16.25C4.75 17.9069 6.09315 19.25 7.75 19.25H16.25C17.9069 19.25 19.25 17.9069 19.25 16.25V14.75"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 14.25L12 4.75"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.75 10.75L12 14.25L15.25 10.75"></path></svg>`,
|
|
@@ -38,6 +39,9 @@ export class ImagePropertiesPanel {
|
|
|
38
39
|
this.currentId = null;
|
|
39
40
|
this._handlers = null;
|
|
40
41
|
|
|
42
|
+
this._cropController = new CropController({ core, eventBus, container });
|
|
43
|
+
this._cropController._onActivate = (active) => this._onCropActivate(active);
|
|
44
|
+
|
|
41
45
|
this._attachEvents();
|
|
42
46
|
this._createPanel();
|
|
43
47
|
}
|
|
@@ -125,6 +129,9 @@ export class ImagePropertiesPanel {
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
showFor(objectId) {
|
|
132
|
+
if (this.currentId !== objectId) {
|
|
133
|
+
this._cropController?.cancel();
|
|
134
|
+
}
|
|
128
135
|
this.currentId = objectId;
|
|
129
136
|
if (this.panel) {
|
|
130
137
|
this.panel.style.display = 'flex';
|
|
@@ -148,13 +155,12 @@ export class ImagePropertiesPanel {
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
_toggleLocked() {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
+
if (!this.currentId) return;
|
|
159
|
+
const newLocked = !this._isLocked();
|
|
160
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
161
|
+
objectId: this.currentId,
|
|
162
|
+
updates: { properties: { locked: newLocked } },
|
|
163
|
+
});
|
|
158
164
|
this._updateLockUI();
|
|
159
165
|
this.reposition();
|
|
160
166
|
}
|
|
@@ -163,7 +169,7 @@ export class ImagePropertiesPanel {
|
|
|
163
169
|
if (!this._btn_lock) return;
|
|
164
170
|
const locked = this._isLocked();
|
|
165
171
|
|
|
166
|
-
this._btn_lock.innerHTML = locked ? ICONS.
|
|
172
|
+
this._btn_lock.innerHTML = locked ? ICONS.lock : ICONS.unlock;
|
|
167
173
|
this._btn_lock.title = locked ? 'Разблокировать' : 'Заблокировать';
|
|
168
174
|
|
|
169
175
|
if (Array.isArray(this._lockableEls)) {
|
|
@@ -178,6 +184,7 @@ export class ImagePropertiesPanel {
|
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
hide() {
|
|
187
|
+
this._cropController?.cancel();
|
|
181
188
|
this.currentId = null;
|
|
182
189
|
if (this.panel) {
|
|
183
190
|
this.panel.style.display = 'none';
|
|
@@ -185,6 +192,36 @@ export class ImagePropertiesPanel {
|
|
|
185
192
|
this._closeBorderRadiusPopover();
|
|
186
193
|
}
|
|
187
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Вызывается CropController при входе/выходе из режима crop.
|
|
197
|
+
* Прячет тулбар и ручки выделения на время crop; восстанавливает по завершении.
|
|
198
|
+
*/
|
|
199
|
+
_onCropActivate(active) {
|
|
200
|
+
if (!this.panel) return;
|
|
201
|
+
|
|
202
|
+
const handlesLayer = typeof window !== 'undefined' ? window.moodboardHtmlHandlesLayer : null;
|
|
203
|
+
const anchorsLayer = typeof window !== 'undefined' ? window.moodboardConnectionAnchorsLayer : null;
|
|
204
|
+
|
|
205
|
+
if (active) {
|
|
206
|
+
this.panel.style.display = 'none';
|
|
207
|
+
if (handlesLayer) {
|
|
208
|
+
handlesLayer._cropMode = true;
|
|
209
|
+
handlesLayer.hide();
|
|
210
|
+
}
|
|
211
|
+
anchorsLayer?.update();
|
|
212
|
+
} else {
|
|
213
|
+
// Восстанавливаем тулбар только если объект ещё выделен
|
|
214
|
+
if (this.currentId) {
|
|
215
|
+
this.panel.style.display = 'flex';
|
|
216
|
+
}
|
|
217
|
+
if (handlesLayer) {
|
|
218
|
+
handlesLayer._cropMode = false;
|
|
219
|
+
handlesLayer.update();
|
|
220
|
+
}
|
|
221
|
+
anchorsLayer?.update();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
188
225
|
_closeBorderRadiusPopover() {
|
|
189
226
|
if (this._borderRadiusPopover) {
|
|
190
227
|
this._borderRadiusPopover.classList.remove('is-open');
|
|
@@ -316,12 +353,6 @@ export class ImagePropertiesPanel {
|
|
|
316
353
|
mainBtn.className = 'ipp-btn-split-main';
|
|
317
354
|
mainBtn.title = 'Обрезать';
|
|
318
355
|
mainBtn.innerHTML = ICONS.crop;
|
|
319
|
-
mainBtn.addEventListener('click', (e) => {
|
|
320
|
-
e.preventDefault();
|
|
321
|
-
e.stopPropagation();
|
|
322
|
-
// Логика применения текущего кропа
|
|
323
|
-
});
|
|
324
|
-
|
|
325
356
|
const expandBtn = document.createElement('button');
|
|
326
357
|
expandBtn.className = 'ipp-btn-split-expand';
|
|
327
358
|
expandBtn.title = 'Выбрать пропорции';
|
|
@@ -330,15 +361,36 @@ export class ImagePropertiesPanel {
|
|
|
330
361
|
const dropdown = document.createElement('div');
|
|
331
362
|
dropdown.className = 'ipp-crop-dropdown';
|
|
332
363
|
|
|
364
|
+
const toggleCropDropdown = (e) => {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
const isOpen = dropdown.classList.contains('is-open');
|
|
368
|
+
|
|
369
|
+
document.querySelectorAll('.ipp-crop-dropdown.is-open, .ipp-more-dropdown.is-open, .ipp-border-radius-popover.is-open, .ipp-border-style-popover.is-open').forEach(el => {
|
|
370
|
+
el.classList.remove('is-open');
|
|
371
|
+
});
|
|
372
|
+
document.querySelectorAll('.ipp-btn-split-expand.is-expanded, .ipp-btn.is-active').forEach(el => {
|
|
373
|
+
el.classList.remove('is-expanded', 'is-active');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (!isOpen) {
|
|
377
|
+
dropdown.classList.add('is-open');
|
|
378
|
+
expandBtn.classList.add('is-expanded');
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
mainBtn.addEventListener('click', toggleCropDropdown);
|
|
383
|
+
expandBtn.addEventListener('click', toggleCropDropdown);
|
|
384
|
+
|
|
333
385
|
const items = [
|
|
334
|
-
{ id: 'custom', label: '
|
|
335
|
-
{ id: 'original', label: '
|
|
386
|
+
{ id: 'custom', label: 'Произвольный', icon: ICONS.ddCustom },
|
|
387
|
+
{ id: 'original', label: 'Оригинал', icon: ICONS.ddOriginal },
|
|
336
388
|
{ divider: true },
|
|
337
|
-
{ id: 'circle', label: '
|
|
338
|
-
{ id: 'square', label: '
|
|
339
|
-
{ id: 'portrait', label: '
|
|
340
|
-
{ id: 'landscape', label: '
|
|
341
|
-
{ id: 'wide', label: '
|
|
389
|
+
{ id: 'circle', label: 'Круг', icon: ICONS.ddCircle },
|
|
390
|
+
{ id: 'square', label: 'Квадрат', icon: ICONS.ddSquare },
|
|
391
|
+
{ id: 'portrait', label: 'Портрет', icon: ICONS.ddPortrait, ratio: '3:4' },
|
|
392
|
+
{ id: 'landscape', label: 'Пейзаж', icon: ICONS.ddLandscape, ratio: '4:3' },
|
|
393
|
+
{ id: 'wide', label: 'Широкий', icon: ICONS.ddWide, ratio: '16:9' },
|
|
342
394
|
];
|
|
343
395
|
|
|
344
396
|
items.forEach(item => {
|
|
@@ -372,33 +424,16 @@ export class ImagePropertiesPanel {
|
|
|
372
424
|
btn.addEventListener('click', (e) => {
|
|
373
425
|
e.preventDefault();
|
|
374
426
|
e.stopPropagation();
|
|
375
|
-
// Логика выбора пропорции
|
|
376
427
|
dropdown.classList.remove('is-open');
|
|
377
428
|
expandBtn.classList.remove('is-expanded');
|
|
429
|
+
if (this.currentId) {
|
|
430
|
+
this._cropController.start(this.currentId, item.id);
|
|
431
|
+
}
|
|
378
432
|
});
|
|
379
433
|
|
|
380
434
|
dropdown.appendChild(btn);
|
|
381
435
|
});
|
|
382
436
|
|
|
383
|
-
expandBtn.addEventListener('click', (e) => {
|
|
384
|
-
e.preventDefault();
|
|
385
|
-
e.stopPropagation();
|
|
386
|
-
const isOpen = dropdown.classList.contains('is-open');
|
|
387
|
-
|
|
388
|
-
// Закрываем все другие дропдауны, если есть (на будущее)
|
|
389
|
-
document.querySelectorAll('.ipp-crop-dropdown.is-open, .ipp-more-dropdown.is-open').forEach(el => {
|
|
390
|
-
el.classList.remove('is-open');
|
|
391
|
-
});
|
|
392
|
-
document.querySelectorAll('.ipp-btn-split-expand.is-expanded, .ipp-btn.is-active').forEach(el => {
|
|
393
|
-
el.classList.remove('is-expanded', 'is-active');
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
if (!isOpen) {
|
|
397
|
-
dropdown.classList.add('is-open');
|
|
398
|
-
expandBtn.classList.add('is-expanded');
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
437
|
wrapper.appendChild(mainBtn);
|
|
403
438
|
wrapper.appendChild(expandBtn);
|
|
404
439
|
wrapper.appendChild(dropdown);
|
|
@@ -1015,6 +1050,14 @@ export class ImagePropertiesPanel {
|
|
|
1015
1050
|
|
|
1016
1051
|
if (item.id === 'copy') {
|
|
1017
1052
|
this.eventBus.emit(Events.Keyboard.Copy);
|
|
1053
|
+
} else if (item.id === 'bring-front') {
|
|
1054
|
+
if (this.currentId) this.eventBus.emit(Events.UI.LayerBringToFront, { objectId: this.currentId });
|
|
1055
|
+
} else if (item.id === 'bring-forward') {
|
|
1056
|
+
if (this.currentId) this.eventBus.emit(Events.UI.LayerBringForward, { objectId: this.currentId });
|
|
1057
|
+
} else if (item.id === 'send-backward') {
|
|
1058
|
+
if (this.currentId) this.eventBus.emit(Events.UI.LayerSendBackward, { objectId: this.currentId });
|
|
1059
|
+
} else if (item.id === 'send-back') {
|
|
1060
|
+
if (this.currentId) this.eventBus.emit(Events.UI.LayerSendToBack, { objectId: this.currentId });
|
|
1018
1061
|
} else if (item.id === 'duplicate') {
|
|
1019
1062
|
this._duplicateImage();
|
|
1020
1063
|
} else if (item.id === 'lock') {
|
|
@@ -1442,6 +1485,7 @@ export class ImagePropertiesPanel {
|
|
|
1442
1485
|
}
|
|
1443
1486
|
|
|
1444
1487
|
destroy() {
|
|
1488
|
+
this._cropController?.destroy();
|
|
1445
1489
|
if (!this.eventBus || !this._handlers) return;
|
|
1446
1490
|
|
|
1447
1491
|
this.eventBus.off(Events.Tool.SelectionAdd, this._handlers.onSelectionAdd);
|
|
@@ -144,6 +144,10 @@ export class ConnectionAnchorsLayer {
|
|
|
144
144
|
update() {
|
|
145
145
|
if (!this.layer) return;
|
|
146
146
|
if (this._commentPopoverOpen) return;
|
|
147
|
+
if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer?._cropMode) {
|
|
148
|
+
this.layer.innerHTML = '';
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
147
151
|
this.layer.innerHTML = '';
|
|
148
152
|
|
|
149
153
|
const selection = Array.from(this.core?.selectTool?.selectedObjects || []);
|
|
@@ -1157,6 +1157,7 @@ export class HandlesDomRenderer {
|
|
|
1157
1157
|
let isFrameTarget = false;
|
|
1158
1158
|
let isMindmapTarget = false;
|
|
1159
1159
|
let isMindmapOnlyGroupTarget = false;
|
|
1160
|
+
let isLockedTarget = false;
|
|
1160
1161
|
let isRevitScreenshotTarget = false;
|
|
1161
1162
|
let revitViewPayload = null;
|
|
1162
1163
|
let isModel3dScreenshotTarget = false;
|
|
@@ -1167,11 +1168,12 @@ export class HandlesDomRenderer {
|
|
|
1167
1168
|
const hiddenIncomingSide = { value: null };
|
|
1168
1169
|
if (id !== '__group__') {
|
|
1169
1170
|
const req = { objectId: id, pixiObject: null };
|
|
1170
|
-
this.host.eventBus.emit(
|
|
1171
|
+
this.host.eventBus.emit('tool:get:object:pixi', req);
|
|
1171
1172
|
const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
|
|
1172
1173
|
isFileTarget = mbType === 'file';
|
|
1173
1174
|
isFrameTarget = mbType === 'frame';
|
|
1174
1175
|
isMindmapTarget = mbType === 'mindmap';
|
|
1176
|
+
isLockedTarget = !!req.pixiObject?._mb?.properties?.locked;
|
|
1175
1177
|
isRevitScreenshotTarget = mbType === 'revit-screenshot-img';
|
|
1176
1178
|
revitViewPayload = req.pixiObject?._mb?.properties?.view || null;
|
|
1177
1179
|
isModel3dScreenshotTarget = mbType === 'model3d-screenshot-img';
|
|
@@ -1199,9 +1201,14 @@ export class HandlesDomRenderer {
|
|
|
1199
1201
|
if (selectionIds.length > 0) {
|
|
1200
1202
|
const byId = new Map((this.host.core?.state?.state?.objects || []).map((obj) => [obj?.id, obj]));
|
|
1201
1203
|
isMindmapOnlyGroupTarget = selectionIds.every((selectedId) => byId.get(selectedId)?.type === 'mindmap');
|
|
1204
|
+
isLockedTarget = selectionIds.some(selId => {
|
|
1205
|
+
const req = { objectId: selId, pixiObject: null };
|
|
1206
|
+
this.host.eventBus.emit('tool:get:object:pixi', req);
|
|
1207
|
+
return !!req.pixiObject?._mb?.properties?.locked;
|
|
1208
|
+
});
|
|
1202
1209
|
}
|
|
1203
1210
|
}
|
|
1204
|
-
const isNonResizableTarget = isFileTarget || isMindmapTarget || isMindmapOnlyGroupTarget;
|
|
1211
|
+
const isNonResizableTarget = isFileTarget || isMindmapTarget || isMindmapOnlyGroupTarget || isLockedTarget;
|
|
1205
1212
|
|
|
1206
1213
|
const left = Math.round(cssRect.left);
|
|
1207
1214
|
const top = Math.round(cssRect.top);
|
|
@@ -114,6 +114,7 @@ export class HandlesEventBridge {
|
|
|
114
114
|
[Events.UI.ZoomPercent, () => this.host.update()],
|
|
115
115
|
[Events.Tool.PanUpdate, () => this.host.update()],
|
|
116
116
|
[Events.Viewport.Changed, () => this.host.update()],
|
|
117
|
+
[Events.Object.TransformUpdated, () => this.host.update()],
|
|
117
118
|
[Events.History.Changed, (data) => {
|
|
118
119
|
if (data?.lastUndone || data?.lastRedone) {
|
|
119
120
|
this.host._endGroupRotationPreview();
|