@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
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { BoardService } from '../../services/BoardService.js';
|
|
|
4
4
|
import { ZoomPanController } from '../../services/ZoomPanController.js';
|
|
5
5
|
import { ZOrderManager } from '../../services/ZOrderManager.js';
|
|
6
6
|
import { FrameService } from '../../services/FrameService.js';
|
|
7
|
+
import { ImageAttachmentService } from '../../services/ImageAttachmentService.js';
|
|
7
8
|
import { Events } from '../events/Events.js';
|
|
8
9
|
|
|
9
10
|
export async function initializeCore(core) {
|
|
@@ -20,6 +21,8 @@ export async function initializeCore(core) {
|
|
|
20
21
|
core.zOrder.attach();
|
|
21
22
|
core.frameService = new FrameService(core.eventBus, core.pixi, core.state);
|
|
22
23
|
core.frameService.attach();
|
|
24
|
+
core.imageAttachmentService = new ImageAttachmentService(core.eventBus, core.pixi, core.state);
|
|
25
|
+
core.imageAttachmentService.attach();
|
|
23
26
|
|
|
24
27
|
setupViewportResize(core);
|
|
25
28
|
|
|
@@ -169,12 +169,14 @@ function tryCreateConnectorStyleCommand(core, object, objectId, updates) {
|
|
|
169
169
|
const { style, start, end } = updates.properties;
|
|
170
170
|
|
|
171
171
|
// Проверяем, что в updates.properties только style/start/end (и нет посторонних ключей)
|
|
172
|
-
const allowedKeys = new Set(['style', 'start', 'end', 'locked']);
|
|
172
|
+
const allowedKeys = new Set(['style', 'start', 'end', 'locked', 'lockMode', 'lockedByFrame']);
|
|
173
173
|
const hasOtherKeys = Object.keys(updates.properties).some(k => !allowedKeys.has(k));
|
|
174
174
|
if (hasOtherKeys) return false;
|
|
175
175
|
// Должно быть хотя бы одно из: style, start, end
|
|
176
176
|
if (!style && start === undefined && end === undefined
|
|
177
|
-
&& updates.properties.locked === undefined
|
|
177
|
+
&& updates.properties.locked === undefined
|
|
178
|
+
&& updates.properties.lockMode === undefined
|
|
179
|
+
&& updates.properties.lockedByFrame === undefined) return false;
|
|
178
180
|
|
|
179
181
|
const commandUpdates = {};
|
|
180
182
|
if (style !== undefined) commandUpdates.style = style;
|
package/src/core/index.js
CHANGED
|
@@ -39,7 +39,7 @@ export class CoreMoodBoard {
|
|
|
39
39
|
autoSave: false,
|
|
40
40
|
width: this.container.clientWidth || 800,
|
|
41
41
|
height: this.container.clientHeight || 600,
|
|
42
|
-
backgroundColor:
|
|
42
|
+
backgroundColor: 0xFFFFFF,
|
|
43
43
|
...options
|
|
44
44
|
};
|
|
45
45
|
|
|
@@ -624,7 +624,7 @@ export class CoreMoodBoard {
|
|
|
624
624
|
const app = this.pixi?.app;
|
|
625
625
|
const rendererBg = app?.renderer?.background?.color ?? app?.renderer?.backgroundColor;
|
|
626
626
|
const toHex = (num) => {
|
|
627
|
-
try { return '#' + Number(num >>> 0).toString(16).padStart(6, '0'); } catch (_) { return '#
|
|
627
|
+
try { return '#' + Number(num >>> 0).toString(16).padStart(6, '0'); } catch (_) { return '#FFFFFF'; }
|
|
628
628
|
};
|
|
629
629
|
const world = this.pixi?.worldLayer || app?.stage;
|
|
630
630
|
const currentZoom = Math.max(0.02, Math.min(5, world?.scale?.x || 1));
|
|
@@ -653,7 +653,7 @@ export class CoreMoodBoard {
|
|
|
653
653
|
})();
|
|
654
654
|
|
|
655
655
|
const settings = {
|
|
656
|
-
backgroundColor: toHex(rendererBg ??
|
|
656
|
+
backgroundColor: toHex(rendererBg ?? 0xFFFFFF),
|
|
657
657
|
grid: gridSettings || undefined,
|
|
658
658
|
zoom: { min: 0.1, max: 5.0, default: 1.0, current: currentZoom },
|
|
659
659
|
pan: currentPan,
|
package/src/initNoBundler.js
CHANGED
|
@@ -17,10 +17,10 @@ export class FrameObject {
|
|
|
17
17
|
this.height = this.objectData.height || 100;
|
|
18
18
|
// Берем стили рамки из CSS-переменных, с дефолтом
|
|
19
19
|
const rootStyles = (typeof window !== 'undefined') ? getComputedStyle(document.documentElement) : null;
|
|
20
|
-
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '
|
|
20
|
+
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '5') : 5;
|
|
21
21
|
const cssCornerRadius = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-corner-radius') || '6') : 6;
|
|
22
22
|
const cssBorderColor = rootStyles ? rootStyles.getPropertyValue('--frame-border-color').trim() : '';
|
|
23
|
-
this.borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth :
|
|
23
|
+
this.borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth : 5;
|
|
24
24
|
// Используем backgroundColor из данных объекта, если есть, иначе белый
|
|
25
25
|
this.fillColor = this.objectData.backgroundColor || this.objectData.properties?.backgroundColor || 0xFFFFFF;
|
|
26
26
|
// Режим заливки: solid | solid-bordered | outline
|
|
@@ -50,6 +50,10 @@ export class FrameObject {
|
|
|
50
50
|
// Под-контейнер: масштаб компенсирует зум, поэтому заголовок всегда одного размера на экране
|
|
51
51
|
this.titleLayer = new PIXI.Container();
|
|
52
52
|
this.titleLayer.eventMode = 'none'; // не перехватывать указатель
|
|
53
|
+
const _frameObjectId = this.objectData.id || '';
|
|
54
|
+
if (_frameObjectId) {
|
|
55
|
+
this.titleLayer.name = `mb-frame-title-${_frameObjectId}`;
|
|
56
|
+
}
|
|
53
57
|
|
|
54
58
|
this.titleBg = new PIXI.Graphics();
|
|
55
59
|
this.titleLayer.addChild(this.titleBg);
|
|
@@ -60,7 +64,7 @@ export class FrameObject {
|
|
|
60
64
|
fill: 0x333333,
|
|
61
65
|
fontWeight: '500'
|
|
62
66
|
});
|
|
63
|
-
this.titleText.anchor.set(0, 0);
|
|
67
|
+
this.titleText.anchor.set(0, 0.5);
|
|
64
68
|
this.titleLayer.addChild(this.titleText);
|
|
65
69
|
|
|
66
70
|
this.container.addChild(this.titleLayer);
|
|
@@ -141,7 +145,10 @@ export class FrameObject {
|
|
|
141
145
|
|
|
142
146
|
_onSelectionAdd(data) {
|
|
143
147
|
const myId = this.objectData?.id ?? this.container?._mb?.objectId;
|
|
144
|
-
if (data?.object
|
|
148
|
+
if (data?.object !== myId) return;
|
|
149
|
+
// В outline цветная рамка — основной визуал фрейма, не дублирует синюю рамку выделения.
|
|
150
|
+
if (this.bgMode === 'outline') return;
|
|
151
|
+
this.setBorderVisible(false);
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
_onSelectionRemove(data) {
|
|
@@ -276,37 +283,29 @@ export class FrameObject {
|
|
|
276
283
|
const fillColor = typeof color === 'number' ? color : 0xFFFFFF;
|
|
277
284
|
|
|
278
285
|
if (bgMode === 'solid') {
|
|
279
|
-
// Сплошная заливка
|
|
280
|
-
if (showStroke) {
|
|
281
|
-
try {
|
|
282
|
-
g.lineStyle({ width: this.borderWidth, color: this.strokeColor, alpha: 1, alignment: 1 });
|
|
283
|
-
} catch (e) {
|
|
284
|
-
g.lineStyle(this.borderWidth, this.strokeColor, 1);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
286
|
+
// Сплошная заливка без собственной рамки (рамка выделения — отдельно)
|
|
287
287
|
g.beginFill(fillColor, 1);
|
|
288
288
|
g.drawRoundedRect(0, 0, Math.max(0, width), Math.max(0, height), this.cornerRadius);
|
|
289
289
|
g.endFill();
|
|
290
290
|
} else if (bgMode === 'solid-bordered') {
|
|
291
|
-
//
|
|
291
|
+
// Фон = выбранный цвет; рамка = S=100,V=50 от того же тона палитры
|
|
292
|
+
const borderColor = this._pickBorderColor(fillColor);
|
|
292
293
|
if (showStroke) {
|
|
293
294
|
try {
|
|
294
|
-
g.lineStyle({ width: this.borderWidth
|
|
295
|
+
g.lineStyle({ width: this.borderWidth, color: borderColor, alpha: 1, alignment: 0 });
|
|
295
296
|
} catch (e) {
|
|
296
|
-
g.lineStyle(this.borderWidth
|
|
297
|
+
g.lineStyle(this.borderWidth, borderColor, 1);
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
|
-
g.beginFill(
|
|
300
|
+
g.beginFill(fillColor, 1);
|
|
300
301
|
g.drawRoundedRect(0, 0, Math.max(0, width), Math.max(0, height), this.cornerRadius);
|
|
301
302
|
g.endFill();
|
|
302
303
|
} else {
|
|
303
|
-
// outline: прозрачный фон, рамка цвета выбранного цвета
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
g.lineStyle(this.borderWidth, fillColor, 1);
|
|
309
|
-
}
|
|
304
|
+
// outline: прозрачный фон, рамка цвета выбранного цвета (всегда видна, в т.ч. при фокусе)
|
|
305
|
+
try {
|
|
306
|
+
g.lineStyle({ width: this.borderWidth, color: fillColor, alpha: 1, alignment: 1 });
|
|
307
|
+
} catch (e) {
|
|
308
|
+
g.lineStyle(this.borderWidth, fillColor, 1);
|
|
310
309
|
}
|
|
311
310
|
g.beginFill(0x000000, 0);
|
|
312
311
|
g.drawRoundedRect(0, 0, Math.max(0, width), Math.max(0, height), this.cornerRadius);
|
|
@@ -352,7 +351,7 @@ export class FrameObject {
|
|
|
352
351
|
|
|
353
352
|
// Высота подложки в базовых пикселях: baseFontSize + 4px сверху + 4px снизу
|
|
354
353
|
const labelBaseH = this.baseFontSize + 8;
|
|
355
|
-
const gap =
|
|
354
|
+
const gap = 5; // зазор между нижним краем подписи и верхней границей фрейма
|
|
356
355
|
|
|
357
356
|
// Позиционируем над фреймом (y=0 — верхний край фрейма в локальных координатах контейнера)
|
|
358
357
|
this.titleLayer.x = 0;
|
|
@@ -372,6 +371,143 @@ export class FrameObject {
|
|
|
372
371
|
this._redrawTitleBg();
|
|
373
372
|
}
|
|
374
373
|
|
|
374
|
+
/**
|
|
375
|
+
* YIQ-контраст: возвращает 0x000000 или 0xFFFFFF — читаемый цвет на заданном фоне
|
|
376
|
+
* @param {number} hexInt Цвет фона (PIXI hex)
|
|
377
|
+
* @returns {number}
|
|
378
|
+
*/
|
|
379
|
+
_pickContrastColor(hexInt) {
|
|
380
|
+
const r = (hexInt >> 16) & 0xFF;
|
|
381
|
+
const g = (hexInt >> 8) & 0xFF;
|
|
382
|
+
const b = hexInt & 0xFF;
|
|
383
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
384
|
+
const darkness = (1 - yiq / 255) * 100;
|
|
385
|
+
return darkness >= 35 ? 0xFFFFFF : 0x000000;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Цвет рамки в режиме solid-bordered.
|
|
390
|
+
* Крайне правый по горизонтали (S=100) и по середине вертикали (V=50)
|
|
391
|
+
* в палитре того же тона, что и фон.
|
|
392
|
+
* Белый фон: крайне левый по горизонтали (S=0), середина по вертикали (V=50) → нейтральный серый.
|
|
393
|
+
* Прочие ахроматические цвета (серый, чёрный — s=0): чёрный.
|
|
394
|
+
* @param {number} hexInt Цвет фона (PIXI hex)
|
|
395
|
+
* @returns {number}
|
|
396
|
+
*/
|
|
397
|
+
_pickBorderColor(hexInt) {
|
|
398
|
+
const r = (hexInt >> 16) & 0xFF;
|
|
399
|
+
const g = (hexInt >> 8) & 0xFF;
|
|
400
|
+
const b = hexInt & 0xFF;
|
|
401
|
+
if (r === 255 && g === 255 && b === 255) {
|
|
402
|
+
return 0x808080;
|
|
403
|
+
}
|
|
404
|
+
const { h, s } = this._rgbToHsv(r, g, b);
|
|
405
|
+
if (s === 0) {
|
|
406
|
+
return 0x000000;
|
|
407
|
+
}
|
|
408
|
+
return this._hsvToHex(h, 100, 50);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Цвет текста заголовка фрейма.
|
|
413
|
+
* Для почти-белых тонированных фонов (яркость < 5 по шкале 0=белый…100=чёрный,
|
|
414
|
+
* но не чистый белый) берёт тон из той же палитры с макс. насыщенностью (S=100, V=50).
|
|
415
|
+
* Иначе — обычный YIQ-контраст (чёрный/белый).
|
|
416
|
+
* @param {number} hexInt Цвет фона (PIXI hex)
|
|
417
|
+
* @returns {number}
|
|
418
|
+
*/
|
|
419
|
+
_pickTitleTextColor(hexInt) {
|
|
420
|
+
const r = (hexInt >> 16) & 0xFF;
|
|
421
|
+
const g = (hexInt >> 8) & 0xFF;
|
|
422
|
+
const b = hexInt & 0xFF;
|
|
423
|
+
|
|
424
|
+
// Чистый белый фон — исключение: чёрный текст
|
|
425
|
+
if (r === 255 && g === 255 && b === 255) {
|
|
426
|
+
return 0x000000;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Яркость по шкале 0 (белый) → 100 (чёрный) через HSL-lightness
|
|
430
|
+
const darkness = 100 - ((Math.max(r, g, b) + Math.min(r, g, b)) / 2 / 255) * 100;
|
|
431
|
+
if (darkness >= 5) {
|
|
432
|
+
return this._pickContrastColor(hexInt);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Почти-белый тонированный фон: та же палитра, правый край по центру (S=100, V=50)
|
|
436
|
+
const { h } = this._rgbToHsv(r, g, b);
|
|
437
|
+
return this._hsvToHex(h, 100, 50);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {number} r 0..255
|
|
442
|
+
* @param {number} g 0..255
|
|
443
|
+
* @param {number} b 0..255
|
|
444
|
+
* @returns {{ h: number, s: number, v: number }} h 0..360, s/v 0..100
|
|
445
|
+
*/
|
|
446
|
+
_rgbToHsv(r, g, b) {
|
|
447
|
+
const rn = r / 255, gn = g / 255, bn = b / 255;
|
|
448
|
+
const max = Math.max(rn, gn, bn);
|
|
449
|
+
const min = Math.min(rn, gn, bn);
|
|
450
|
+
const d = max - min;
|
|
451
|
+
let h = 0;
|
|
452
|
+
if (d !== 0) {
|
|
453
|
+
if (max === rn) {
|
|
454
|
+
h = ((gn - bn) / d) % 6;
|
|
455
|
+
} else if (max === gn) {
|
|
456
|
+
h = (bn - rn) / d + 2;
|
|
457
|
+
} else {
|
|
458
|
+
h = (rn - gn) / d + 4;
|
|
459
|
+
}
|
|
460
|
+
h *= 60;
|
|
461
|
+
if (h < 0) {
|
|
462
|
+
h += 360;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const s = max === 0 ? 0 : (d / max) * 100;
|
|
466
|
+
return { h, s, v: max * 100 };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @param {number} h 0..360
|
|
471
|
+
* @param {number} s 0..100
|
|
472
|
+
* @param {number} v 0..100
|
|
473
|
+
* @returns {number} PIXI hex
|
|
474
|
+
*/
|
|
475
|
+
_hsvToHex(h, s, v) {
|
|
476
|
+
const sn = s / 100, vn = v / 100;
|
|
477
|
+
const c = vn * sn;
|
|
478
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
479
|
+
const m = vn - c;
|
|
480
|
+
let rp = 0, gp = 0, bp = 0;
|
|
481
|
+
if (h < 60) {
|
|
482
|
+
rp = c; gp = x;
|
|
483
|
+
} else if (h < 120) {
|
|
484
|
+
rp = x; gp = c;
|
|
485
|
+
} else if (h < 180) {
|
|
486
|
+
gp = c; bp = x;
|
|
487
|
+
} else if (h < 240) {
|
|
488
|
+
gp = x; bp = c;
|
|
489
|
+
} else if (h < 300) {
|
|
490
|
+
rp = x; bp = c;
|
|
491
|
+
} else {
|
|
492
|
+
rp = c; bp = x;
|
|
493
|
+
}
|
|
494
|
+
const r = Math.round((rp + m) * 255);
|
|
495
|
+
const g = Math.round((gp + m) * 255);
|
|
496
|
+
const b = Math.round((bp + m) * 255);
|
|
497
|
+
return (r << 16) | (g << 8) | b;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Возвращает цвета плашки заголовка в формате CSS-строк для HTML-инпута
|
|
502
|
+
* @returns {{ bgCss: string, textCss: string }}
|
|
503
|
+
*/
|
|
504
|
+
getTitleColors() {
|
|
505
|
+
const bg = this.fillColor;
|
|
506
|
+
const text = this._pickTitleTextColor(bg);
|
|
507
|
+
const toHex = (n) => '#' + n.toString(16).padStart(6, '0');
|
|
508
|
+
return { bgCss: toHex(bg), textCss: toHex(text) };
|
|
509
|
+
}
|
|
510
|
+
|
|
375
511
|
/**
|
|
376
512
|
* Нарисовать скруглённую подложку под текущую ширину текста
|
|
377
513
|
*/
|
|
@@ -381,6 +517,13 @@ export class FrameObject {
|
|
|
381
517
|
const padH = 8; // горизонтальный отступ с каждой стороны
|
|
382
518
|
const padV = 4; // вертикальный отступ с каждой стороны
|
|
383
519
|
|
|
520
|
+
// Фон плашки = цвет фрейма (fill или border в зависимости от bgMode — всегда this.fillColor)
|
|
521
|
+
const bgColor = typeof this.fillColor === 'number' ? this.fillColor : 0xFFFFFF;
|
|
522
|
+
const textColor = this._pickTitleTextColor(bgColor);
|
|
523
|
+
|
|
524
|
+
// Обновляем цвет текста под контраст фона
|
|
525
|
+
this.titleText.style.fill = textColor;
|
|
526
|
+
|
|
384
527
|
// Измеряем текст в базовых единицах
|
|
385
528
|
const style = new PIXI.TextStyle({
|
|
386
529
|
fontFamily: this.titleText.style.fontFamily,
|
|
@@ -394,18 +537,14 @@ export class FrameObject {
|
|
|
394
537
|
|
|
395
538
|
const g = this.titleBg;
|
|
396
539
|
g.clear();
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} catch (_) {
|
|
400
|
-
g.lineStyle(1, this.strokeColor, 1);
|
|
401
|
-
}
|
|
402
|
-
g.beginFill(0xFFFFFF, 1);
|
|
540
|
+
g.lineStyle(0);
|
|
541
|
+
g.beginFill(bgColor, 1);
|
|
403
542
|
g.drawRoundedRect(0, 0, bgW, bgH, 6);
|
|
404
543
|
g.endFill();
|
|
405
544
|
|
|
406
|
-
// Текст внутри подложки
|
|
545
|
+
// Текст внутри подложки — по вертикали по центру (anchor.y = 0.5)
|
|
407
546
|
this.titleText.x = padH;
|
|
408
|
-
this.titleText.y =
|
|
547
|
+
this.titleText.y = Math.round(bgH / 2);
|
|
409
548
|
}
|
|
410
549
|
|
|
411
550
|
/**
|
|
@@ -86,14 +86,40 @@ export class FrameService {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
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
|
+
}
|
|
89
116
|
};
|
|
90
117
|
this.eventBus.on(Events.Object.StateChanged, this._onStateChanged);
|
|
91
118
|
|
|
92
|
-
this._onDragStart = (data) => {
|
|
119
|
+
this. _onDragStart = (data) => {
|
|
93
120
|
const moved = this.state.state.objects.find(o => o.id === data.object);
|
|
94
121
|
if (moved && moved.type === 'frame') {
|
|
95
|
-
|
|
96
|
-
this.pixi.setFrameFill(moved.id, moved.width, moved.height, 0xFAFAFA);
|
|
122
|
+
this._frameDragOriginalFill = moved.backgroundColor ?? moved.properties?.backgroundColor ?? 0xFFFFFF;
|
|
97
123
|
// Cнимок стартовых позиций по центру PIXI
|
|
98
124
|
const fp = this.pixi.objects.get(moved.id);
|
|
99
125
|
this._frameDragFrameStart = { x: fp?.x || 0, y: fp?.y || 0 };
|
|
@@ -157,18 +183,25 @@ export class FrameService {
|
|
|
157
183
|
const movedObj = this.state.state.objects.find(o => o.id === data.object);
|
|
158
184
|
if (!movedObj) return;
|
|
159
185
|
if (movedObj.type === 'frame') {
|
|
160
|
-
this.
|
|
186
|
+
const restoreFill = this._frameDragOriginalFill ?? movedObj.backgroundColor ?? movedObj.properties?.backgroundColor ?? 0xFFFFFF;
|
|
187
|
+
this.pixi.setFrameFill(movedObj.id, movedObj.width, movedObj.height, restoreFill);
|
|
161
188
|
}
|
|
162
189
|
this._recomputeFrameAttachment(movedObj.id);
|
|
163
190
|
this._forceFramesBelow();
|
|
164
191
|
this._frameDragFrameStart = null;
|
|
165
192
|
this._frameDragChildStart = null;
|
|
193
|
+
this._frameDragOriginalFill = null;
|
|
166
194
|
if (this._frameHoverId) {
|
|
167
195
|
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
168
196
|
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
169
|
-
if (prev)
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
}
|
|
172
205
|
};
|
|
173
206
|
this.eventBus.on(Events.Tool.DragEnd, this._onDragEnd);
|
|
174
207
|
}
|
|
@@ -192,7 +225,9 @@ export class FrameService {
|
|
|
192
225
|
this._onDragEnd = null;
|
|
193
226
|
this._frameDragFrameStart = null;
|
|
194
227
|
this._frameDragChildStart = null;
|
|
228
|
+
this._frameDragOriginalFill = null;
|
|
195
229
|
this._frameHoverId = null;
|
|
230
|
+
this._frameHoverOriginalFill = null;
|
|
196
231
|
}
|
|
197
232
|
|
|
198
233
|
_getFrameChildren(frameId) {
|
|
@@ -234,11 +269,22 @@ export class FrameService {
|
|
|
234
269
|
if (hoverId !== this._frameHoverId) {
|
|
235
270
|
if (this._frameHoverId) {
|
|
236
271
|
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
237
|
-
if (prev)
|
|
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);
|
|
238
278
|
}
|
|
239
279
|
if (hoverId) {
|
|
240
280
|
const cur = frames.find(fr => fr.id === hoverId);
|
|
241
|
-
if (cur)
|
|
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
|
+
}
|
|
242
288
|
}
|
|
243
289
|
this._frameHoverId = hoverId || null;
|
|
244
290
|
}
|