@sequent-org/moodboard 1.4.70 → 1.4.71
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/objects/shape/shapeTextArea.js +92 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +3 -1
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +1 -1
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +1 -1
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -1
- package/src/tools/object-tools/selection/SelectInputRouter.js +1 -1
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +2 -2
- package/src/tools/object-tools/selection/TextInlineEditorController.js +98 -11
- package/src/ui/HtmlTextLayer.js +35 -0
package/package.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-safe area (вписанный прямоугольник) внутри фигуры.
|
|
3
|
+
*
|
|
4
|
+
* Текст фигуры не должен выходить за её видимый контур. Bounding box фигуры
|
|
5
|
+
* прямоугольный, но сама фигура (треугольник, ромб, круг, стрелка, параллелограмм)
|
|
6
|
+
* занимает лишь часть бокса. Здесь считаем максимальный прямоугольник, который
|
|
7
|
+
* гарантированно лежит внутри контура — по нему позиционируется и статический
|
|
8
|
+
* HTML-оверлей текста, и inline-редактор.
|
|
9
|
+
*
|
|
10
|
+
* Геометрия форм — из ShapeDrawer._drawFillShape (тот же контур).
|
|
11
|
+
*
|
|
12
|
+
* @param {string} kind — square|rounded|circle|triangle|diamond|parallelogram|arrow
|
|
13
|
+
* @param {number} w — ширина bounds (world px)
|
|
14
|
+
* @param {number} h — высота bounds (world px)
|
|
15
|
+
* @returns {{left:number, top:number, width:number, height:number}} область в локальных координатах бокса (0,0 — левый верх)
|
|
16
|
+
*/
|
|
17
|
+
// Доля запаса внутрь контура для непрямоугольных фигур. Вписанный прямоугольник и
|
|
18
|
+
// клип-контур касаются краёв фигуры углами/сторонами, поэтому глифы верхней строки
|
|
19
|
+
// при переполнении «липнут» к наклонной стороне (для круга — углы вписанного квадрата
|
|
20
|
+
// лежат ровно на окружности). Ужимаем безопасную зону на этот множитель, чтобы текст
|
|
21
|
+
// не доходил до самого контура. 0.8 даёт видимый зазор (~20% радиуса) и служит буфером
|
|
22
|
+
// против небольшого смещения бокса относительно центра фигуры.
|
|
23
|
+
const SHAPE_INNER_MARGIN = 0.8;
|
|
24
|
+
|
|
25
|
+
export function getShapeTextSafeArea(kind, w, h) {
|
|
26
|
+
const W = Math.max(0, w || 0);
|
|
27
|
+
const H = Math.max(0, h || 0);
|
|
28
|
+
|
|
29
|
+
switch (kind) {
|
|
30
|
+
case 'circle': {
|
|
31
|
+
// Вписанный прямоугольник в эллипс (полуоси W/2, H/2): множитель 1/√2.
|
|
32
|
+
// Углы такого прямоугольника лежат точно на окружности, поэтому дополнительно
|
|
33
|
+
// ужимаем на SHAPE_INNER_MARGIN — иначе крайние строки касаются контура.
|
|
34
|
+
const f = (1 / Math.SQRT2) * SHAPE_INNER_MARGIN;
|
|
35
|
+
const iw = W * f;
|
|
36
|
+
const ih = H * f;
|
|
37
|
+
return { left: (W - iw) / 2, top: (H - ih) / 2, width: iw, height: ih };
|
|
38
|
+
}
|
|
39
|
+
case 'triangle': {
|
|
40
|
+
// Вершина сверху (W/2,0), основание снизу. Ширину берём по вписанному в
|
|
41
|
+
// основание прямоугольнику (W/2), но по вертикали центрируем на середине
|
|
42
|
+
// фигуры, а не прижимаем к низу — текст должен стоять по центру и расти
|
|
43
|
+
// вверх при переполнении. Верхние углы box'а выходят за наклонные стороны,
|
|
44
|
+
// но их прячет клип-контур (getShapeTextClipPath) с внутренним отступом.
|
|
45
|
+
return { left: W * 0.25, top: H * 0.25, width: W * 0.5, height: H * 0.5 };
|
|
46
|
+
}
|
|
47
|
+
case 'diamond': {
|
|
48
|
+
// Ромб с вершинами по серединам сторон: вписанный центрированный
|
|
49
|
+
// прямоугольник — половина ширины и высоты. Его углы лежат на сторонах
|
|
50
|
+
// ромба, поэтому ужимаем на SHAPE_INNER_MARGIN, чтобы текст не касался края.
|
|
51
|
+
const dw = W * 0.5 * SHAPE_INNER_MARGIN;
|
|
52
|
+
const dh = H * 0.5 * SHAPE_INNER_MARGIN;
|
|
53
|
+
return { left: (W - dw) / 2, top: (H - dh) / 2, width: dw, height: dh };
|
|
54
|
+
}
|
|
55
|
+
case 'parallelogram': {
|
|
56
|
+
// skew совпадает с ShapeDrawer. Безопасна средняя вертикальная полоса.
|
|
57
|
+
const skew = Math.min(W * 0.25, 20);
|
|
58
|
+
return { left: skew, top: 0, width: Math.max(0, W - 2 * skew), height: H };
|
|
59
|
+
}
|
|
60
|
+
case 'arrow': {
|
|
61
|
+
// Прямоугольное «древко» стрелки — единственная зона с полной высотой полосы.
|
|
62
|
+
const shaftH = Math.max(6, H * 0.3);
|
|
63
|
+
const shaftY = (H - shaftH) / 2;
|
|
64
|
+
return { left: 0, top: shaftY, width: W * 0.6, height: shaftH };
|
|
65
|
+
}
|
|
66
|
+
case 'square':
|
|
67
|
+
case 'rounded':
|
|
68
|
+
default:
|
|
69
|
+
return { left: 0, top: 0, width: W, height: H };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* CSS clip-path для текстового слоя фигуры — чтобы текст, вышедший за наклонные
|
|
75
|
+
* стороны, обрезался контуром («прятался внутри фигуры»), а не торчал наружу.
|
|
76
|
+
*
|
|
77
|
+
* Координаты заданы в системе САМОГО слоя (он позиционируется по safe-area из
|
|
78
|
+
* getShapeTextSafeArea). Для большинства фигур safe-area целиком лежит внутри
|
|
79
|
+
* контура, поэтому клип не нужен (null). Исключение — треугольник: верх safe-area
|
|
80
|
+
* (центральный прямоугольник W/2×H/2) шире контура, поэтому клипуем трапецией.
|
|
81
|
+
* Трапеция задана с внутренним отступом от наклонных сторон (не 25%/0, а 29%/5%),
|
|
82
|
+
* чтобы верхняя строка при переполнении не касалась контура, а обрезалась чуть внутри.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} kind
|
|
85
|
+
* @returns {string|null}
|
|
86
|
+
*/
|
|
87
|
+
export function getShapeTextClipPath(kind) {
|
|
88
|
+
if (kind === 'triangle') {
|
|
89
|
+
return 'polygon(29% 5%, 71% 5%, 93% 95%, 7% 95%)';
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
@@ -262,7 +262,9 @@ export class PlacementInputRouter {
|
|
|
262
262
|
id: objectData.id,
|
|
263
263
|
type: 'shape',
|
|
264
264
|
position: objectData.position,
|
|
265
|
-
|
|
265
|
+
// kind обязателен: без него редактор берёт safe-area как для квадрата
|
|
266
|
+
// (полные границы), и текст в круге/треугольнике вылезает за контур.
|
|
267
|
+
properties: { content: '', kind: objectData.properties?.kind || 'square' }
|
|
266
268
|
},
|
|
267
269
|
create: true
|
|
268
270
|
});
|
|
@@ -322,7 +322,7 @@ export function onDoubleClick(event) {
|
|
|
322
322
|
id: hitResult.object,
|
|
323
323
|
type: 'shape',
|
|
324
324
|
position: posData.position,
|
|
325
|
-
properties: { content: shapeContent },
|
|
325
|
+
properties: { content: shapeContent, kind: pix._mb?.properties?.kind || 'square' },
|
|
326
326
|
caretClick: {
|
|
327
327
|
clientX: event?.originalEvent?.clientX,
|
|
328
328
|
clientY: event?.originalEvent?.clientY,
|
|
@@ -232,7 +232,7 @@ export function bindTextEditorInteractions(controller, {
|
|
|
232
232
|
// который тут же снимает фокус с только что созданного пустого поля.
|
|
233
233
|
// В течение короткого окна после создания не финализируем, а возвращаем фокус.
|
|
234
234
|
if (isNewCreation && (Date.now() - editorCreatedAt) < 400) {
|
|
235
|
-
try { textarea.focus(); } catch (_) {}
|
|
235
|
+
try { textarea.focus({ preventScroll: true }); } catch (_) {}
|
|
236
236
|
return;
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -291,7 +291,7 @@ export function bindTextEditorInteractions(controller, {
|
|
|
291
291
|
// Страховочный возврат фокуса на случай, если браузер всё-таки убрал фокус.
|
|
292
292
|
setTimeout(() => {
|
|
293
293
|
if (controller?.textEditor?.active && textarea) {
|
|
294
|
-
try { textarea.focus(); } catch (_) {}
|
|
294
|
+
try { textarea.focus({ preventScroll: true }); } catch (_) {}
|
|
295
295
|
}
|
|
296
296
|
}, 0);
|
|
297
297
|
};
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { updateCustomCaret } from './TextEditorCaretService.js';
|
|
33
33
|
import { TEXT_BOX_BOTTOM_PAD_PX } from '../../../services/text/TextBoxMetrics.js';
|
|
34
34
|
import { buildHtmlWithRanges } from '../../../ui/HtmlTextLayer.js';
|
|
35
|
+
import { getShapeTextSafeArea, getShapeTextClipPath } from '../../../objects/shape/shapeTextArea.js';
|
|
35
36
|
|
|
36
37
|
export function openTextEditor(object, create = false) {
|
|
37
38
|
|
|
@@ -77,7 +78,7 @@ export function openTextEditor(object, create = false) {
|
|
|
77
78
|
this.textEditor.objectType === objectType
|
|
78
79
|
);
|
|
79
80
|
if (sameEditorObject && this.textEditor.textarea) {
|
|
80
|
-
try { this.textEditor.textarea.focus(); } catch (_) {}
|
|
81
|
+
try { this.textEditor.textarea.focus({ preventScroll: true }); } catch (_) {}
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
84
|
this._closeTextEditor(true);
|
|
@@ -99,6 +100,9 @@ export function openTextEditor(object, create = false) {
|
|
|
99
100
|
const meta = pixiReq.pixiObject && pixiReq.pixiObject._mb ? pixiReq.pixiObject._mb.properties || {} : {};
|
|
100
101
|
if (meta.content) properties.content = meta.content;
|
|
101
102
|
if (meta.fontSize) properties.fontSize = meta.fontSize;
|
|
103
|
+
// kind берём из самого объекта, если его не передали в payload — иначе safe-area
|
|
104
|
+
// фигуры считается как для квадрата и текст вылезает за контур круга/треугольника.
|
|
105
|
+
if (meta.kind && !properties.kind) properties.kind = meta.kind;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
// Уведомляем о начале редактирования (для разных типов отдельно)
|
|
@@ -355,27 +359,67 @@ export function openTextEditor(object, create = false) {
|
|
|
355
359
|
if (sizeData.size) { shapeW = sizeData.size.width; shapeH = sizeData.size.height; }
|
|
356
360
|
}
|
|
357
361
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
const
|
|
362
|
+
// Text-safe area: редактор занимает не весь bounds, а вписанный в контур
|
|
363
|
+
// фигуры прямоугольник — иначе строки при вводе вылезают за наклонные стороны.
|
|
364
|
+
const kindShape = properties?.kind || 'square';
|
|
365
|
+
const safeShape = getShapeTextSafeArea(kindShape, shapeW, shapeH);
|
|
366
|
+
const safeLeftCss = Math.round(safeShape.left * sCssShape);
|
|
367
|
+
const safeTopCss = Math.round(safeShape.top * sCssShape);
|
|
368
|
+
const shapeCssW = Math.max(1, Math.round(safeShape.width * sCssShape));
|
|
369
|
+
const shapeCssH = Math.max(1, Math.round(safeShape.height * sCssShape));
|
|
370
|
+
|
|
371
|
+
// Фактическая line-height textarea в px: читаем computed style, т.к. textarea
|
|
372
|
+
// уже вставлена в DOM (view.parentElement.appendChild выше). Это надёжнее, чем
|
|
373
|
+
// hardcode * 1.4 — реальный коэффициент зависит от baseFontSizePx через resolveLineHeightRatio.
|
|
374
|
+
let shapeLineHeightPx = Math.max(1, Math.round(effectiveFontPx * 1.4));
|
|
375
|
+
try {
|
|
376
|
+
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
|
377
|
+
const lhComputed = parseFloat(window.getComputedStyle(textarea).lineHeight);
|
|
378
|
+
if (isFinite(lhComputed) && lhComputed > 0) shapeLineHeightPx = Math.round(lhComputed);
|
|
379
|
+
}
|
|
380
|
+
} catch (_) {}
|
|
381
|
+
|
|
382
|
+
// backdrop: text center = shape center → paddingTop = (H - lineH) / 2
|
|
383
|
+
const backdropPadTop = Math.max(0, Math.round((shapeCssH - shapeLineHeightPx) / 2));
|
|
384
|
+
// textarea: компенсируем -2px смещение в updateCustomCaret (caretEl.style.top = top - 2),
|
|
385
|
+
// чтобы визуальная каретка тоже встала строго по центру фигуры
|
|
386
|
+
const textareaPadTop = backdropPadTop + 2;
|
|
363
387
|
|
|
364
|
-
wrapper.style.left = `${Math.round(screenPos.x)}px`;
|
|
365
|
-
wrapper.style.top = `${Math.round(screenPos.y)}px`;
|
|
388
|
+
wrapper.style.left = `${Math.round(screenPos.x) + safeLeftCss}px`;
|
|
389
|
+
wrapper.style.top = `${Math.round(screenPos.y) + safeTopCss}px`;
|
|
366
390
|
wrapper.style.width = `${shapeCssW}px`;
|
|
367
391
|
wrapper.style.height = `${shapeCssH}px`;
|
|
392
|
+
// Клип по контуру: при вводе текст, вышедший за наклонные стороны, прячется.
|
|
393
|
+
wrapper.style.clipPath = getShapeTextClipPath(kindShape) || 'none';
|
|
368
394
|
|
|
369
395
|
textarea.style.width = `${shapeCssW}px`;
|
|
370
396
|
textarea.style.height = `${shapeCssH}px`;
|
|
371
397
|
textarea.style.boxSizing = 'border-box';
|
|
398
|
+
// Перенос строк внутри фигуры (applyEditorSizing ставит white-space:pre — без
|
|
399
|
+
// переноса текст рос бы вбок и вылезал за границы). Совпадает со статическим
|
|
400
|
+
// слоем (mb-shape-text), поэтому строки в редакторе и после совпадают.
|
|
401
|
+
textarea.style.whiteSpace = 'pre-wrap';
|
|
402
|
+
textarea.style.wordBreak = 'break-word';
|
|
403
|
+
textarea.style.overflowWrap = 'break-word';
|
|
404
|
+
if (backdrop) {
|
|
405
|
+
backdrop.style.whiteSpace = 'pre-wrap';
|
|
406
|
+
backdrop.style.wordBreak = 'break-word';
|
|
407
|
+
backdrop.style.overflowWrap = 'break-word';
|
|
408
|
+
}
|
|
409
|
+
// CSS .moodboard-text-input задаёт transition: all 0.2s — он анимирует padding-top
|
|
410
|
+
// от старого значения к вычисленному. Начальный расчёт каретки (setTimeout 0 в
|
|
411
|
+
// bindTextEditorInteractions) читает промежуточный padding и ставит каретку у верха
|
|
412
|
+
// фигуры; центр появляется только после ввода. Отключаем анимацию, чтобы padding
|
|
413
|
+
// применился мгновенно и каретка сразу встала по центру.
|
|
414
|
+
textarea.style.transition = 'none';
|
|
372
415
|
|
|
373
416
|
if (backdrop) {
|
|
374
417
|
backdrop.style.width = `${shapeCssW}px`;
|
|
375
418
|
backdrop.style.height = `${shapeCssH}px`;
|
|
376
419
|
backdrop.style.boxSizing = 'border-box';
|
|
377
420
|
backdrop.style.display = 'block';
|
|
378
|
-
backdrop.style.
|
|
421
|
+
backdrop.style.transition = 'none';
|
|
422
|
+
backdrop.style.paddingTop = `${backdropPadTop}px`;
|
|
379
423
|
backdrop.style.paddingBottom = '0px';
|
|
380
424
|
}
|
|
381
425
|
|
|
@@ -385,9 +429,48 @@ export function openTextEditor(object, create = false) {
|
|
|
385
429
|
// padding-bottom:0 перебивает CSS .moodboard-text-input (5px), чтобы не раздувать
|
|
386
430
|
// высоту поля относительно крошечной фигуры.
|
|
387
431
|
textarea.style.display = 'block';
|
|
388
|
-
textarea.style.paddingTop = `${
|
|
432
|
+
textarea.style.paddingTop = `${textareaPadTop}px`;
|
|
389
433
|
textarea.style.paddingBottom = '0px';
|
|
390
434
|
|
|
435
|
+
// Вертикаль: пока текст помещается — центр по середине фигуры; при
|
|
436
|
+
// переполнении — рост вверх (низ/каретка видны, старые строки уходят выше).
|
|
437
|
+
// Пересчитываем paddingTop по фактической высоте контента, т.к. фиксированный
|
|
438
|
+
// padding центрирует только одну строку. Замер — по backdrop (видимый слой).
|
|
439
|
+
const applyShapeVerticalAlign = () => {
|
|
440
|
+
let contentH = shapeLineHeightPx;
|
|
441
|
+
if (backdrop) {
|
|
442
|
+
// Замер реальной высоты контента: у backdrop фиксированная height,
|
|
443
|
+
// поэтому scrollHeight не опускается ниже неё — временно снимаем height
|
|
444
|
+
// и padding, иначе одна строка «измеряется» как полный box и padding=0.
|
|
445
|
+
const prevPad = backdrop.style.paddingTop;
|
|
446
|
+
const prevH = backdrop.style.height;
|
|
447
|
+
backdrop.style.paddingTop = '0px';
|
|
448
|
+
backdrop.style.height = 'auto';
|
|
449
|
+
contentH = backdrop.scrollHeight;
|
|
450
|
+
backdrop.style.height = prevH;
|
|
451
|
+
backdrop.style.paddingTop = prevPad;
|
|
452
|
+
}
|
|
453
|
+
const overflow = contentH > shapeCssH + 1;
|
|
454
|
+
const bdPad = overflow ? 0 : Math.max(0, Math.round((shapeCssH - contentH) / 2));
|
|
455
|
+
if (backdrop) {
|
|
456
|
+
backdrop.style.paddingTop = `${bdPad}px`;
|
|
457
|
+
backdrop.scrollTop = overflow ? backdrop.scrollHeight : 0;
|
|
458
|
+
}
|
|
459
|
+
textarea.style.paddingTop = `${bdPad + 2}px`;
|
|
460
|
+
textarea.scrollTop = overflow ? textarea.scrollHeight : 0;
|
|
461
|
+
};
|
|
462
|
+
const scheduleShapeVerticalAlign = () => {
|
|
463
|
+
// rAF: даём syncBackdrop (input-листенер) обновить содержимое до замера.
|
|
464
|
+
requestAnimationFrame(() => {
|
|
465
|
+
applyShapeVerticalAlign();
|
|
466
|
+
if (this.textEditor && this.textEditor.caret) {
|
|
467
|
+
updateCustomCaret(textarea, this.textEditor.caret);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
textarea.addEventListener('input', scheduleShapeVerticalAlign);
|
|
472
|
+
scheduleShapeVerticalAlign();
|
|
473
|
+
|
|
391
474
|
this.textEditor._cssLeftPx = Math.round(screenPos.x);
|
|
392
475
|
this.textEditor._cssTopPx = Math.round(screenPos.y);
|
|
393
476
|
} else {
|
|
@@ -540,7 +623,11 @@ export function openTextEditor(object, create = false) {
|
|
|
540
623
|
if (!isNote && !isShape && create) {
|
|
541
624
|
autoSize();
|
|
542
625
|
}
|
|
543
|
-
|
|
626
|
+
// preventScroll: на планшете экранная клавиатура иначе триггерит нативный
|
|
627
|
+
// scroll-into-view, который прокручивает контейнер холста — фигура и поле
|
|
628
|
+
// ввода уезжают вниз от точки тапа. Позиция редактора уже привязана к
|
|
629
|
+
// координатам канваса, нативная прокрутка здесь только мешает.
|
|
630
|
+
textarea.focus({ preventScroll: true });
|
|
544
631
|
// Записка: после фокуса повторно подгоняем блок (focus может изменить layout),
|
|
545
632
|
// чтобы стартовая каретка считалась от уже подобранного размера шрифта, а не от 32px.
|
|
546
633
|
if (isNote && updateNoteEditor) {
|
package/src/ui/HtmlTextLayer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Events } from '../core/events/Events.js';
|
|
|
4
4
|
import * as PIXI from 'pixi.js';
|
|
5
5
|
import { renderRichText, hasMath } from '../utils/richText.js';
|
|
6
6
|
import { renderTextList } from './text-properties/TextListRenderer.js';
|
|
7
|
+
import { getShapeTextSafeArea, getShapeTextClipPath } from '../objects/shape/shapeTextArea.js';
|
|
7
8
|
import {
|
|
8
9
|
applyTextStyles,
|
|
9
10
|
resolveLineHeightRatio,
|
|
@@ -714,12 +715,46 @@ export class HtmlTextLayer {
|
|
|
714
715
|
if (el.dataset.baseFontSize !== String(newShapeFS)) {
|
|
715
716
|
el.dataset.baseFontSize = String(newShapeFS);
|
|
716
717
|
}
|
|
718
|
+
// Реальный размер применяется здесь: при смене шрифта в панели фигуры updateOne
|
|
719
|
+
// раньше обновлял только dataset, а style.fontSize оставался с момента создания.
|
|
720
|
+
el.style.fontSize = `${newShapeFS}px`;
|
|
717
721
|
el.style.fontFamily = textProps.fontFamily || 'Inter, sans-serif';
|
|
718
722
|
el.style.color = textProps.color || '#111111';
|
|
719
723
|
el.style.fontWeight = textProps.bold ? 'bold' : '';
|
|
720
724
|
el.style.fontStyle = textProps.italic ? 'italic' : '';
|
|
721
725
|
const shapeContent = obj.properties?.content || '';
|
|
722
726
|
if (el.textContent !== shapeContent) el.textContent = shapeContent;
|
|
727
|
+
// Text-safe area: оверлей занимает не весь bounds, а вписанный в контур
|
|
728
|
+
// фигуры прямоугольник (overflow: hidden клипует по нему). Иначе текст
|
|
729
|
+
// выходит за наклонные стороны треугольника/ромба/круга/стрелки.
|
|
730
|
+
if (w > 0 && h > 0) {
|
|
731
|
+
const kind = obj.properties?.kind || 'square';
|
|
732
|
+
const safe = getShapeTextSafeArea(kind, w, h);
|
|
733
|
+
const sx = logWidth / w;
|
|
734
|
+
const sy = logHeight / h;
|
|
735
|
+
const safeLeft = Math.round(logLeft + safe.left * sx);
|
|
736
|
+
const safeTop = Math.round(logTop + safe.top * sy);
|
|
737
|
+
const safeW = Math.max(1, Math.round(safe.width * sx));
|
|
738
|
+
const safeH = Math.max(1, Math.round(safe.height * sy));
|
|
739
|
+
el.style.left = `${safeLeft}px`;
|
|
740
|
+
el.style.top = `${safeTop}px`;
|
|
741
|
+
el.style.width = `${safeW}px`;
|
|
742
|
+
el.style.height = `${safeH}px`;
|
|
743
|
+
// Поворот должен идти вокруг центра фигуры, а не центра safe-area.
|
|
744
|
+
const originX = (logLeft + logWidth / 2) - safeLeft;
|
|
745
|
+
const originY = (logTop + logHeight / 2) - safeTop;
|
|
746
|
+
el.style.transformOrigin = `${originX}px ${originY}px`;
|
|
747
|
+
// Клип по контуру: текст, вышедший за наклонные стороны, прячется внутри.
|
|
748
|
+
el.style.clipPath = getShapeTextClipPath(kind) || 'none';
|
|
749
|
+
}
|
|
750
|
+
// Вертикальное выравнивание: пока текст помещается — центр по середине
|
|
751
|
+
// фигуры; при переполнении — прижимаем к низу safe-area, чтобы текст рос
|
|
752
|
+
// вверх (низ зафиксирован, верхние строки уходят выше и обрезаются
|
|
753
|
+
// overflow: hidden). Замер overflow делаем при flex-start — при
|
|
754
|
+
// align-items:center контент вылезает вверх/вниз и scrollHeight врёт.
|
|
755
|
+
el.style.alignItems = 'flex-start';
|
|
756
|
+
const overflowsBox = el.scrollHeight > el.clientHeight + 1;
|
|
757
|
+
el.style.alignItems = overflowsBox ? 'flex-end' : 'center';
|
|
723
758
|
return;
|
|
724
759
|
}
|
|
725
760
|
|