@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.
@@ -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(Events.Tool.GetObjectPixi, req);
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();