@sequent-org/moodboard 1.4.49 → 1.4.52

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,242 @@
1
+ import { Events } from '../core/events/Events.js';
2
+ import { ApplyCropCommand } from '../core/commands/ApplyCropCommand.js';
3
+ import { CropOverlay } from '../ui/CropOverlay.js';
4
+
5
+ const ASPECT_RATIOS = {
6
+ custom: null,
7
+ original: null,
8
+ square: 1,
9
+ circle: 1,
10
+ portrait: 3 / 4,
11
+ landscape: 4 / 3,
12
+ wide: 16 / 9,
13
+ };
14
+
15
+ /**
16
+ * Оркестратор режима кадрирования изображения.
17
+ * Управляет жизненным циклом CropOverlay и применяет команду ApplyCropCommand.
18
+ */
19
+ export class CropController {
20
+ constructor({ core, eventBus, container }) {
21
+ this.core = core;
22
+ this.eventBus = eventBus;
23
+ this.container = container;
24
+
25
+ this._overlay = null;
26
+ this._objectId = null;
27
+ this._currentImgBounds = null;
28
+ this._onEscapeHandler = null;
29
+ this._onViewportHandler = null;
30
+
31
+ // Колбэк вызывается при входе/выходе из режима crop (true = вошли, false = вышли).
32
+ // Устанавливается снаружи (ImagePropertiesPanel).
33
+ this._onActivate = null;
34
+ // Флаг: смена формата внутри активного crop — не нужно скрывать/показывать панель.
35
+ this._changingFormat = false;
36
+ }
37
+
38
+ /**
39
+ * Войти в режим кадрирования.
40
+ * @param {string} objectId
41
+ * @param {string} template - 'custom'|'original'|'circle'|'square'|'portrait'|'landscape'|'wide'
42
+ */
43
+ start(objectId, template) {
44
+ const wasActive = !!this._overlay;
45
+ this.cancel();
46
+
47
+ let imgBounds = this._getObjectBounds(objectId);
48
+ if (!imgBounds) return;
49
+
50
+ // Для 'original' разворачиваем до оригинальных границ, если они сохранены
51
+ if (template === 'original') {
52
+ const objects = this.core.state.getObjects();
53
+ const obj = objects.find(o => o.id === objectId);
54
+ if (obj?.properties?.originalPosition && obj?.properties?.originalSize) {
55
+ imgBounds = {
56
+ x: obj.properties.originalPosition.x,
57
+ y: obj.properties.originalPosition.y,
58
+ w: obj.properties.originalSize.width,
59
+ h: obj.properties.originalSize.height,
60
+ };
61
+ }
62
+ }
63
+
64
+ this._objectId = objectId;
65
+ this._currentImgBounds = imgBounds;
66
+
67
+ const aspectRatio = ASPECT_RATIOS[template] ?? null;
68
+ const initCrop = this._computeInitialCrop(imgBounds, aspectRatio);
69
+
70
+ this._overlay = new CropOverlay({
71
+ container: this.container,
72
+ core: this.core,
73
+ imgBounds,
74
+ initCrop,
75
+ template,
76
+ aspectRatio,
77
+ onFormatChange: (newTemplate) => {
78
+ this._changingFormat = true;
79
+ this.start(objectId, newTemplate);
80
+ this._changingFormat = false;
81
+ },
82
+ onCommit: (cropNorm) => this._commit(objectId, cropNorm, template),
83
+ onCancel: () => this.cancel(),
84
+ });
85
+
86
+ if (!this._changingFormat && !wasActive) {
87
+ this._onActivate?.(true);
88
+ }
89
+
90
+ this._onEscapeHandler = () => this.cancel();
91
+ this.eventBus.on(Events.Keyboard.Escape, this._onEscapeHandler);
92
+
93
+ this._onViewportHandler = () => {
94
+ if (this._overlay && this._currentImgBounds) {
95
+ this._overlay.reposition(this._currentImgBounds);
96
+ }
97
+ };
98
+ this.eventBus.on(Events.Viewport.Changed, this._onViewportHandler);
99
+ this.eventBus.on(Events.UI.ZoomPercent, this._onViewportHandler);
100
+ this.eventBus.on(Events.Tool.PanUpdate, this._onViewportHandler);
101
+ }
102
+
103
+ /** Отменить режим кадрирования без применения. */
104
+ cancel() {
105
+ const wasActive = !!this._overlay;
106
+ if (this._overlay) {
107
+ this._overlay.destroy();
108
+ this._overlay = null;
109
+ }
110
+ if (this._onEscapeHandler) {
111
+ this.eventBus.off(Events.Keyboard.Escape, this._onEscapeHandler);
112
+ this._onEscapeHandler = null;
113
+ }
114
+ if (this._onViewportHandler) {
115
+ this.eventBus.off(Events.Viewport.Changed, this._onViewportHandler);
116
+ this.eventBus.off(Events.UI.ZoomPercent, this._onViewportHandler);
117
+ this.eventBus.off(Events.Tool.PanUpdate, this._onViewportHandler);
118
+ this._onViewportHandler = null;
119
+ }
120
+ this._objectId = null;
121
+ this._currentImgBounds = null;
122
+
123
+ if (wasActive && !this._changingFormat) {
124
+ this._onActivate?.(false);
125
+ }
126
+ }
127
+
128
+ destroy() {
129
+ this.cancel();
130
+ }
131
+
132
+ // ─────────────────────────────────────────────────────────────────────
133
+ // Приватные
134
+ // ─────────────────────────────────────────────────────────────────────
135
+
136
+ _getObjectBounds(objectId) {
137
+ const posData = { objectId, position: null };
138
+ const sizeData = { objectId, size: null };
139
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
140
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
141
+ if (!posData.position || !sizeData.size) return null;
142
+ return {
143
+ x: posData.position.x,
144
+ y: posData.position.y,
145
+ w: sizeData.size.width,
146
+ h: sizeData.size.height,
147
+ };
148
+ }
149
+
150
+ _computeInitialCrop(imgBounds, aspectRatio) {
151
+ if (!aspectRatio) {
152
+ return { x: 0, y: 0, w: 1, h: 1 };
153
+ }
154
+ // Вписать прямоугольник с нужным AR в центр изображения
155
+ const imgAR = imgBounds.w / imgBounds.h;
156
+ let normW, normH;
157
+ if (imgAR > aspectRatio) {
158
+ normH = 1;
159
+ normW = aspectRatio / imgAR;
160
+ } else {
161
+ normW = 1;
162
+ normH = imgAR / aspectRatio;
163
+ }
164
+ return {
165
+ x: (1 - normW) / 2,
166
+ y: (1 - normH) / 2,
167
+ w: normW,
168
+ h: normH,
169
+ };
170
+ }
171
+
172
+ _commit(objectId, cropNorm, template) {
173
+ // Сначала снимаем оверлей
174
+ this.cancel();
175
+
176
+ const objects = this.core.state.getObjects();
177
+ const obj = objects.find(o => o.id === objectId);
178
+ if (!obj) return;
179
+
180
+ // Запоминаем оригинал (один раз при первом кропе)
181
+ const isFirstCrop = !obj.properties?.originalPosition;
182
+ const originalPosition = isFirstCrop
183
+ ? { ...obj.position }
184
+ : { ...obj.properties.originalPosition };
185
+ const originalSize = isFirstCrop
186
+ ? { width: obj.width, height: obj.height }
187
+ : { ...obj.properties.originalSize };
188
+
189
+ const existingCrop = obj.properties?.cropRect || null;
190
+ let newX, newY, newW, newH, finalCropRect;
191
+
192
+ if (template === 'original') {
193
+ // cropNorm задан относительно оригинальных границ
194
+ newX = originalPosition.x + cropNorm.x * originalSize.width;
195
+ newY = originalPosition.y + cropNorm.y * originalSize.height;
196
+ newW = cropNorm.w * originalSize.width;
197
+ newH = cropNorm.h * originalSize.height;
198
+ finalCropRect = { ...cropNorm };
199
+ } else {
200
+ // Новая позиция и размер (в мировых пикселях)
201
+ newX = obj.position.x + cropNorm.x * obj.width;
202
+ newY = obj.position.y + cropNorm.y * obj.height;
203
+ newW = cropNorm.w * obj.width;
204
+ newH = cropNorm.h * obj.height;
205
+
206
+ // Составной cropRect относительно ОРИГИНАЛЬНОЙ текстуры
207
+ if (existingCrop) {
208
+ finalCropRect = {
209
+ x: existingCrop.x + cropNorm.x * existingCrop.w,
210
+ y: existingCrop.y + cropNorm.y * existingCrop.h,
211
+ w: cropNorm.w * existingCrop.w,
212
+ h: cropNorm.h * existingCrop.h,
213
+ };
214
+ } else {
215
+ finalCropRect = { ...cropNorm };
216
+ }
217
+ }
218
+
219
+ const before = {
220
+ cropRect: existingCrop ? { ...existingCrop } : null,
221
+ cropShape: obj.properties?.cropShape || null,
222
+ position: { ...obj.position },
223
+ size: { width: obj.width, height: obj.height },
224
+ originalPosition: { ...originalPosition },
225
+ originalSize: { ...originalSize },
226
+ borderRadius: obj.properties?.borderRadius || 0,
227
+ };
228
+
229
+ const after = {
230
+ cropRect: finalCropRect,
231
+ cropShape: template === 'circle' ? 'circle' : 'rect',
232
+ position: { x: newX, y: newY },
233
+ size: { width: newW, height: newH },
234
+ originalPosition,
235
+ originalSize,
236
+ borderRadius: obj.properties?.borderRadius || 0,
237
+ };
238
+
239
+ const cmd = new ApplyCropCommand(this.core, objectId, before, after);
240
+ this.core.history.executeCommand(cmd);
241
+ }
242
+ }
@@ -71,11 +71,55 @@ export class FrameService {
71
71
  };
