@sequent-org/moodboard 1.4.32 → 1.4.34
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 +5 -1
- package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/src/assets/icons/attachments.svg +3 -1
- package/src/assets/icons/comments.svg +2 -2
- package/src/assets/icons/connector.svg +6 -0
- package/src/assets/icons/emoji.svg +6 -1
- package/src/assets/icons/frame.svg +4 -1
- package/src/assets/icons/image.svg +5 -1
- package/src/assets/icons/laser.svg +1 -0
- package/src/assets/icons/lasso.svg +5 -0
- package/src/assets/icons/mindmap.svg +10 -2
- package/src/assets/icons/note.svg +4 -1
- package/src/assets/icons/pan.svg +5 -2
- package/src/assets/icons/pencil.svg +4 -1
- package/src/assets/icons/reactions.svg +5 -0
- package/src/assets/icons/redo.svg +3 -2
- package/src/assets/icons/select.svg +2 -8
- package/src/assets/icons/shapes.svg +5 -1
- package/src/assets/icons/text-add.svg +15 -1
- package/src/assets/icons/undo.svg +3 -2
- package/src/assets/reactions/1f44d.svg +20 -0
- package/src/assets/reactions/1f44e.svg +20 -0
- package/src/assets/reactions/2705.svg +20 -0
- package/src/assets/reactions/274c.svg +19 -0
- package/src/assets/reactions/2753.svg +20 -0
- package/src/assets/reactions/2764.svg +22 -0
- package/src/assets/reactions/2b50.svg +19 -0
- package/src/assets/reactions/plus-one.svg +25 -0
- package/src/core/PixiEngine.js +23 -0
- package/src/core/bootstrap/CoreInitializer.js +43 -0
- package/src/core/commands/GroupDeleteCommand.js +13 -1
- package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
- package/src/core/commands/UpdateTextStyleCommand.js +17 -6
- package/src/core/commands/index.js +3 -0
- package/src/core/events/Events.js +22 -0
- package/src/core/flows/LayerAndViewportFlow.js +1 -0
- package/src/core/flows/ObjectLifecycleFlow.js +155 -7
- package/src/core/index.js +28 -1
- package/src/grid/CrossGridZoomPhases.js +3 -3
- package/src/initNoBundler.js +1 -1
- package/src/moodboard/DataManager.js +28 -0
- package/src/moodboard/MoodBoard.js +27 -0
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
- package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
- package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
- package/src/objects/ConnectorObject.js +2 -2
- package/src/objects/FrameObject.js +119 -59
- package/src/objects/ShapeObject.js +49 -74
- package/src/objects/shape/ShapeDrawer.js +210 -0
- package/src/services/ConnectorBindingResolver.js +112 -0
- package/src/services/ConnectorRouter.js +210 -0
- package/src/services/ai/ChatSessionController.js +14 -8
- package/src/services/comments/CommentService.js +344 -0
- package/src/tools/object-tools/CommentTool.js +85 -0
- package/src/tools/object-tools/DrawingTool.js +110 -10
- package/src/tools/object-tools/LaserPointerTool.js +121 -0
- package/src/tools/object-tools/SelectTool.js +25 -1
- package/src/tools/object-tools/TextTool.js +6 -1
- package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
- package/src/tools/object-tools/connector/connectorGesture.js +33 -19
- package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
- package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
- package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
- package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
- package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
- package/src/ui/CommentPopover.js +6 -0
- package/src/ui/CommentsBar.js +91 -0
- package/src/ui/ConnectorPropertiesPanel.js +150 -0
- package/src/ui/ContextMenu.js +25 -0
- package/src/ui/DrawingPropertiesPanel.js +362 -0
- package/src/ui/FilePropertiesPanel.js +5 -0
- package/src/ui/FramePropertiesPanel.js +5 -0
- package/src/ui/HtmlTextLayer.js +246 -66
- package/src/ui/NotePropertiesPanel.js +6 -0
- package/src/ui/ShapePropertiesPanel.js +307 -0
- package/src/ui/TextPropertiesPanel.js +100 -1
- package/src/ui/Toolbar.js +25 -2
- package/src/ui/Topbar.js +2 -2
- package/src/ui/animation/HoverLiftController.js +6 -7
- package/src/ui/chat/ChatComposer.js +63 -9
- package/src/ui/chat/ChatWindow.js +329 -166
- package/src/ui/comments/CommentListPanel.js +213 -0
- package/src/ui/comments/CommentPinLayer.js +448 -0
- package/src/ui/comments/CommentThreadPopover.js +539 -0
- package/src/ui/comments/commentFormat.js +32 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
- package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
- package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
- package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
- package/src/ui/connectors/ConnectorLayer.js +264 -57
- package/src/ui/handles/HandlesDomRenderer.js +5 -13
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
- package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
- package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
- package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
- package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
- package/src/ui/styles/chat.css +710 -18
- package/src/ui/styles/index.css +1 -0
- package/src/ui/styles/panels.css +112 -2
- package/src/ui/styles/shape-properties-panel.css +250 -0
- package/src/ui/styles/toolbar.css +7 -2
- package/src/ui/styles/topbar.css +1 -1
- package/src/ui/styles/workspace.css +257 -6
- package/src/ui/text-properties/TextFormatControls.js +88 -0
- package/src/ui/text-properties/TextListRenderer.js +137 -0
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
- package/src/ui/toolbar/ReactionsPopupController.js +88 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
- package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
- package/src/ui/toolbar/ToolbarRenderer.js +9 -1
- package/src/ui/toolbar/ToolbarStateController.js +4 -1
- package/src/utils/iconLoader.js +17 -16
- package/src/utils/markdown.js +14 -0
- package/src/utils/richText.js +125 -0
|
@@ -60,7 +60,6 @@ const BOARD_IMAGE_REARRANGE_MS = 520;
|
|
|
60
60
|
const BOARD_IMAGE_PENDING_ENTER_FACTOR = 1.6;
|
|
61
61
|
// Каскад между блоками одного батча (мс): пользователь видит, что они приезжают друг за другом.
|
|
62
62
|
const BOARD_IMAGE_PENDING_STAGGER_MS = 90;
|
|
63
|
-
const REFERENCE_DRAG_PREVIEW_SIZE = 96;
|
|
64
63
|
|
|
65
64
|
const MODEL_OPTIONS = [
|
|
66
65
|
{
|
|
@@ -145,12 +144,14 @@ export class ChatWindow {
|
|
|
145
144
|
this._pendingOverlays = new Map();
|
|
146
145
|
this._pendingOverlayTimers = new Map();
|
|
147
146
|
this._boardImageShiftAnimations = new Map();
|
|
148
|
-
this.
|
|
149
|
-
this._draggedReferenceObject = null;
|
|
150
|
-
this._draggedReferenceStartPosition = null;
|
|
151
|
-
this._referenceDragPreview = null;
|
|
152
|
-
this._referenceDragHandlers = null;
|
|
147
|
+
this._pendingBatchOffsets = new Map();
|
|
153
148
|
this._clearSelectionOnSendClick = null;
|
|
149
|
+
this._selectionHandlers = null;
|
|
150
|
+
this._viewportHandlers = null;
|
|
151
|
+
// Окно от BoxSelectStart до BoxSelectCommit: в это время SelectionAdd
|
|
152
|
+
// приходит на каждый mousemove и не должен пушить превью в чат —
|
|
153
|
+
// финальный набор картинок мы получим из BoxSelectCommit по strict-contains.
|
|
154
|
+
this._boxSelectActive = false;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
attach() {
|
|
@@ -180,7 +181,8 @@ export class ChatWindow {
|
|
|
180
181
|
this._clearSelectionOnSendClick = () => this._clearBoardSelection();
|
|
181
182
|
this._refs.send.addEventListener('click', this._clearSelectionOnSendClick);
|
|
182
183
|
this._composer.attach();
|
|
183
|
-
this.
|
|
184
|
+
this._attachSelectionEvents();
|
|
185
|
+
this._attachViewportSync();
|
|
184
186
|
|
|
185
187
|
this._extendedPromptModal = new ChatExtendedPromptModal(
|
|
186
188
|
this._container,
|
|
@@ -263,15 +265,16 @@ export class ChatWindow {
|
|
|
263
265
|
if (!this._attached) return;
|
|
264
266
|
this._clearPendingOverlays();
|
|
265
267
|
this._cancelBoardImageShiftAnimations();
|
|
266
|
-
this._clearReferenceDragState();
|
|
267
268
|
if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; }
|
|
268
269
|
if (this._clearSelectionOnSendClick && this._refs?.send) {
|
|
269
270
|
this._refs.send.removeEventListener('click', this._clearSelectionOnSendClick);
|
|
270
271
|
this._clearSelectionOnSendClick = null;
|
|
271
272
|
}
|
|
272
|
-
this.
|
|
273
|
+
this._detachSelectionEvents();
|
|
274
|
+
this._detachViewportSync();
|
|
273
275
|
this._shiftedForImageBatchKeys.clear();
|
|
274
276
|
this._boardImageShiftHistory.clear();
|
|
277
|
+
this._pendingBatchOffsets.clear();
|
|
275
278
|
this._composer?.destroy();
|
|
276
279
|
this._extendedPromptModal?.destroy();
|
|
277
280
|
this._contentTypeMenu?.destroy();
|
|
@@ -340,6 +343,7 @@ export class ChatWindow {
|
|
|
340
343
|
if (state.status !== 'streaming') {
|
|
341
344
|
this._revertFailedBatchShifts(state.messages);
|
|
342
345
|
}
|
|
346
|
+
this._cleanupPlacedBatchOffsets(state.messages);
|
|
343
347
|
this._messageList.render(state.messages);
|
|
344
348
|
this._contentTypeMenu.refresh();
|
|
345
349
|
this._modelMenu.refresh();
|
|
@@ -369,35 +373,62 @@ export class ChatWindow {
|
|
|
369
373
|
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
370
374
|
const s = world?.scale?.x || 1;
|
|
371
375
|
|
|
372
|
-
|
|
376
|
+
// Смещаем уже размещённые board-объекты для каждого батча (дедупликация внутри метода)
|
|
377
|
+
const shiftedBids = new Set();
|
|
378
|
+
for (const m of pending) {
|
|
379
|
+
const bid = m.batchId || m.id;
|
|
380
|
+
if (!shiftedBids.has(bid)) {
|
|
381
|
+
shiftedBids.add(bid);
|
|
382
|
+
this._shiftExistingImagesForBatch(messages, m.id, s);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
373
385
|
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
386
|
+
// Смещаем pending-оверлеи для новых батчей (от старших к новейшему)
|
|
387
|
+
const pendingBatchMeta = [];
|
|
388
|
+
const seenBids = new Set();
|
|
389
|
+
for (const m of pending) {
|
|
390
|
+
if (m.batchId && !seenBids.has(m.batchId)) {
|
|
391
|
+
seenBids.add(m.batchId);
|
|
392
|
+
pendingBatchMeta.push({
|
|
393
|
+
batchId: m.batchId,
|
|
394
|
+
count: pending.filter((pm) => pm.batchId === m.batchId).length
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const { batchId, count } of pendingBatchMeta) {
|
|
399
|
+
this._shiftPendingOverlaysForNewBatch(batchId, count, s);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const enterDistance = this._computeEnterDistance(messages, pending, s, Math.round(BOARD_IMAGE_WIDTH * s));
|
|
379
403
|
|
|
380
404
|
let newIndex = 0;
|
|
381
405
|
pending.forEach((message) => {
|
|
382
|
-
const slot = this._getImageBatchSlot(messages, message.id, s);
|
|
383
|
-
const left = Math.round(slot.x - wScreen / 2);
|
|
384
|
-
const top = Math.round(slot.y - hScreen / 2);
|
|
385
|
-
|
|
386
406
|
const existing = this._pendingOverlays.get(message.id);
|
|
387
407
|
if (existing) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
408
|
+
// Мировые координаты оверлея зафиксированы в момент создания батча.
|
|
409
|
+
// Пересчитываем только экранную позицию из сохранённых world-координат,
|
|
410
|
+
// чтобы пан холста между рендерами не сдвигал worldX/worldY —
|
|
411
|
+
// иначе размещение реального изображения попадёт в неправильную точку.
|
|
412
|
+
const [wr2, hr2] = parseFormatRatio(this._formatId);
|
|
413
|
+
const wScreen2 = Math.round(BOARD_IMAGE_WIDTH * s);
|
|
414
|
+
const hScreen2 = Math.round(wScreen2 / (wr2 / hr2));
|
|
415
|
+
const screenLayout = {
|
|
416
|
+
left: Math.round(existing.worldX * s + (world?.x || 0) - wScreen2 / 2),
|
|
417
|
+
top: Math.round(existing.worldY * s + (world?.y || 0) - hScreen2 / 2),
|
|
418
|
+
width: wScreen2,
|
|
419
|
+
height: hScreen2,
|
|
420
|
+
worldX: existing.worldX,
|
|
421
|
+
worldY: existing.worldY
|
|
422
|
+
};
|
|
423
|
+
this._applyPendingOverlayScreenLayout(existing.el, screenLayout, { animate: true, enterDistance });
|
|
395
424
|
return;
|
|
396
425
|
}
|
|
397
426
|
|
|
427
|
+
const layout = this._computePendingOverlayScreenLayout(messages, message.id, s);
|
|
428
|
+
|
|
398
429
|
const overlay = document.createElement('div');
|
|
399
430
|
overlay.className = 'moodboard-chat__pending-overlay moodboard-chat__pending-overlay--enter';
|
|
400
|
-
overlay.style.cssText = `left:${left}px;top:${top}px;width:${
|
|
431
|
+
overlay.style.cssText = `left:${layout.left}px;top:${layout.top}px;width:${layout.width}px;height:${layout.height}px`;
|
|
401
432
|
overlay.style.setProperty('--moodboard-chat-board-animation-ms', `${BOARD_IMAGE_REARRANGE_MS}ms`);
|
|
402
433
|
overlay.style.setProperty('--moodboard-chat-pending-enter-x', `${enterDistance}px`);
|
|
403
434
|
|
|
@@ -406,14 +437,19 @@ export class ChatWindow {
|
|
|
406
437
|
label.textContent = 'В процессе...';
|
|
407
438
|
overlay.appendChild(label);
|
|
408
439
|
|
|
409
|
-
document.body.appendChild(overlay);
|
|
440
|
+
(this._container ?? document.body).appendChild(overlay);
|
|
410
441
|
|
|
411
442
|
// Принудительный reflow: фиксируем стартовое состояние (translateX справа + opacity 0)
|
|
412
443
|
// в layout до переключения класса. Без этого браузер может смерджить два состояния
|
|
413
444
|
// в один кадр и transition не запустится — заглушка появится мгновенно.
|
|
414
445
|
void overlay.offsetWidth;
|
|
415
446
|
|
|
416
|
-
this._pendingOverlays.set(message.id, {
|
|
447
|
+
this._pendingOverlays.set(message.id, {
|
|
448
|
+
el: overlay,
|
|
449
|
+
batchId: message.batchId,
|
|
450
|
+
worldX: layout.worldX,
|
|
451
|
+
worldY: layout.worldY
|
|
452
|
+
});
|
|
417
453
|
|
|
418
454
|
const stagger = newIndex * BOARD_IMAGE_PENDING_STAGGER_MS;
|
|
419
455
|
newIndex += 1;
|
|
@@ -444,6 +480,39 @@ export class ChatWindow {
|
|
|
444
480
|
}
|
|
445
481
|
}
|
|
446
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Вычисляет расстояние входа заглушки так, чтобы новая заглушка въезжала справа
|
|
485
|
+
* от уже размещённых AI-изображений на доске, а не накрывала их во время анимации
|
|
486
|
+
* сдвига. Без этого при N>1 изображений в батче enter-расстояние фиксированное
|
|
487
|
+
* (512px) не покрывало ширину существующего ряда (940px для трёх изображений),
|
|
488
|
+
* и заглушка пересекалась с ещё не ушедшими влево картинками.
|
|
489
|
+
*/
|
|
490
|
+
_computeEnterDistance(messages, pending, scale, wScreen) {
|
|
491
|
+
const baseEnter = Math.round(BOARD_IMAGE_STEP * scale * BOARD_IMAGE_PENDING_ENTER_FACTOR);
|
|
492
|
+
|
|
493
|
+
const aiObjects = this._getBoardAiImageObjects();
|
|
494
|
+
if (aiObjects.length === 0) return baseEnter;
|
|
495
|
+
|
|
496
|
+
const firstNewPending = pending.find((m) => !this._pendingOverlays.has(m.id));
|
|
497
|
+
if (!firstNewPending) return baseEnter;
|
|
498
|
+
|
|
499
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
500
|
+
const worldX = world?.x || 0;
|
|
501
|
+
const existingRight_world = Math.max(...aiObjects.map((obj) => obj.position.x + getBoardObjectWidth(obj)));
|
|
502
|
+
const existingRight_screen = Math.round(existingRight_world * scale + worldX);
|
|
503
|
+
|
|
504
|
+
const step = Math.round(BOARD_IMAGE_STEP * scale);
|
|
505
|
+
const gap = Math.round(BOARD_IMAGE_GAP * scale);
|
|
506
|
+
|
|
507
|
+
const slot = this._getImageBatchSlot(messages, firstNewPending.id, scale);
|
|
508
|
+
const batch = findImageGenerationBatch(messages, firstNewPending.id);
|
|
509
|
+
const leftmostSlotX = Math.round(slot.x - batch.index * step);
|
|
510
|
+
const leftmostFinalLeft = leftmostSlotX - Math.round(wScreen / 2);
|
|
511
|
+
|
|
512
|
+
const neededEnter = existingRight_screen - leftmostFinalLeft + gap;
|
|
513
|
+
return Math.max(baseEnter, Math.ceil(neededEnter));
|
|
514
|
+
}
|
|
515
|
+
|
|
447
516
|
_clearPendingOverlays() {
|
|
448
517
|
for (const record of this._pendingOverlays.values()) {
|
|
449
518
|
record.el.remove();
|
|
@@ -453,6 +522,54 @@ export class ChatWindow {
|
|
|
453
522
|
clearTimeout(timer);
|
|
454
523
|
}
|
|
455
524
|
this._pendingOverlayTimers.clear();
|
|
525
|
+
this._pendingBatchOffsets.clear();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
_shiftPendingOverlaysForNewBatch(batchId, count, scale) {
|
|
529
|
+
if (!batchId || this._pendingBatchOffsets.has(batchId)) return;
|
|
530
|
+
|
|
531
|
+
const step = Math.round(BOARD_IMAGE_STEP * scale);
|
|
532
|
+
const shiftAmount = count * step;
|
|
533
|
+
|
|
534
|
+
for (const [existingId, offset] of this._pendingBatchOffsets) {
|
|
535
|
+
this._pendingBatchOffsets.set(existingId, offset - shiftAmount);
|
|
536
|
+
}
|
|
537
|
+
this._pendingBatchOffsets.set(batchId, 0);
|
|
538
|
+
|
|
539
|
+
let existingOverlaysShifted = false;
|
|
540
|
+
const messages = this._session?.getState?.()?.messages || [];
|
|
541
|
+
for (const [messageId, record] of this._pendingOverlays) {
|
|
542
|
+
if (record.batchId === batchId) continue;
|
|
543
|
+
const layout = this._computePendingOverlayScreenLayout(messages, messageId, scale);
|
|
544
|
+
record.worldX = layout.worldX;
|
|
545
|
+
record.worldY = layout.worldY;
|
|
546
|
+
this._applyPendingOverlayScreenLayout(record.el, layout, { animate: true });
|
|
547
|
+
existingOverlaysShifted = true;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Когда существующие заглушки сдвигаются влево, уже размещённые AI-изображения
|
|
551
|
+
// на доске должны сдвинуться на то же расстояние — иначе они окажутся
|
|
552
|
+
// правее заглушек и перекроются ими.
|
|
553
|
+
if (!existingOverlaysShifted) return;
|
|
554
|
+
|
|
555
|
+
const worldShift = shiftAmount / scale;
|
|
556
|
+
for (const obj of this._getBoardAiImageObjects()) {
|
|
557
|
+
if (obj?.position) {
|
|
558
|
+
const from = { x: obj.position.x, y: obj.position.y };
|
|
559
|
+
const to = { x: Math.round(obj.position.x - worldShift), y: obj.position.y };
|
|
560
|
+
this._animateBoardImageToPosition(obj.id, from, to);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
_cleanupPlacedBatchOffsets(messages) {
|
|
566
|
+
if (this._pendingBatchOffsets.size === 0) return;
|
|
567
|
+
for (const batchId of [...this._pendingBatchOffsets.keys()]) {
|
|
568
|
+
const batchMessages = (messages || []).filter((m) => m.batchId === batchId);
|
|
569
|
+
if (batchMessages.length === 0 || batchMessages.every((m) => !m.pending)) {
|
|
570
|
+
this._pendingBatchOffsets.delete(batchId);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
456
573
|
}
|
|
457
574
|
|
|
458
575
|
_getImageRequestOptions() {
|
|
@@ -465,175 +582,195 @@ export class ChatWindow {
|
|
|
465
582
|
};
|
|
466
583
|
}
|
|
467
584
|
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
585
|
+
_computePendingOverlayScreenLayout(messages, messageId, scale = 1) {
|
|
586
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
587
|
+
const s = scale || world?.scale?.x || 1;
|
|
588
|
+
const [wr, hr] = parseFormatRatio(this._formatId);
|
|
589
|
+
const ratio = wr / hr;
|
|
590
|
+
const wScreen = Math.round(BOARD_IMAGE_WIDTH * s);
|
|
591
|
+
const hScreen = Math.round(wScreen / ratio);
|
|
592
|
+
const slot = this._getImageBatchSlot(messages, messageId, s);
|
|
593
|
+
const worldX = (slot.x - (world?.x || 0)) / s;
|
|
594
|
+
const worldY = (slot.y - (world?.y || 0)) / s;
|
|
595
|
+
const screenX = Math.round(worldX * s + (world?.x || 0));
|
|
596
|
+
const screenY = Math.round(worldY * s + (world?.y || 0));
|
|
471
597
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
this._handleReferenceDragStart(data);
|
|
480
|
-
};
|
|
481
|
-
const onDragEnd = (data) => {
|
|
482
|
-
void this._handleReferenceDragEnd(data);
|
|
483
|
-
};
|
|
484
|
-
const onSelectionAdd = (data) => {
|
|
485
|
-
void this._handleSelectionAdd(data);
|
|
598
|
+
return {
|
|
599
|
+
left: Math.round(screenX - wScreen / 2),
|
|
600
|
+
top: Math.round(screenY - hScreen / 2),
|
|
601
|
+
width: wScreen,
|
|
602
|
+
height: hScreen,
|
|
603
|
+
worldX,
|
|
604
|
+
worldY
|
|
486
605
|
};
|
|
487
|
-
|
|
488
|
-
this._referenceDragHandlers = { onCursorMove, onDragStart, onDragEnd, onSelectionAdd };
|
|
489
|
-
eventBus.on(Events.UI.CursorMove, onCursorMove);
|
|
490
|
-
eventBus.on(Events.Tool.DragStart, onDragStart);
|
|
491
|
-
eventBus.on(Events.Tool.DragEnd, onDragEnd);
|
|
492
|
-
eventBus.on(Events.Tool.SelectionAdd, onSelectionAdd);
|
|
493
606
|
}
|
|
494
607
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
608
|
+
_applyPendingOverlayScreenLayout(el, layout, { animate = false, enterDistance } = {}) {
|
|
609
|
+
el.style.left = `${layout.left}px`;
|
|
610
|
+
el.style.top = `${layout.top}px`;
|
|
611
|
+
el.style.width = `${layout.width}px`;
|
|
612
|
+
el.style.height = `${layout.height}px`;
|
|
613
|
+
if (!animate) return;
|
|
499
614
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
this._referenceDragHandlers = null;
|
|
615
|
+
el.style.setProperty('--moodboard-chat-board-animation-ms', `${BOARD_IMAGE_REARRANGE_MS}ms`);
|
|
616
|
+
if (enterDistance != null) {
|
|
617
|
+
el.style.setProperty('--moodboard-chat-pending-enter-x', `${enterDistance}px`);
|
|
618
|
+
}
|
|
505
619
|
}
|
|
506
620
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
|
|
513
|
-
await this._addImageObjectAsReference(object);
|
|
514
|
-
}
|
|
621
|
+
/**
|
|
622
|
+
* Пересчитывает screen-позиции заглушек из world-координат — вместе с AI-изображениями на доске при pan/zoom.
|
|
623
|
+
*/
|
|
624
|
+
_syncPendingOverlaysToViewport({ disableTransition = false, recomputeWorld = false } = {}) {
|
|
625
|
+
if (this._pendingOverlays.size === 0) return;
|
|
515
626
|
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
: null;
|
|
523
|
-
this._updateReferenceDragPreview();
|
|
524
|
-
}
|
|
627
|
+
const messages = this._session?.getState?.()?.messages;
|
|
628
|
+
const world = this._boardCore?.pixi?.worldLayer || this._boardCore?.pixi?.app?.stage;
|
|
629
|
+
const s = world?.scale?.x || 1;
|
|
630
|
+
const [wr, hr] = parseFormatRatio(this._formatId);
|
|
631
|
+
const wScreen = Math.round(BOARD_IMAGE_WIDTH * s);
|
|
632
|
+
const hScreen = Math.round(wScreen / (wr / hr));
|
|
525
633
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (!isDropTarget || !isReferenceImageObject(object)) return null;
|
|
634
|
+
for (const [messageId, record] of this._pendingOverlays) {
|
|
635
|
+
if (recomputeWorld || typeof record.worldX !== 'number' || typeof record.worldY !== 'number') {
|
|
636
|
+
const layout = this._computePendingOverlayScreenLayout(messages, messageId, s);
|
|
637
|
+
record.worldX = layout.worldX;
|
|
638
|
+
record.worldY = layout.worldY;
|
|
639
|
+
}
|
|
533
640
|
|
|
534
|
-
|
|
535
|
-
|
|
641
|
+
const screenX = Math.round(record.worldX * s + (world?.x || 0));
|
|
642
|
+
const screenY = Math.round(record.worldY * s + (world?.y || 0));
|
|
643
|
+
const el = record.el;
|
|
644
|
+
if (disableTransition) {
|
|
645
|
+
el.style.transition = 'none';
|
|
646
|
+
}
|
|
647
|
+
el.style.left = `${Math.round(screenX - wScreen / 2)}px`;
|
|
648
|
+
el.style.top = `${Math.round(screenY - hScreen / 2)}px`;
|
|
649
|
+
el.style.width = `${wScreen}px`;
|
|
650
|
+
el.style.height = `${hScreen}px`;
|
|
651
|
+
if (disableTransition) {
|
|
652
|
+
void el.offsetWidth;
|
|
653
|
+
el.style.removeProperty('transition');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
536
656
|
}
|
|
537
657
|
|
|
538
|
-
|
|
539
|
-
|
|
658
|
+
_attachViewportSync() {
|
|
659
|
+
const eventBus = this._boardCore?.eventBus;
|
|
660
|
+
if (!eventBus || typeof eventBus.on !== 'function' || this._viewportHandlers) return;
|
|
540
661
|
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
662
|
+
const onPanUpdate = () => {
|
|
663
|
+
this._syncPendingOverlaysToViewport({ disableTransition: true });
|
|
664
|
+
};
|
|
665
|
+
const onViewportChange = () => {
|
|
666
|
+
this._syncPendingOverlaysToViewport({ disableTransition: true, recomputeWorld: false });
|
|
667
|
+
};
|
|
546
668
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
669
|
+
this._viewportHandlers = { onPanUpdate, onViewportChange };
|
|
670
|
+
eventBus.on(Events.Tool.PanUpdate, onPanUpdate);
|
|
671
|
+
eventBus.on(Events.UI.ZoomPercent, onViewportChange);
|
|
672
|
+
eventBus.on(Events.Viewport.Changed, onViewportChange);
|
|
551
673
|
}
|
|
552
674
|
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const src = getImageObjectSource(object);
|
|
561
|
-
if (!src) {
|
|
562
|
-
this._hideReferenceDragPreview();
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
675
|
+
_detachViewportSync() {
|
|
676
|
+
const eventBus = this._boardCore?.eventBus;
|
|
677
|
+
const handlers = this._viewportHandlers;
|
|
678
|
+
if (!eventBus || typeof eventBus.off !== 'function' || !handlers) return;
|
|
565
679
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
this._refs?.textarea?.closest?.('.moodboard-chat__input-row')?.classList.add('is-reference-drop-target');
|
|
680
|
+
eventBus.off(Events.Tool.PanUpdate, handlers.onPanUpdate);
|
|
681
|
+
eventBus.off(Events.UI.ZoomPercent, handlers.onViewportChange);
|
|
682
|
+
eventBus.off(Events.Viewport.Changed, handlers.onViewportChange);
|
|
683
|
+
this._viewportHandlers = null;
|
|
571
684
|
}
|
|
572
685
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
preview.className = 'moodboard-chat__reference-drag-preview';
|
|
577
|
-
preview.alt = getImageObjectFileName(object, src);
|
|
578
|
-
preview.width = REFERENCE_DRAG_PREVIEW_SIZE;
|
|
579
|
-
preview.height = REFERENCE_DRAG_PREVIEW_SIZE;
|
|
580
|
-
document.body.appendChild(preview);
|
|
581
|
-
this._referenceDragPreview = preview;
|
|
582
|
-
}
|
|
686
|
+
_attachSelectionEvents() {
|
|
687
|
+
const eventBus = this._boardCore?.eventBus;
|
|
688
|
+
if (!eventBus || typeof eventBus.on !== 'function' || this._selectionHandlers) return;
|
|
583
689
|
|
|
584
|
-
|
|
585
|
-
this.
|
|
586
|
-
}
|
|
587
|
-
|
|
690
|
+
const onSelectionAdd = (data) => {
|
|
691
|
+
void this._handleSelectionAdd(data);
|
|
692
|
+
};
|
|
693
|
+
const onSelectionRemove = (data) => {
|
|
694
|
+
const objectId = data?.object;
|
|
695
|
+
if (objectId) this._composer?.removeAttachmentForObject?.(objectId);
|
|
696
|
+
};
|
|
697
|
+
const onSelectionClear = () => {
|
|
698
|
+
this._composer?.removeAllBoardAttachments?.();
|
|
699
|
+
};
|
|
700
|
+
const onBoxSelectStart = () => {
|
|
701
|
+
this._boxSelectActive = true;
|
|
702
|
+
};
|
|
703
|
+
const onBoxSelectCommit = (data) => {
|
|
704
|
+
void this._handleBoxSelectCommit(data);
|
|
705
|
+
};
|
|
588
706
|
|
|
589
|
-
|
|
707
|
+
this._selectionHandlers = {
|
|
708
|
+
onSelectionAdd,
|
|
709
|
+
onSelectionRemove,
|
|
710
|
+
onSelectionClear,
|
|
711
|
+
onBoxSelectStart,
|
|
712
|
+
onBoxSelectCommit
|
|
713
|
+
};
|
|
714
|
+
eventBus.on(Events.Tool.SelectionAdd, onSelectionAdd);
|
|
715
|
+
eventBus.on(Events.Tool.SelectionRemove, onSelectionRemove);
|
|
716
|
+
eventBus.on(Events.Tool.SelectionClear, onSelectionClear);
|
|
717
|
+
eventBus.on(Events.Tool.BoxSelectStart, onBoxSelectStart);
|
|
718
|
+
eventBus.on(Events.Tool.BoxSelectCommit, onBoxSelectCommit);
|
|
590
719
|
}
|
|
591
720
|
|
|
592
|
-
|
|
593
|
-
this.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
721
|
+
_detachSelectionEvents() {
|
|
722
|
+
const eventBus = this._boardCore?.eventBus;
|
|
723
|
+
const handlers = this._selectionHandlers;
|
|
724
|
+
if (!eventBus || typeof eventBus.off !== 'function' || !handlers) return;
|
|
597
725
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
726
|
+
eventBus.off(Events.Tool.SelectionAdd, handlers.onSelectionAdd);
|
|
727
|
+
eventBus.off(Events.Tool.SelectionRemove, handlers.onSelectionRemove);
|
|
728
|
+
eventBus.off(Events.Tool.SelectionClear, handlers.onSelectionClear);
|
|
729
|
+
eventBus.off(Events.Tool.BoxSelectStart, handlers.onBoxSelectStart);
|
|
730
|
+
eventBus.off(Events.Tool.BoxSelectCommit, handlers.onBoxSelectCommit);
|
|
731
|
+
this._selectionHandlers = null;
|
|
732
|
+
this._boxSelectActive = false;
|
|
603
733
|
}
|
|
604
734
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
735
|
+
async _handleSelectionAdd(data = {}) {
|
|
736
|
+
// Во время box-select каждый mousemove перевыставляет selection и снова
|
|
737
|
+
// эмитит SelectionAdd для тех же id. Финальный набор reference-картинок
|
|
738
|
+
// мы получим из BoxSelectCommit по strict-contains, поэтому здесь молчим.
|
|
739
|
+
if (this._boxSelectActive) return;
|
|
609
740
|
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
|
|
741
|
+
const objectId = data?.object;
|
|
742
|
+
const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
|
|
743
|
+
|
|
744
|
+
if (!isReferenceImageObject(object)) return;
|
|
613
745
|
|
|
614
|
-
|
|
615
|
-
&& clientX <= rect.right
|
|
616
|
-
&& clientY >= rect.top
|
|
617
|
-
&& clientY <= rect.bottom;
|
|
746
|
+
await this._addImageObjectAsReference(object);
|
|
618
747
|
}
|
|
619
748
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
749
|
+
async _handleBoxSelectCommit(data = {}) {
|
|
750
|
+
this._boxSelectActive = false;
|
|
751
|
+
const ids = Array.isArray(data?.selected) ? data.selected : (Array.isArray(data?.objects) ? data.objects : []);
|
|
752
|
+
if (!ids.length || !this._composer) return;
|
|
623
753
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
754
|
+
const all = this._boardCore?.state?.state?.objects ?? [];
|
|
755
|
+
const byId = new Map(all.map((item) => [item?.id, item]));
|
|
756
|
+
const seen = new Set();
|
|
757
|
+
for (const id of ids) {
|
|
758
|
+
if (!id || seen.has(id)) continue;
|
|
759
|
+
seen.add(id);
|
|
760
|
+
const obj = byId.get(id);
|
|
761
|
+
if (!obj || !isReferenceImageObject(obj)) continue;
|
|
762
|
+
await this._addImageObjectAsReference(obj);
|
|
763
|
+
}
|
|
628
764
|
}
|
|
629
765
|
|
|
630
766
|
async _addImageObjectAsReference(object) {
|
|
631
767
|
if (!object || !this._composer) return;
|
|
768
|
+
if (this._composer.hasAttachmentForObject?.(object.id)) return;
|
|
632
769
|
|
|
633
770
|
try {
|
|
634
771
|
const file = await createFileFromImageObject(object);
|
|
635
772
|
if (!file) return;
|
|
636
|
-
this._composer.addAttachment(file);
|
|
773
|
+
this._composer.addAttachment(file, { sourceObjectId: object.id });
|
|
637
774
|
this._composer.focus();
|
|
638
775
|
} catch (err) {
|
|
639
776
|
console.warn('[ChatWindow] cannot add selected image reference:', err);
|
|
@@ -646,7 +783,8 @@ export class ChatWindow {
|
|
|
646
783
|
const step = Math.round(BOARD_IMAGE_STEP * scale);
|
|
647
784
|
const count = Math.max(batch.count, 1);
|
|
648
785
|
const index = Math.min(Math.max(batch.index, 0), count - 1);
|
|
649
|
-
const
|
|
786
|
+
const batchOffset = batch.batchId ? (this._pendingBatchOffsets.get(batch.batchId) ?? 0) : 0;
|
|
787
|
+
const leftmostCenter = anchor.x - ((count - 1) * step) / 2 + batchOffset;
|
|
650
788
|
|
|
651
789
|
return {
|
|
652
790
|
x: Math.round(leftmostCenter + index * step),
|
|
@@ -845,11 +983,24 @@ export class ChatWindow {
|
|
|
845
983
|
const s = world?.scale?.x || 1;
|
|
846
984
|
const messages = this._session.getState().messages;
|
|
847
985
|
this._shiftExistingImagesForBatch(messages, msg.id, s);
|
|
848
|
-
|
|
849
|
-
|
|
986
|
+
|
|
987
|
+
// Pending-оверлей хранит worldX/worldY, зафиксированные в момент начала генерации.
|
|
988
|
+
// Используем их чтобы разместить изображение в той же мировой точке,
|
|
989
|
+
// независимо от того, сдвинул ли пользователь холст пока шла генерация.
|
|
990
|
+
const pendingRecord = this._pendingOverlays.get(msg.id);
|
|
991
|
+
let x, y;
|
|
992
|
+
if (pendingRecord && typeof pendingRecord.worldX === 'number' && typeof pendingRecord.worldY === 'number') {
|
|
993
|
+
x = Math.round(pendingRecord.worldX * s + (world?.x || 0));
|
|
994
|
+
y = Math.round(pendingRecord.worldY * s + (world?.y || 0));
|
|
995
|
+
} else {
|
|
996
|
+
const slot = this._getImageBatchSlot(messages, msg.id, s);
|
|
997
|
+
x = slot.x;
|
|
998
|
+
y = slot.y;
|
|
999
|
+
}
|
|
1000
|
+
|
|
850
1001
|
this._boardCore.eventBus.emit(Events.UI.PasteImageAt, {
|
|
851
|
-
x
|
|
852
|
-
y
|
|
1002
|
+
x,
|
|
1003
|
+
y,
|
|
853
1004
|
src: dataUrl,
|
|
854
1005
|
name: 'ai-generated.jpg',
|
|
855
1006
|
skipUpload: true
|
|
@@ -911,13 +1062,25 @@ function findImageGenerationBatch(messages, messageId) {
|
|
|
911
1062
|
return { index: 0, count: 1 };
|
|
912
1063
|
}
|
|
913
1064
|
|
|
1065
|
+
const target = list[targetIndex];
|
|
1066
|
+
if (target?.batchId) {
|
|
1067
|
+
const batchMessages = list.filter((m) => m.batchId === target.batchId);
|
|
1068
|
+
const index = batchMessages.findIndex((m) => m.id === messageId);
|
|
1069
|
+
return {
|
|
1070
|
+
index: Math.max(index, 0),
|
|
1071
|
+
count: batchMessages.length,
|
|
1072
|
+
ids: batchMessages.map((m) => m.id),
|
|
1073
|
+
batchId: target.batchId
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
914
1077
|
let start = targetIndex;
|
|
915
|
-
while (start > 0 && isImageGenerationMessage(list[start - 1])) {
|
|
1078
|
+
while (start > 0 && isImageGenerationMessage(list[start - 1]) && !list[start - 1].batchId) {
|
|
916
1079
|
start--;
|
|
917
1080
|
}
|
|
918
1081
|
|
|
919
1082
|
let end = targetIndex;
|
|
920
|
-
while (end + 1 < list.length && isImageGenerationMessage(list[end + 1])) {
|
|
1083
|
+
while (end + 1 < list.length && isImageGenerationMessage(list[end + 1]) && !list[end + 1].batchId) {
|
|
921
1084
|
end++;
|
|
922
1085
|
}
|
|
923
1086
|
|