@sequent-org/moodboard 1.4.48 → 1.4.51
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/commands/ApplyCropCommand.js +90 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +9 -2
- package/src/core/events/Events.js +2 -0
- package/src/core/flows/ClipboardFlow.js +13 -0
- package/src/core/flows/ObjectLifecycleFlow.js +25 -6
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +1 -1
- package/src/objects/FrameObject.js +90 -8
- package/src/objects/ImageObject.js +25 -4
- package/src/services/CropController.js +242 -0
- package/src/services/FrameService.js +32 -0
- package/src/tools/manager/ToolManagerLifecycle.js +4 -0
- package/src/tools/object-tools/DrawingTool.js +1 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +50 -0
- package/src/ui/CropOverlay.js +654 -0
- package/src/ui/FramePropertiesPanel.js +1061 -407
- package/src/ui/HtmlHandlesLayer.js +2 -0
- package/src/ui/ImagePropertiesPanel.js +1509 -4
- package/src/ui/comments/CommentThreadPopover.js +68 -0
- package/src/ui/connectors/ConnectionAnchorsLayer.js +12 -1
- package/src/ui/handles/HandlesDomRenderer.js +9 -2
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/styles/panels.css +750 -28
- package/src/ui/styles/workspace.css +21 -1
- package/src/utils/applyCrop.js +92 -0
- package/src/utils/applyRoundedMask.js +39 -0
|
@@ -125,6 +125,7 @@ export class CommentThreadPopover {
|
|
|
125
125
|
this._onThreadOpened = this._onThreadOpened.bind(this);
|
|
126
126
|
this._onRemote = () => this._refreshIfOpen();
|
|
127
127
|
this._onOpenDraftAt = this._onOpenDraftAt.bind(this);
|
|
128
|
+
this._onOpenImageDraft = this._onOpenImageDraft.bind(this);
|
|
128
129
|
this._onThreadDeleted = this._onThreadDeleted.bind(this);
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -143,6 +144,7 @@ export class CommentThreadPopover {
|
|
|
143
144
|
this.eventBus.on(Events.Comment.RemoteUpdated, this._onRemote);
|
|
144
145
|
this.eventBus.on(Events.Comment.MessageAdded, this._onRemote);
|
|
145
146
|
this.eventBus.on(Events.Comment.OpenDraftAt, this._onOpenDraftAt);
|
|
147
|
+
this.eventBus.on(Events.Comment.OpenImageDraft, this._onOpenImageDraft);
|
|
146
148
|
this.eventBus.on(Events.Comment.ThreadDeleted, this._onThreadDeleted);
|
|
147
149
|
this.eventBus.on(Events.Viewport.Changed, () => this.reposition());
|
|
148
150
|
this.eventBus.on(Events.Tool.PanUpdate, () => this.reposition());
|
|
@@ -155,6 +157,7 @@ export class CommentThreadPopover {
|
|
|
155
157
|
this.eventBus.off(Events.Comment.RemoteUpdated, this._onRemote);
|
|
156
158
|
this.eventBus.off(Events.Comment.MessageAdded, this._onRemote);
|
|
157
159
|
this.eventBus.off(Events.Comment.OpenDraftAt, this._onOpenDraftAt);
|
|
160
|
+
this.eventBus.off(Events.Comment.OpenImageDraft, this._onOpenImageDraft);
|
|
158
161
|
this.eventBus.off(Events.Comment.ThreadDeleted, this._onThreadDeleted);
|
|
159
162
|
if (this.layer) this.layer.remove();
|
|
160
163
|
this.layer = null;
|
|
@@ -178,6 +181,70 @@ export class CommentThreadPopover {
|
|
|
178
181
|
this.openDraftAt({ x: local.x, y: local.y });
|
|
179
182
|
}
|
|
180
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Открывает черновик комментария, привязанный к центру объекта.
|
|
186
|
+
* Если на объекте уже есть комментарии, смещает точку от занятых позиций.
|
|
187
|
+
*/
|
|
188
|
+
_onOpenImageDraft({ objectId }) {
|
|
189
|
+
if (!objectId) return;
|
|
190
|
+
|
|
191
|
+
const posData = { objectId, position: null };
|
|
192
|
+
const sizeData = { objectId, size: null };
|
|
193
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
194
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
195
|
+
|
|
196
|
+
if (!posData.position || !sizeData.size) return;
|
|
197
|
+
|
|
198
|
+
const { x: left, y: top } = posData.position;
|
|
199
|
+
const { width, height } = sizeData.size;
|
|
200
|
+
|
|
201
|
+
const anchored = this.commentService.getAllThreads().filter(
|
|
202
|
+
(t) => t.anchor_object_id === objectId
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// anchor_dx/anchor_dy хранятся как смещение от top-left объекта,
|
|
206
|
+
// поэтому центр = (width/2, height/2) в этом пространстве
|
|
207
|
+
const { dx, dy } = this._findFreeCommentOffset(anchored, width / 2, height / 2);
|
|
208
|
+
|
|
209
|
+
this.openDraftAt(
|
|
210
|
+
{ x: left + dx, y: top + dy },
|
|
211
|
+
{ anchor_object_id: objectId, anchor_dx: dx, anchor_dy: dy }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Ищет свободный anchor-offset от top-left объекта.
|
|
217
|
+
* Начинает с центра (centerDx, centerDy), затем пробует 8 направлений шагами по 10px.
|
|
218
|
+
* «Свободным» считается offset, удалённый от всех занятых не менее чем на 12px.
|
|
219
|
+
*/
|
|
220
|
+
_findFreeCommentOffset(anchoredThreads, centerDx, centerDy) {
|
|
221
|
+
const STEP = 10;
|
|
222
|
+
const MIN_DIST = 12;
|
|
223
|
+
|
|
224
|
+
const isFree = (dx, dy) => anchoredThreads.every((t) => {
|
|
225
|
+
const ex = t.anchor_dx || 0;
|
|
226
|
+
const ey = t.anchor_dy || 0;
|
|
227
|
+
return Math.sqrt((dx - ex) ** 2 + (dy - ey) ** 2) >= MIN_DIST;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (isFree(centerDx, centerDy)) return { dx: centerDx, dy: centerDy };
|
|
231
|
+
|
|
232
|
+
const directions = [
|
|
233
|
+
[1, 0], [0, 1], [-1, 0], [0, -1],
|
|
234
|
+
[1, 1], [-1, 1], [1, -1], [-1, -1],
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
for (let radius = 1; radius <= 20; radius++) {
|
|
238
|
+
for (const [dirX, dirY] of directions) {
|
|
239
|
+
const dx = centerDx + dirX * STEP * radius;
|
|
240
|
+
const dy = centerDy + dirY * STEP * radius;
|
|
241
|
+
if (isFree(dx, dy)) return { dx, dy };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { dx: centerDx + STEP, dy: centerDy };
|
|
246
|
+
}
|
|
247
|
+
|
|
181
248
|
_onThreadOpened({ threadId, pinEl }) {
|
|
182
249
|
if (threadId == null && this._draftWorld) return;
|
|
183
250
|
this._draftWorld = null;
|
|
@@ -203,6 +270,7 @@ export class CommentThreadPopover {
|
|
|
203
270
|
this._closePalette();
|
|
204
271
|
this._disarmOutsideClose();
|
|
205
272
|
if (wasDraft) this.eventBus.emit(Events.Comment.DraftClosed, {});
|
|
273
|
+
this.eventBus.emit(Events.Comment.PopoverClosed, {});
|
|
206
274
|
}
|
|
207
275
|
|
|
208
276
|
reposition() {
|
|
@@ -18,6 +18,7 @@ export class ConnectionAnchorsLayer {
|
|
|
18
18
|
this.hoveredObjectId = null;
|
|
19
19
|
this._dragController = null;
|
|
20
20
|
this._onAnchorPointerDown = null;
|
|
21
|
+
this._commentPopoverOpen = false;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
attach() {
|
|
@@ -94,7 +95,12 @@ export class ConnectionAnchorsLayer {
|
|
|
94
95
|
[Events.Viewport.Changed, () => this.update()],
|
|
95
96
|
[Events.UI.ZoomPercent, () => this.update()],
|
|
96
97
|
[Events.History.Changed, () => this.update()],
|
|
97
|
-
[Events.Board.Loaded, () => this.update()]
|
|
98
|
+
[Events.Board.Loaded, () => this.update()],
|
|
99
|
+
[Events.Comment.ThreadOpened, () => { this._commentPopoverOpen = true; this.layer.innerHTML = ''; }],
|
|
100
|
+
[Events.Comment.DraftOpened, () => { this._commentPopoverOpen = true; this.layer.innerHTML = ''; }],
|
|
101
|
+
[Events.Comment.DraftClosed, () => { this._commentPopoverOpen = false; this.update(); }],
|
|
102
|
+
[Events.Comment.ThreadDeleted, () => { this._commentPopoverOpen = false; this.update(); }],
|
|
103
|
+
[Events.Comment.PopoverClosed, () => { this._commentPopoverOpen = false; this.update(); }],
|
|
98
104
|
];
|
|
99
105
|
|
|
100
106
|
bindings.forEach(([event, handler]) => {
|
|
@@ -137,6 +143,11 @@ export class ConnectionAnchorsLayer {
|
|
|
137
143
|
|
|
138
144
|
update() {
|
|
139
145
|
if (!this.layer) return;
|
|
146
|
+
if (this._commentPopoverOpen) return;
|
|
147
|
+
if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer?._cropMode) {
|
|
148
|
+
this.layer.innerHTML = '';
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
140
151
|
this.layer.innerHTML = '';
|
|
141
152
|
|
|
142
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();
|