72
72
  this.eventBus.on(Events.Object.Created, this._onObjectCreated);
73
73
 
74
- this._onDragStart = (data) => {
74
+ this._onStateChanged = (data) => {
75
+ if (!data || !data.updates || !data.updates.properties) return;
76
+ if (data.updates.properties.hidden !== undefined) {
77
+ const obj = this.state.state.objects.find(o => o.id === data.objectId);
78
+ if (obj && obj.type === 'frame') {
79
+ const isHidden = data.updates.properties.hidden;
80
+ const children = this._getFrameChildren(obj.id);
81
+ for (const childId of children) {
82
+ const pixi = this.pixi.objects.get(childId);
83
+ if (pixi) {
84
+ pixi.visible = !isHidden;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ const lockedChanged = data.updates.properties.locked !== undefined;
90
+ const lockModeChanged = data.updates.properties.lockMode !== undefined;
91
+ if (lockedChanged || lockModeChanged) {
92
+ const obj = this.state.state.objects.find(o => o.id === data.objectId);
93
+ if (obj && obj.type === 'frame') {
94
+ const newLocked = lockedChanged
95
+ ? !!data.updates.properties.locked
96
+ : !!(obj.properties && obj.properties.locked);
97
+ const newLockMode = lockModeChanged
98
+ ? data.updates.properties.lockMode
99
+ : (obj.properties && obj.properties.lockMode) || 'frame';
100
+ const children = this._getFrameChildren(obj.id);
101
+ for (const childId of children) {
102
+ const childObj = this.state.state.objects.find(o => o.id === childId);
103
+ if (!childObj) continue;
104
+ childObj.properties = childObj.properties || {};
105
+ if (newLocked && newLockMode === 'frame-and-content') {
106
+ childObj.properties.locked = true;
107
+ childObj.properties.lockedByFrame = true;
108
+ } else if (childObj.properties.lockedByFrame) {
109
+ childObj.properties.locked = false;
110
+ delete childObj.properties.lockedByFrame;
111
+ }
112
+ }
113
+ this.state.markDirty();
114
+ }
115
+ }
116
+ };
117
+ this.eventBus.on(Events.Object.StateChanged, this._onStateChanged);
118
+
119
+ this. _onDragStart = (data) => {
75
120
  const moved = this.state.state.objects.find(o => o.id === data.object);
76
121
  if (moved && moved.type === 'frame') {
77
- // Серый фон
78
- this.pixi.setFrameFill(moved.id, moved.width, moved.height, 0xFAFAFA);
122
+ this._frameDragOriginalFill = moved.backgroundColor ?? moved.properties?.backgroundColor ?? 0xFFFFFF;
79
123
  // Cнимок стартовых позиций по центру PIXI
80
124
  const fp = this.pixi.objects.get(moved.id);
81
125
  this._frameDragFrameStart = { x: fp?.x || 0, y: fp?.y || 0 };
@@ -139,18 +183,25 @@ export class FrameService {
139
183
  const movedObj = this.state.state.objects.find(o => o.id === data.object);
140
184
  if (!movedObj) return;
141
185
  if (movedObj.type === 'frame') {
142
- this.pixi.setFrameFill(movedObj.id, movedObj.width, movedObj.height, 0xFFFFFF);
186
+ const restoreFill = this._frameDragOriginalFill ?? movedObj.backgroundColor ?? movedObj.properties?.backgroundColor ?? 0xFFFFFF;
187
+ this.pixi.setFrameFill(movedObj.id, movedObj.width, movedObj.height, restoreFill);
143
188
  }
144
189
  this._recomputeFrameAttachment(movedObj.id);
145
190
  this._forceFramesBelow();
146
191
  this._frameDragFrameStart = null;
147
192
  this._frameDragChildStart = null;
193
+ this._frameDragOriginalFill = null;
148
194
  if (this._frameHoverId) {
149
195
  const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
150
196
  const prev = frames.find(fr => fr.id === this._frameHoverId);
151
- if (prev) this.pixi.setFrameFill(prev.id, prev.width, prev.height, 0xFFFFFF);
152
- this._frameHoverId = null;
153
- }
197
+ if (prev) {
198
+ const hoverOriginal = this._frameHoverOriginalFill?.get(this._frameHoverId)
199
+ ?? prev.backgroundColor ?? prev.properties?.backgroundColor ?? 0xFFFFFF;
200
+ this.pixi.setFrameFill(prev.id, prev.width, prev.height, hoverOriginal);
201
+ }
202
+ this._frameHoverId = null;
203
+ this._frameHoverOriginalFill = null;
204
+ }
154
205
  };
155
206
  this.eventBus.on(Events.Tool.DragEnd, this._onDragEnd);
156
207
  }
@@ -174,7 +225,9 @@ export class FrameService {
174
225
  this._onDragEnd = null;
175
226
  this._frameDragFrameStart = null;
176
227
  this._frameDragChildStart = null;
228
+ this._frameDragOriginalFill = null;
177
229
  this._frameHoverId = null;
230
+ this._frameHoverOriginalFill = null;
178
231
  }
179
232
 
180
233
  _getFrameChildren(frameId) {
@@ -216,11 +269,22 @@ export class FrameService {
216
269
  if (hoverId !== this._frameHoverId) {
217
270
  if (this._frameHoverId) {
218
271
  const prev = frames.find(fr => fr.id === this._frameHoverId);
219
- if (prev) this.pixi.setFrameFill(prev.id, prev.width, prev.height, 0xFFFFFF);
272
+ if (prev) {
273
+ const origFill = this._frameHoverOriginalFill?.get(this._frameHoverId)
274
+ ?? prev.backgroundColor ?? prev.properties?.backgroundColor ?? 0xFFFFFF;
275
+ this.pixi.setFrameFill(prev.id, prev.width, prev.height, origFill);
276
+ }
277
+ this._frameHoverOriginalFill?.delete(this._frameHoverId);
220
278
  }
221
279
  if (hoverId) {
222
280
  const cur = frames.find(fr => fr.id === hoverId);
223
- if (cur) this.pixi.setFrameFill(cur.id, cur.width, cur.height, 0xFAFAFA);
281
+ if (cur) {
282
+ if (!this._frameHoverOriginalFill) this._frameHoverOriginalFill = new Map();
283
+ if (!this._frameHoverOriginalFill.has(hoverId)) {
284
+ this._frameHoverOriginalFill.set(hoverId, cur.backgroundColor ?? cur.properties?.backgroundColor ?? 0xFFFFFF);
285
+ }
286
+ this.pixi.setFrameFill(cur.id, cur.width, cur.height, 0xFAFAFA);
287
+ }
224
288
  }
225
289
  this._frameHoverId = hoverId || null;
226
290
  }
@@ -256,6 +320,20 @@ export class FrameService {
256
320
  this._forceFramesBelow();
257
321
  this.eventBus.emit(Events.Object.Reordered, { reason: 'recompute_frame_attachment' });
258
322
  }
323
+
324
+ // Обновляем видимость в зависимости от скрытости фрейма
325
+ const pixi = this.pixi.objects.get(objectId);
326
+ if (pixi) {
327
+ let isHidden = false;
328
+ const targetFrameId = obj.properties?.frameId;
329
+ if (targetFrameId) {
330
+ const frameObj = this.state.state.objects.find(o => o.id === targetFrameId);
331
+ if (frameObj && frameObj.properties?.hidden) {
332
+ isHidden = true;
333
+ }
334
+ }
335
+ pixi.visible = !isHidden;
336
+ }
259
337
  }
260
338
  }
261
339
 
@@ -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
+ }
@@ -29,6 +29,10 @@ export class ToolManagerLifecycle {
29
29
  manager._onContextMenu = (e) => {
30
30
  e.preventDefault();
31
31
  if (!manager.activeTool) return;
32
+ if (manager.activeTool.name === 'draw') {
33
+ manager.activateDefaultTool();
34
+ return;
35
+ }
32
36
  const rect = manager.container.getBoundingClientRect();
33
37
  const toolEvent = {
34
38
  x: e.clientX - rect.left,
@@ -98,6 +98,7 @@ export class DrawingTool extends BaseTool {
98
98
  }
99
99
 
100
100
  onMouseDown(event) {
101
+ if (event.button === 2) return;
101
102
  super.onMouseDown(event);
102
103
  if (!this.world) this.world = this._getWorldLayer();
103
104
  if (!this.world) return;
@@ -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('#')) {