@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.
@@ -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') || '4') : 4;
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 : 4;
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: { width: Math.round(w), height: Math.round(h), title: 'Произвольный', lockedAspect: false, isArbitrary: true }
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
- // Ширина редактора = ширина titleBg в экранных пикселях (с учётом зум-компенсации)
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
- inputWidth = Math.max(100, Math.round(bgRight.x - screenPos.x));
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 wrapper = createFrameTitleEditorWrapper();
61
- const input = createFrameTitleEditorInput(currentTitle);
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
- background: white;
40
- border: 2px solid #2563eb;
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: 3px 6px;
43
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
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
- padding: 1px 2px;
64
- color: #1f2937;
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 mid = getConnectorMidpointScreen(this.core, this.currentId);
66
- if (!mid) return;
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 = 18;
71
+ const GAP = 20;
71
72
 
72
- let px = mid.x - Math.round(panelW / 2);
73
- let py = mid.y - panelH - GAP;
73
+ let px = top.x - Math.round(panelW / 2);
74
+ let py = top.topY - panelH - GAP;
74
75
 
75
- // Если уходит вверх за контейнер — переносим ниже середины
76
- if (py < 0) py = mid.y + GAP;
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;