@sequent-org/moodboard 1.4.51 → 1.4.53
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/flows/ObjectLifecycleFlow.js +4 -2
- package/src/core/index.js +3 -3
- package/src/initNoBundler.js +1 -1
- package/src/moodboard/integration/MoodBoardLoadApi.js +1 -1
- package/src/objects/FrameObject.js +171 -32
- package/src/services/FrameService.js +55 -9
- package/src/services/ImageAttachmentService.js +260 -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/ui/ConnectorPropertiesPanel.js +11 -7
- package/src/ui/FramePropertiesPanel.js +150 -25
- package/src/ui/ImagePropertiesPanel.js +8 -0
- package/src/ui/Topbar.js +24 -10
- package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +39 -0
- package/src/ui/styles/panels.css +43 -3
- package/src/ui/styles/workspace.css +2 -2
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
const IMAGE_TYPES = new Set(['image', 'revit-screenshot-img', 'model3d-screenshot-img']);
|
|
4
|
+
|
|
5
|
+
function isImageType(type) { return IMAGE_TYPES.has(type); }
|
|
6
|
+
function isDrawingType(type) { return type === 'drawing'; }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Проверяет, что прямоугольник inner целиком вмещается в outer.
|
|
10
|
+
* Координаты — мировые (top-left + size).
|
|
11
|
+
*/
|
|
12
|
+
function isFullyContained(inner, outer) {
|
|
13
|
+
return inner.x >= outer.x &&
|
|
14
|
+
inner.y >= outer.y &&
|
|
15
|
+
(inner.x + inner.w) <= (outer.x + outer.w) &&
|
|
16
|
+
(inner.y + inner.h) <= (outer.y + outer.h);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getObjRect(obj) {
|
|
20
|
+
return {
|
|
21
|
+
x: obj.position?.x ?? 0,
|
|
22
|
+
y: obj.position?.y ?? 0,
|
|
23
|
+
w: obj.width ?? 0,
|
|
24
|
+
h: obj.height ?? 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Управляет привязкой drawing-объектов к изображениям.
|
|
30
|
+
*
|
|
31
|
+
* Правила:
|
|
32
|
+
* - Привязка происходит только если drawing целиком внутри изображения.
|
|
33
|
+
* - Фрейм первичен: если у drawing уже есть properties.frameId — imageId не ставим.
|
|
34
|
+
* - Привязанный drawing перемещается вместе с изображением при drag.
|
|
35
|
+
* - При resize изображения пересчитываем, какие drawing остаются внутри.
|
|
36
|
+
*/
|
|
37
|
+
export class ImageAttachmentService {
|
|
38
|
+
constructor(eventBus, pixi, state) {
|
|
39
|
+
this.eventBus = eventBus;
|
|
40
|
+
this.pixi = pixi;
|
|
41
|
+
this.state = state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Вспомогательные ──────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Находит id изображения, полностью вмещающего drawing.
|
|
48
|
+
* Если несколько — берём с наибольшим zIndex PIXI (верхнее).
|
|
49
|
+
* Если у drawing есть frameId — возвращает null (фрейм первичен).
|
|
50
|
+
*/
|
|
51
|
+
_findHostImage(drawingObj) {
|
|
52
|
+
if (drawingObj.properties?.frameId) return null;
|
|
53
|
+
|
|
54
|
+
const dr = getObjRect(drawingObj);
|
|
55
|
+
const images = (this.state.state.objects || []).filter(o => isImageType(o.type));
|
|
56
|
+
const candidates = images.filter(img => isFullyContained(dr, getObjRect(img)));
|
|
57
|
+
if (!candidates.length) return null;
|
|
58
|
+
|
|
59
|
+
candidates.sort((a, b) => {
|
|
60
|
+
const pa = this.pixi.objects.get(a.id);
|
|
61
|
+
const pb = this.pixi.objects.get(b.id);
|
|
62
|
+
return (pb?.zIndex ?? 0) - (pa?.zIndex ?? 0);
|
|
63
|
+
});
|
|
64
|
+
return candidates[0].id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_getImageChildren(imageId) {
|
|
68
|
+
return (this.state.state.objects || [])
|
|
69
|
+
.filter(o => o.properties?.imageId === imageId)
|
|
70
|
+
.map(o => o.id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Логика пересчёта ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/** Пересчитывает imageId для одного drawing по текущим bounds. */
|
|
76
|
+
_recomputeImageAttachment(objectId) {
|
|
77
|
+
const obj = (this.state.state.objects || []).find(o => o.id === objectId);
|
|
78
|
+
if (!obj || !isDrawingType(obj.type)) return;
|
|
79
|
+
|
|
80
|
+
const newImageId = this._findHostImage(obj);
|
|
81
|
+
const prevImageId = obj.properties?.imageId ?? null;
|
|
82
|
+
if (newImageId === prevImageId) return;
|
|
83
|
+
|
|
84
|
+
obj.properties = obj.properties || {};
|
|
85
|
+
if (newImageId) {
|
|
86
|
+
obj.properties.imageId = newImageId;
|
|
87
|
+
} else {
|
|
88
|
+
delete obj.properties.imageId;
|
|
89
|
+
}
|
|
90
|
+
this.state.markDirty();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** При создании изображения прикрепляет все drawing, оказавшиеся внутри. */
|
|
94
|
+
_attachDrawingsToNewImage(imageId) {
|
|
95
|
+
const imageObj = (this.state.state.objects || []).find(o => o.id === imageId);
|
|
96
|
+
if (!imageObj) return;
|
|
97
|
+
const ir = getObjRect(imageObj);
|
|
98
|
+
let changed = false;
|
|
99
|
+
|
|
100
|
+
for (const obj of this.state.state.objects || []) {
|
|
101
|
+
if (!isDrawingType(obj.type)) continue;
|
|
102
|
+
if (obj.properties?.frameId) continue;
|
|
103
|
+
if (obj.properties?.imageId) continue;
|
|
104
|
+
if (isFullyContained(getObjRect(obj), ir)) {
|
|
105
|
+
obj.properties = obj.properties || {};
|
|
106
|
+
obj.properties.imageId = imageId;
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (changed) this.state.markDirty();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Пересчитывает все drawing относительно конкретного изображения.
|
|
115
|
+
* Вызывается после resize или drag изображения.
|
|
116
|
+
*/
|
|
117
|
+
_recomputeAllForImage(imageId) {
|
|
118
|
+
const imageObj = (this.state.state.objects || []).find(o => o.id === imageId);
|
|
119
|
+
if (!imageObj) return;
|
|
120
|
+
const ir = getObjRect(imageObj);
|
|
121
|
+
let changed = false;
|
|
122
|
+
|
|
123
|
+
for (const obj of this.state.state.objects || []) {
|
|
124
|
+
if (!isDrawingType(obj.type)) continue;
|
|
125
|
+
if (obj.properties?.frameId) continue;
|
|
126
|
+
|
|
127
|
+
const prevImageId = obj.properties?.imageId ?? null;
|
|
128
|
+
const inside = isFullyContained(getObjRect(obj), ir);
|
|
129
|
+
const attached = prevImageId === imageId;
|
|
130
|
+
|
|
131
|
+
if (inside && !attached && !prevImageId) {
|
|
132
|
+
obj.properties = obj.properties || {};
|
|
133
|
+
obj.properties.imageId = imageId;
|
|
134
|
+
changed = true;
|
|
135
|
+
} else if (!inside && attached) {
|
|
136
|
+
delete obj.properties.imageId;
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (changed) this.state.markDirty();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
attach() {
|
|
146
|
+
if (this._attached) return;
|
|
147
|
+
this._attached = true;
|
|
148
|
+
|
|
149
|
+
// Создан объект: изображение → прикрепляем вложенные drawing;
|
|
150
|
+
// drawing → проверяем попадание внутрь изображения.
|
|
151
|
+
this._onObjectCreated = ({ objectId, objectData }) => {
|
|
152
|
+
try {
|
|
153
|
+
if (!objectData) return;
|
|
154
|
+
if (isImageType(objectData.type)) {
|
|
155
|
+
this._attachDrawingsToNewImage(objectId);
|
|
156
|
+
} else if (isDrawingType(objectData.type)) {
|
|
157
|
+
this._recomputeImageAttachment(objectId);
|
|
158
|
+
}
|
|
159
|
+
} catch (_) { /* no-op */ }
|
|
160
|
+
};
|
|
161
|
+
this.eventBus.on(Events.Object.Created, this._onObjectCreated);
|
|
162
|
+
|
|
163
|
+
// DragStart изображения — сохраняем начальные позиции (PIXI-центры) детей.
|
|
164
|
+
this._onDragStart = (data) => {
|
|
165
|
+
const moved = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
166
|
+
if (!moved || !isImageType(moved.type)) return;
|
|
167
|
+
|
|
168
|
+
const p = this.pixi.objects.get(moved.id);
|
|
169
|
+
this._imgDragImgStart = { x: p?.x ?? 0, y: p?.y ?? 0 };
|
|
170
|
+
this._imgDragChildStart = new Map();
|
|
171
|
+
for (const childId of this._getImageChildren(moved.id)) {
|
|
172
|
+
const cp = this.pixi.objects.get(childId);
|
|
173
|
+
if (cp) this._imgDragChildStart.set(childId, { x: cp.x, y: cp.y });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
this.eventBus.on(Events.Tool.DragStart, this._onDragStart);
|
|
177
|
+
|
|
178
|
+
// DragUpdate изображения — тащим привязанные drawing.
|
|
179
|
+
this._onDragUpdate = (data) => {
|
|
180
|
+
const moved = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
181
|
+
if (!moved || !isImageType(moved.type)) return;
|
|
182
|
+
|
|
183
|
+
const p = this.pixi.objects.get(moved.id);
|
|
184
|
+
const start = this._imgDragImgStart ?? { x: p?.x ?? 0, y: p?.y ?? 0 };
|
|
185
|
+
const dx = (p?.x ?? 0) - start.x;
|
|
186
|
+
const dy = (p?.y ?? 0) - start.y;
|
|
187
|
+
|
|
188
|
+
for (const childId of this._getImageChildren(moved.id)) {
|
|
189
|
+
let startPos = this._imgDragChildStart?.get(childId);
|
|
190
|
+
const cp = this.pixi.objects.get(childId);
|
|
191
|
+
if (!startPos) {
|
|
192
|
+
if (cp) {
|
|
193
|
+
startPos = { x: cp.x - dx, y: cp.y - dy };
|
|
194
|
+
this._imgDragChildStart ??= new Map();
|
|
195
|
+
this._imgDragChildStart.set(childId, startPos);
|
|
196
|
+
} else {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const nx = startPos.x + dx;
|
|
201
|
+
const ny = startPos.y + dy;
|
|
202
|
+
if (cp) { cp.x = nx; cp.y = ny; }
|
|
203
|
+
const stObj = (this.state.state.objects || []).find(o => o.id === childId);
|
|
204
|
+
if (stObj) {
|
|
205
|
+
const hw = cp ? (cp.width ?? 0) / 2 : 0;
|
|
206
|
+
const hh = cp ? (cp.height ?? 0) / 2 : 0;
|
|
207
|
+
stObj.position.x = nx - hw;
|
|
208
|
+
stObj.position.y = ny - hh;
|
|
209
|
+
this.eventBus.emit(Events.Object.TransformUpdated, {
|
|
210
|
+
objectId: childId,
|
|
211
|
+
type: 'position',
|
|
212
|
+
position: { x: stObj.position.x, y: stObj.position.y },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
this.eventBus.on(Events.Tool.DragUpdate, this._onDragUpdate);
|
|
218
|
+
|
|
219
|
+
// DragEnd: для drawing — пересчитываем imageId;
|
|
220
|
+
// для изображения — финализируем drag-state и пересчитываем вложенных.
|
|
221
|
+
this._onDragEnd = (data) => {
|
|
222
|
+
const movedObj = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
223
|
+
if (!movedObj) return;
|
|
224
|
+
|
|
225
|
+
if (isDrawingType(movedObj.type)) {
|
|
226
|
+
this._recomputeImageAttachment(movedObj.id);
|
|
227
|
+
} else if (isImageType(movedObj.type)) {
|
|
228
|
+
this._imgDragImgStart = null;
|
|
229
|
+
this._imgDragChildStart = null;
|
|
230
|
+
this._recomputeAllForImage(movedObj.id);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
this.eventBus.on(Events.Tool.DragEnd, this._onDragEnd);
|
|
234
|
+
|
|
235
|
+
// ResizeEnd изображения — пересчитываем, какие drawing остаются внутри.
|
|
236
|
+
this._onResizeEnd = (data) => {
|
|
237
|
+
const obj = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
238
|
+
if (!obj || !isImageType(obj.type)) return;
|
|
239
|
+
this._recomputeAllForImage(obj.id);
|
|
240
|
+
};
|
|
241
|
+
this.eventBus.on(Events.Tool.ResizeEnd, this._onResizeEnd);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
detach() {
|
|
245
|
+
if (!this._attached) return;
|
|
246
|
+
this._attached = false;
|
|
247
|
+
if (this._onObjectCreated) this.eventBus.off(Events.Object.Created, this._onObjectCreated);
|
|
248
|
+
if (this._onDragStart) this.eventBus.off(Events.Tool.DragStart, this._onDragStart);
|
|
249
|
+
if (this._onDragUpdate) this.eventBus.off(Events.Tool.DragUpdate, this._onDragUpdate);
|
|
250
|
+
if (this._onDragEnd) this.eventBus.off(Events.Tool.DragEnd, this._onDragEnd);
|
|
251
|
+
if (this._onResizeEnd) this.eventBus.off(Events.Tool.ResizeEnd, this._onResizeEnd);
|
|
252
|
+
this._onObjectCreated = null;
|
|
253
|
+
this._onDragStart = null;
|
|
254
|
+
this._onDragUpdate = null;
|
|
255
|
+
this._onDragEnd = null;
|
|
256
|
+
this._onResizeEnd = null;
|
|
257
|
+
this._imgDragImgStart = null;
|
|
258
|
+
this._imgDragChildStart = null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -378,10 +378,10 @@ export class GhostController {
|
|
|
378
378
|
const title = host.pending.properties?.title || 'Новый';
|
|
379
379
|
|
|
380
380
|
const rootStyles = (typeof window !== 'undefined') ? getComputedStyle(document.documentElement) : null;
|
|
381
|
-
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '
|
|
381
|
+
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '5') : 5;
|
|
382
382
|
const cssCornerRadius = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-corner-radius') || '6') : 6;
|
|
383
383
|
const cssBorderColor = rootStyles ? rootStyles.getPropertyValue('--frame-border-color').trim() : '';
|
|
384
|
-
const borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth :
|
|
384
|
+
const borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth : 5;
|
|
385
385
|
const cornerRadius = Number.isFinite(cssCornerRadius) ? cssCornerRadius : 6;
|
|
386
386
|
let strokeColor;
|
|
387
387
|
if (cssBorderColor && cssBorderColor.startsWith('#')) {
|
|
@@ -43,7 +43,15 @@ export class PlacementPayloadFactory {
|
|
|
43
43
|
type: 'frame',
|
|
44
44
|
id: 'frame',
|
|
45
45
|
position: { x, y },
|
|
46
|
-
properties: {
|
|
46
|
+
properties: {
|
|
47
|
+
width: Math.round(w),
|
|
48
|
+
height: Math.round(h),
|
|
49
|
+
title: 'Произвольный',
|
|
50
|
+
lockedAspect: false,
|
|
51
|
+
isArbitrary: true,
|
|
52
|
+
bgMode: 'solid',
|
|
53
|
+
backgroundColor: 0xFFFFFF,
|
|
54
|
+
}
|
|
47
55
|
});
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -50,16 +50,25 @@ export function openFrameTitleEditor(object, _create = false) {
|
|
|
50
50
|
? toScreenWithContainerOffset(titleLayer, view, 0, 0)
|
|
51
51
|
: { x: 0, y: 0 };
|
|
52
52
|
|
|
53
|
-
//
|
|
53
|
+
// Габариты редактора = titleBg в экранных пикселях (с учётом зум-компенсации)
|
|
54
54
|
let inputWidth = 150;
|
|
55
|
+
let inputHeight = 22;
|
|
55
56
|
if (titleLayer && frameInstance.titleBg) {
|
|
56
57
|
const bgRight = toScreenWithContainerOffset(titleLayer, view, frameInstance.titleBg.width || 150, 0);
|
|
57
|
-
|
|
58
|
+
const bgBottom = toScreenWithContainerOffset(titleLayer, view, 0, frameInstance.titleBg.height || 22);
|
|
59
|
+
inputWidth = Math.max(1, Math.round(bgRight.x - screenPos.x));
|
|
60
|
+
inputHeight = Math.max(1, Math.round(bgBottom.y - screenPos.y));
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
const
|
|
61
|
-
|
|
63
|
+
const titleColors = typeof frameInstance.getTitleColors === 'function'
|
|
64
|
+
? frameInstance.getTitleColors()
|
|
65
|
+
: { bgCss: '#ffffff', textCss: '#333333' };
|
|
66
|
+
|
|
67
|
+
const wrapper = createFrameTitleEditorWrapper(titleColors.bgCss);
|
|
68
|
+
wrapper.id = `mb-frame-title-editor-${objectId}`;
|
|
69
|
+
const input = createFrameTitleEditorInput(currentTitle, titleColors.textCss);
|
|
62
70
|
wrapper.style.width = `${inputWidth}px`;
|
|
71
|
+
wrapper.style.height = `${inputHeight}px`;
|
|
63
72
|
|
|
64
73
|
wrapper.appendChild(input);
|
|
65
74
|
view.parentElement.appendChild(wrapper);
|
|
@@ -30,38 +30,43 @@ export function createFileNameEditorWrapper() {
|
|
|
30
30
|
return wrapper;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function createFrameTitleEditorWrapper() {
|
|
33
|
+
export function createFrameTitleEditorWrapper(bgColor = '#ffffff') {
|
|
34
34
|
const wrapper = document.createElement('div');
|
|
35
35
|
wrapper.className = 'moodboard-frame-title-editor';
|
|
36
36
|
wrapper.style.cssText = `
|
|
37
37
|
position: absolute;
|
|
38
38
|
z-index: 1000;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
background: ${bgColor};
|
|
43
|
+
border: none;
|
|
41
44
|
border-radius: 6px;
|
|
42
|
-
padding:
|
|
43
|
-
|
|
44
|
-
min-width: 100px;
|
|
45
|
-
max-width: 320px;
|
|
45
|
+
padding: 4px 8px;
|
|
46
|
+
overflow: hidden;
|
|
46
47
|
font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
|
|
47
48
|
`;
|
|
48
49
|
return wrapper;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export function createFrameTitleEditorInput(title) {
|
|
52
|
+
export function createFrameTitleEditorInput(title, textColor = '#333333') {
|
|
52
53
|
const input = document.createElement('input');
|
|
53
54
|
input.type = 'text';
|
|
54
55
|
input.value = title;
|
|
55
56
|
input.style.cssText = `
|
|
57
|
+
box-sizing: border-box;
|
|
56
58
|
border: none;
|
|
57
59
|
outline: none;
|
|
58
60
|
background: transparent;
|
|
59
61
|
font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
|
|
60
62
|
font-size: 14px;
|
|
61
63
|
font-weight: 500;
|
|
64
|
+
line-height: 14px;
|
|
65
|
+
height: 14px;
|
|
62
66
|
width: 100%;
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
margin: 0;
|
|
68
|
+
padding: 0;
|
|
69
|
+
color: ${textColor};
|
|
65
70
|
`;
|
|
66
71
|
return input;
|
|
67
72
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getSelectedConnectorId,
|
|
11
11
|
getConnectorData,
|
|
12
12
|
getConnectorMidpointScreen,
|
|
13
|
+
getConnectorTopScreen,
|
|
13
14
|
getConnectorStyle,
|
|
14
15
|
buildStyleUpdate,
|
|
15
16
|
} from './connector-properties/ConnectorPropertiesPanelMapper.js';
|
|
@@ -62,18 +63,21 @@ export class ConnectorPropertiesPanel {
|
|
|
62
63
|
const stillSelected = getSelectedConnectorId(this.core) === this.currentId;
|
|
63
64
|
if (!stillSelected) { this.hide(); return; }
|
|
64
65
|
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
66
|
+
const top = getConnectorTopScreen(this.core, this.currentId);
|
|
67
|
+
if (!top) return;
|
|
67
68
|
|
|
68
69
|
const panelW = this.panel.offsetWidth || 480;
|
|
69
70
|
const panelH = this.panel.offsetHeight || 40;
|
|
70
|
-
const GAP =
|
|
71
|
+
const GAP = 20;
|
|
71
72
|
|
|
72
|
-
let px =
|
|
73
|
-
let py =
|
|
73
|
+
let px = top.x - Math.round(panelW / 2);
|
|
74
|
+
let py = top.topY - panelH - GAP;
|
|
74
75
|
|
|
75
|
-
// Если уходит вверх за контейнер — переносим ниже
|
|
76
|
-
if (py < 0)
|
|
76
|
+
// Если уходит вверх за контейнер — переносим ниже наивысшей точки
|
|
77
|
+
if (py < 0) {
|
|
78
|
+
const mid = getConnectorMidpointScreen(this.core, this.currentId);
|
|
79
|
+
py = (mid?.y ?? top.topY) + GAP;
|
|
80
|
+
}
|
|
77
81
|
|
|
78
82
|
// Clamp по ширине контейнера
|
|
79
83
|
const cw = this.container.offsetWidth || window.innerWidth;
|