textElementLayout.ts
4.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/** 文本控件在元素框内的垂直对齐(config.verticalAlign) */
export type TextVerticalAlign = 'top' | 'center' | 'bottom';
export function readVerticalAlign(
config: Record<string, unknown> | undefined | null,
): TextVerticalAlign {
const raw = config?.verticalAlign ?? config?.VerticalAlign;
const v = String(raw ?? 'top').trim().toLowerCase();
if (v === 'center' || v === 'middle') return 'center';
if (v === 'bottom') return 'bottom';
return 'top';
}
export function verticalAlignToFlexJustify(
align: TextVerticalAlign,
): 'flex-start' | 'center' | 'flex-end' {
if (align === 'center') return 'center';
if (align === 'bottom') return 'flex-end';
return 'flex-start';
}
export function textAlignToFlexAlign(
align: string | undefined,
): 'flex-start' | 'center' | 'flex-end' {
const v = String(align ?? 'left').toLowerCase();
if (v === 'center') return 'center';
if (v === 'right') return 'flex-end';
return 'flex-start';
}
/** 画布/位图:文本块在元素框内的顶部 Y 偏移(不含 baseline) */
export function computeVerticalTextBlockOffset(
innerHeight: number,
blockHeight: number,
verticalAlign: TextVerticalAlign,
): number {
const extra = Math.max(0, innerHeight - blockHeight);
if (verticalAlign === 'center') return Math.floor(extra / 2);
if (verticalAlign === 'bottom') return extra;
return 0;
}
export function readFontWeight(
config: Record<string, unknown> | undefined | null,
): 'normal' | 'bold' {
const raw = config?.fontWeight ?? config?.FontWeight;
const v = String(raw ?? 'normal').trim().toLowerCase();
return v === 'bold' || v === '700' || v === 'bolder' ? 'bold' : 'normal';
}
export function readFontStyle(
config: Record<string, unknown> | undefined | null,
): 'normal' | 'italic' {
const raw = config?.fontStyle ?? config?.FontStyle;
const v = String(raw ?? 'normal').trim().toLowerCase();
return v === 'italic' || v === 'oblique' ? 'italic' : 'normal';
}
export function readTextDecoration(
config: Record<string, unknown> | undefined | null,
): 'none' | 'underline' {
const raw = config?.textDecoration ?? config?.TextDecoration;
const v = String(raw ?? 'none').trim().toLowerCase();
return v.includes('underline') ? 'underline' : 'none';
}
/** 元素级 border(兼容 Border / BorderType) */
export function readElementBorder(
el: { border?: string | null; Border?: string | null; BorderType?: string | null; borderType?: string | null },
): string {
const raw = el.border ?? el.Border ?? el.BorderType ?? el.borderType ?? 'none';
return String(raw).trim().toLowerCase() || 'none';
}
/** 读取元素 rotation(兼容大小写 / 旧数据) */
export function readElementRotation(
el: { rotation?: string | null },
): 'horizontal' | 'vertical' {
const v = String(el.rotation ?? 'horizontal').trim().toLowerCase();
return v === 'vertical' ? 'vertical' : 'horizontal';
}
/** 切换 horizontal / vertical 时交换宽高并保持中心点不变(与参考图 2 一致) */
export function patchElementRotationWithLayout(
el: { x: number; y: number; width: number; height: number; rotation?: string | null },
nextRotation: 'horizontal' | 'vertical',
): {
rotation: 'horizontal' | 'vertical';
x: number;
y: number;
width: number;
height: number;
} {
const prev = readElementRotation(el);
if (prev === nextRotation) {
// vertical 但宽>高:选框与文字方向不一致,强制纠正
if (nextRotation === 'vertical' && el.width > el.height) {
const w = Math.max(1, el.width);
const h = Math.max(1, el.height);
const cx = el.x + w / 2;
const cy = el.y + h / 2;
const newW = h;
const newH = w;
return {
rotation: nextRotation,
width: newW,
height: newH,
x: Math.round(cx - newW / 2),
y: Math.round(cy - newH / 2),
};
}
return {
rotation: nextRotation,
x: el.x,
y: el.y,
width: el.width,
height: el.height,
};
}
const w = Math.max(1, el.width);
const h = Math.max(1, el.height);
const cx = el.x + w / 2;
const cy = el.y + h / 2;
const newW = h;
const newH = w;
return {
rotation: nextRotation,
width: newW,
height: newH,
x: Math.round(cx - newW / 2),
y: Math.round(cy - newH / 2),
};
}
/** 竖排且宽>高时纠正选框(落库 / 渲染用) */
export function canonicalElementGeometry<
T extends { rotation?: string | null; width: number; height: number; x: number; y: number },
>(el: T): T {
return normalizeElementRotationBox(el);
}
/** 旧模板 vertical 但宽>高时自动纠正选框 */
export function normalizeElementRotationBox<
T extends { rotation?: string | null; width: number; height: number; x: number; y: number },
>(el: T): T {
if (readElementRotation(el) !== 'vertical') return el;
if (el.width <= el.height) return el;
return {
...el,
rotation: 'vertical',
...patchElementRotationWithLayout({ ...el, rotation: 'horizontal' }, 'vertical'),
};
}