@empty-sekai/sekai-custom-profile-sdk 0.3.0
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/LICENSE +235 -0
- package/LICENSE-EXCEPTION +32 -0
- package/NOTICE +11 -0
- package/README.en.md +456 -0
- package/README.md +458 -0
- package/dist/allium_renderer_wasm.js +14 -0
- package/dist/allium_renderer_wasm.wasm +0 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +34 -0
- package/dist/cache/glyphPersistentCache.d.ts +145 -0
- package/dist/cache/glyphPersistentCache.js +346 -0
- package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
- package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
- package/dist/cache/sessionImageResourceCache.d.ts +39 -0
- package/dist/cache/sessionImageResourceCache.js +156 -0
- package/dist/emscripten.d.ts +11 -0
- package/dist/emscripten.js +1 -0
- package/dist/fontProvider.d.ts +36 -0
- package/dist/fontProvider.js +84 -0
- package/dist/fontSdfAtlas.d.ts +92 -0
- package/dist/fontSdfAtlas.js +296 -0
- package/dist/gpu/browserSemanticResources.d.ts +64 -0
- package/dist/gpu/browserSemanticResources.js +232 -0
- package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
- package/dist/gpu/generalTextRenderPlacement.js +131 -0
- package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
- package/dist/gpu/previewTransformTextureLayout.js +12 -0
- package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
- package/dist/gpu/semanticCommandGeometry.js +226 -0
- package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
- package/dist/gpu/semanticCommandPlanner.js +203 -0
- package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
- package/dist/gpu/semanticTextGlyphBridge.js +22 -0
- package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
- package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
- package/dist/gpu/slotRanges.d.ts +5 -0
- package/dist/gpu/slotRanges.js +14 -0
- package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
- package/dist/gpu/webglSdfAtlasTexture.js +72 -0
- package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
- package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
- package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
- package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/interaction/numericTextRegions.d.ts +46 -0
- package/dist/interaction/numericTextRegions.js +44 -0
- package/dist/localizationProvider.d.ts +33 -0
- package/dist/localizationProvider.js +74 -0
- package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
- package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
- package/dist/prebuiltSdfAtlas.d.ts +36 -0
- package/dist/prebuiltSdfAtlas.js +261 -0
- package/dist/protocol.d.ts +456 -0
- package/dist/protocol.js +1 -0
- package/dist/renderer.d.ts +218 -0
- package/dist/renderer.js +641 -0
- package/dist/resourceProvider.d.ts +24 -0
- package/dist/resourceProvider.js +41 -0
- package/dist/telemetry/rendererTelemetry.d.ts +158 -0
- package/dist/telemetry/rendererTelemetry.js +232 -0
- package/dist/third-party/freetype/FTL.txt +169 -0
- package/dist/types/atlas.d.ts +90 -0
- package/dist/types/atlas.js +1 -0
- package/dist/types/authoring.d.ts +81 -0
- package/dist/types/authoring.js +1 -0
- package/dist/types/core.d.ts +128 -0
- package/dist/types/core.js +1 -0
- package/dist/types/freeType.d.ts +54 -0
- package/dist/types/freeType.js +13 -0
- package/dist/types/glyph.d.ts +33 -0
- package/dist/types/glyph.js +1 -0
- package/dist/types/layout.d.ts +25 -0
- package/dist/types/layout.js +1 -0
- package/dist/types/text.d.ts +28 -0
- package/dist/types/text.js +1 -0
- package/dist/worker/glyphWorkScheduler.d.ts +29 -0
- package/dist/worker/glyphWorkScheduler.js +107 -0
- package/dist/worker-client.d.ts +114 -0
- package/dist/worker-client.js +413 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +708 -0
- package/package.json +46 -0
- package/src/atlas.rs +652 -0
- package/src/authoring.ts +42 -0
- package/src/authoring_runtime.rs +303 -0
- package/src/cache/glyphPersistentCache.ts +463 -0
- package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
- package/src/cache/sessionImageResourceCache.ts +186 -0
- package/src/edt.rs +97 -0
- package/src/emscripten.ts +17 -0
- package/src/fontProvider.ts +127 -0
- package/src/fontSdfAtlas.ts +431 -0
- package/src/geometry.rs +149 -0
- package/src/glyph_plan.rs +226 -0
- package/src/gpu/browserSemanticResources.ts +294 -0
- package/src/gpu/generalTextRenderPlacement.ts +175 -0
- package/src/gpu/previewTransformTextureLayout.ts +12 -0
- package/src/gpu/semanticCommandGeometry.ts +260 -0
- package/src/gpu/semanticCommandPlanner.ts +280 -0
- package/src/gpu/semanticTextGlyphBridge.ts +35 -0
- package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
- package/src/gpu/slotRanges.ts +15 -0
- package/src/gpu/webglSdfAtlasTexture.ts +69 -0
- package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
- package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
- package/src/index.ts +84 -0
- package/src/interaction/numericTextRegions.ts +80 -0
- package/src/layout.rs +1882 -0
- package/src/lib.rs +1443 -0
- package/src/localizationProvider.ts +113 -0
- package/src/main.rs +55 -0
- package/src/masterdata_runtime.rs +496 -0
- package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
- package/src/prebuiltSdfAtlas.ts +306 -0
- package/src/protocol.ts +150 -0
- package/src/renderer.ts +792 -0
- package/src/resourceProvider.ts +69 -0
- package/src/scene.rs +665 -0
- package/src/telemetry/rendererTelemetry.ts +358 -0
- package/src/types/atlas.ts +80 -0
- package/src/types/authoring.ts +85 -0
- package/src/types/core.ts +110 -0
- package/src/types/freeType.ts +63 -0
- package/src/types/glyph.ts +33 -0
- package/src/types/layout.ts +28 -0
- package/src/types/text.ts +28 -0
- package/src/worker/glyphWorkScheduler.ts +130 -0
- package/src/worker-client.ts +484 -0
- package/src/worker.ts +895 -0
package/src/layout.rs
ADDED
|
@@ -0,0 +1,1882 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use std::collections::BTreeSet;
|
|
3
|
+
|
|
4
|
+
const TEXT_SCALE: f32 = 2.0;
|
|
5
|
+
const TMP_POINT_SIZE: f32 = 75.0;
|
|
6
|
+
const ASCENT_LINE: f32 = 66.0;
|
|
7
|
+
const DESCENT_LINE: f32 = -9.0;
|
|
8
|
+
const LINE_GAP: f32 = 150.0 - (66.0 + 9.0) + 0.625;
|
|
9
|
+
const PAD_ORIGINAL: f32 = 64.0 / TEXT_SCALE;
|
|
10
|
+
const TMP_SHADER_CLAMP: f32 = 1.0;
|
|
11
|
+
const GRADIENT_SCALE: f32 = 6.0;
|
|
12
|
+
const FACE_DILATE: f32 = 0.0;
|
|
13
|
+
const OUTLINE_WIDTH: f32 = 0.0;
|
|
14
|
+
const OUTLINE_SOFTNESS: f32 = 0.0;
|
|
15
|
+
const UNDERLAY_SOFTNESS: f32 = 0.0;
|
|
16
|
+
const WEIGHT_NORMAL: f32 = 0.0;
|
|
17
|
+
const WEIGHT_BOLD: f32 = 0.75;
|
|
18
|
+
const SHARPNESS: f32 = 0.0;
|
|
19
|
+
const RUNTIME_SCALE_RATIO_C: f32 = 0.6770833;
|
|
20
|
+
const RUNTIME_PIXEL_SCALE: f32 = 763.6753237;
|
|
21
|
+
|
|
22
|
+
pub fn build_layout_json(input: &str) -> Result<String, String> {
|
|
23
|
+
let request: LayoutRequest =
|
|
24
|
+
serde_json::from_str(input).map_err(|err| format!("parse layout json failed: {err}"))?;
|
|
25
|
+
let batch = build_layout(request);
|
|
26
|
+
serde_json::to_string(&batch).map_err(|err| format!("serialize layout json failed: {err}"))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn build_glyph_demand_json(input: &str) -> Result<String, String> {
|
|
30
|
+
let request: GlyphDemandRequest = serde_json::from_str(input)
|
|
31
|
+
.map_err(|err| format!("parse glyph-demand json failed: {err}"))?;
|
|
32
|
+
let mut seen = BTreeSet::new();
|
|
33
|
+
let mut requests = Vec::new();
|
|
34
|
+
for layer in request.layers {
|
|
35
|
+
for ch in glyph_demand_chars(&layer.text) {
|
|
36
|
+
let identity = (
|
|
37
|
+
layer.region.clone(),
|
|
38
|
+
layer.font_family.clone(),
|
|
39
|
+
layer.font_source_hash.clone(),
|
|
40
|
+
ch,
|
|
41
|
+
);
|
|
42
|
+
if seen.insert(identity) {
|
|
43
|
+
requests.push(GlyphDemandEntry {
|
|
44
|
+
region: layer.region.clone(),
|
|
45
|
+
family: layer.font_family.clone(),
|
|
46
|
+
font_source_hash: layer.font_source_hash.clone(),
|
|
47
|
+
ch: ch.to_string(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
serde_json::to_string(&GlyphDemandBatch {
|
|
53
|
+
version: 1,
|
|
54
|
+
source: "wasm-tmp-glyph-demand",
|
|
55
|
+
requests,
|
|
56
|
+
})
|
|
57
|
+
.map_err(|err| format!("serialize glyph-demand json failed: {err}"))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[derive(Deserialize)]
|
|
61
|
+
#[serde(rename_all = "camelCase")]
|
|
62
|
+
struct GlyphDemandRequest {
|
|
63
|
+
layers: Vec<GlyphDemandLayer>,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[derive(Deserialize)]
|
|
67
|
+
#[serde(rename_all = "camelCase")]
|
|
68
|
+
struct GlyphDemandLayer {
|
|
69
|
+
text: String,
|
|
70
|
+
region: String,
|
|
71
|
+
font_family: String,
|
|
72
|
+
font_source_hash: String,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[derive(Serialize)]
|
|
76
|
+
struct GlyphDemandBatch {
|
|
77
|
+
version: u32,
|
|
78
|
+
source: &'static str,
|
|
79
|
+
requests: Vec<GlyphDemandEntry>,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[derive(Serialize)]
|
|
83
|
+
struct GlyphDemandEntry {
|
|
84
|
+
region: String,
|
|
85
|
+
family: String,
|
|
86
|
+
font_source_hash: String,
|
|
87
|
+
#[serde(rename = "char")]
|
|
88
|
+
ch: String,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn build_layout(request: LayoutRequest) -> LayoutBatch {
|
|
92
|
+
let mut instances = Vec::new();
|
|
93
|
+
let mut dynamic_programs = Vec::new();
|
|
94
|
+
for layer in &request.layers {
|
|
95
|
+
let (layer_instances, dynamic_program) =
|
|
96
|
+
layout_layer(layer, &request.atlas, &request.atlas.glyphs);
|
|
97
|
+
instances.extend(layer_instances);
|
|
98
|
+
if let Some(program) = dynamic_program {
|
|
99
|
+
dynamic_programs.push(program);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
LayoutBatch {
|
|
103
|
+
version: 1,
|
|
104
|
+
source: "wasm-sdf-freetype-layout".to_string(),
|
|
105
|
+
instances,
|
|
106
|
+
dynamic_programs,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn layout_layer(
|
|
111
|
+
layer: &TextLayer,
|
|
112
|
+
atlas: &AtlasInput,
|
|
113
|
+
glyphs: &[GlyphInfo],
|
|
114
|
+
) -> (Vec<GlyphInstance>, Option<DynamicProgramDescriptor>) {
|
|
115
|
+
let segments = parse_rich_segments(&layer.text);
|
|
116
|
+
let dynamic_percent = line_indent_dynamic_percent(&layer.text, &segments);
|
|
117
|
+
let global = segments_to_global(&segments);
|
|
118
|
+
let mut clean = global.clean.clone();
|
|
119
|
+
if clean.ends_with('\n') {
|
|
120
|
+
clean.pop();
|
|
121
|
+
}
|
|
122
|
+
let line_texts = clean
|
|
123
|
+
.split('\n')
|
|
124
|
+
.map(|part| part.to_string())
|
|
125
|
+
.collect::<Vec<_>>();
|
|
126
|
+
let mut line_segs: Vec<Vec<TextSegment>> = vec![Vec::new()];
|
|
127
|
+
for seg in &segments {
|
|
128
|
+
for (idx, part) in seg.text.split('\n').enumerate() {
|
|
129
|
+
if idx > 0 {
|
|
130
|
+
line_segs.push(Vec::new());
|
|
131
|
+
}
|
|
132
|
+
if !part.is_empty() {
|
|
133
|
+
line_segs.last_mut().unwrap().push(seg.clone());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let seg_cleans = segments
|
|
139
|
+
.iter()
|
|
140
|
+
.map(|seg| {
|
|
141
|
+
seg.text
|
|
142
|
+
.chars()
|
|
143
|
+
.filter(|ch| *ch != '\n')
|
|
144
|
+
.collect::<Vec<_>>()
|
|
145
|
+
})
|
|
146
|
+
.collect::<Vec<_>>();
|
|
147
|
+
let mut measure_consumed = vec![0usize; segments.len()];
|
|
148
|
+
let mut line_widths = Vec::new();
|
|
149
|
+
let mut rect_widths = Vec::new();
|
|
150
|
+
let mut line_max_sizes = Vec::new();
|
|
151
|
+
let mut vbounds_max_top = f32::NEG_INFINITY;
|
|
152
|
+
let mut vbounds_min_bottom = f32::INFINITY;
|
|
153
|
+
let mut line_advances_tmp = Vec::new();
|
|
154
|
+
|
|
155
|
+
for line_text in &line_texts {
|
|
156
|
+
let mut current_line_advances_tmp = Vec::new();
|
|
157
|
+
let mut remaining = line_text.chars().collect::<Vec<_>>();
|
|
158
|
+
let mut w_scaled = 0.0;
|
|
159
|
+
let mut max_seg_size: f32 = 0.0;
|
|
160
|
+
let mut prev_cspace = None;
|
|
161
|
+
let mut cpv_xadv_tmp = 0.0;
|
|
162
|
+
let mut max_cpv_width_tmp = 0.0;
|
|
163
|
+
let mut has_chars = false;
|
|
164
|
+
let mut current_position: Option<Indent> = None;
|
|
165
|
+
let mut caret_position: Option<Indent> = None;
|
|
166
|
+
let mut caret_xadv_tmp = 0.0;
|
|
167
|
+
|
|
168
|
+
for (si, seg) in segments.iter().enumerate() {
|
|
169
|
+
if remaining.is_empty() {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if let Some(fixed) = seg.fixed_advance {
|
|
173
|
+
let seg_font_size = resolve_segment_font_size(&seg.size, layer.font_size);
|
|
174
|
+
if seg.position != current_position {
|
|
175
|
+
if let Some(pos_shift) = resolve_indent_value(&seg.position, seg_font_size, 0.0)
|
|
176
|
+
{
|
|
177
|
+
cpv_xadv_tmp = pos_shift * TEXT_SCALE;
|
|
178
|
+
caret_xadv_tmp = pos_shift * TEXT_SCALE;
|
|
179
|
+
}
|
|
180
|
+
current_position = seg.position.clone();
|
|
181
|
+
caret_position = seg.position.clone();
|
|
182
|
+
}
|
|
183
|
+
let adv = fixed / TEXT_SCALE;
|
|
184
|
+
w_scaled += adv;
|
|
185
|
+
cpv_xadv_tmp += fixed;
|
|
186
|
+
caret_xadv_tmp += fixed;
|
|
187
|
+
current_line_advances_tmp.push(fixed);
|
|
188
|
+
max_cpv_width_tmp = update_cpv_width(max_cpv_width_tmp, cpv_xadv_tmp, 0.0);
|
|
189
|
+
has_chars = true;
|
|
190
|
+
max_seg_size = max_seg_size.max(seg_font_size);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let sc = &seg_cleans[si];
|
|
195
|
+
if sc.is_empty() || measure_consumed[si] >= sc.len() {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
let seg_rest = &sc[measure_consumed[si]..];
|
|
199
|
+
let part: Vec<char>;
|
|
200
|
+
if starts_with_chars(&remaining, seg_rest) {
|
|
201
|
+
part = seg_rest.to_vec();
|
|
202
|
+
remaining.drain(..seg_rest.len());
|
|
203
|
+
measure_consumed[si] = sc.len();
|
|
204
|
+
} else if starts_with_chars(seg_rest, &remaining) {
|
|
205
|
+
part = remaining.clone();
|
|
206
|
+
measure_consumed[si] += remaining.len();
|
|
207
|
+
remaining.clear();
|
|
208
|
+
} else {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if part.is_empty() {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let seg_size = resolve_segment_font_size(&seg.size, layer.font_size);
|
|
216
|
+
if seg.position != current_position {
|
|
217
|
+
if let Some(pos_shift) = resolve_indent_value(&seg.position, seg_size, 0.0) {
|
|
218
|
+
cpv_xadv_tmp = pos_shift * TEXT_SCALE;
|
|
219
|
+
max_cpv_width_tmp = 0.0;
|
|
220
|
+
}
|
|
221
|
+
current_position = seg.position.clone();
|
|
222
|
+
}
|
|
223
|
+
if seg.position != caret_position {
|
|
224
|
+
if let Some(pos_shift) = resolve_indent_value(&seg.position, seg_size, 0.0) {
|
|
225
|
+
caret_xadv_tmp = pos_shift * TEXT_SCALE;
|
|
226
|
+
}
|
|
227
|
+
caret_position = seg.position.clone();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let measure_size = if seg.subscript || seg.superscript {
|
|
231
|
+
seg_size * 0.5
|
|
232
|
+
} else {
|
|
233
|
+
seg_size
|
|
234
|
+
};
|
|
235
|
+
let seg_scale = seg.scale.unwrap_or(1.0);
|
|
236
|
+
let cspace_raw_tmp = seg.cspace.unwrap_or(0.0);
|
|
237
|
+
let voffset_tmp = seg.voffset.unwrap_or(0.0);
|
|
238
|
+
let mut measured = 0.0;
|
|
239
|
+
let mut rendered_count = 0usize;
|
|
240
|
+
for raw_ch in part {
|
|
241
|
+
for (rendered_ch, char_scale) in transformed_glyphs(raw_ch, seg) {
|
|
242
|
+
let text = rendered_ch.to_string();
|
|
243
|
+
let glyph = if force_fallback_glyph(rendered_ch) {
|
|
244
|
+
None
|
|
245
|
+
} else {
|
|
246
|
+
glyph_for(layer, glyphs, &text)
|
|
247
|
+
};
|
|
248
|
+
let glyph_advance_tmp = glyph_advance(
|
|
249
|
+
glyph,
|
|
250
|
+
rendered_ch,
|
|
251
|
+
measure_size,
|
|
252
|
+
atlas.base_size,
|
|
253
|
+
&layer.font_family,
|
|
254
|
+
) * char_scale
|
|
255
|
+
* TEXT_SCALE;
|
|
256
|
+
measured += (glyph_advance_tmp * seg_scale) / TEXT_SCALE;
|
|
257
|
+
rendered_count += 1;
|
|
258
|
+
max_cpv_width_tmp = update_cpv_width_for_char(
|
|
259
|
+
max_cpv_width_tmp,
|
|
260
|
+
cpv_xadv_tmp,
|
|
261
|
+
glyph_advance_tmp,
|
|
262
|
+
raw_ch,
|
|
263
|
+
);
|
|
264
|
+
let glyph_advance_caret = glyph_advance_tmp * seg_scale;
|
|
265
|
+
current_line_advances_tmp.push(glyph_advance_caret + cspace_raw_tmp);
|
|
266
|
+
let glyph_asc_tmp = measure_size * (66.0 / 75.0) * TEXT_SCALE;
|
|
267
|
+
let glyph_des_tmp = measure_size * (9.0 / 75.0) * TEXT_SCALE;
|
|
268
|
+
vbounds_max_top = vbounds_max_top.max(voffset_tmp + glyph_asc_tmp);
|
|
269
|
+
vbounds_min_bottom = vbounds_min_bottom.min(voffset_tmp - glyph_des_tmp);
|
|
270
|
+
cpv_xadv_tmp += glyph_advance_tmp + cspace_raw_tmp;
|
|
271
|
+
caret_xadv_tmp += glyph_advance_caret + cspace_raw_tmp;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
let cspace = seg.cspace.unwrap_or(0.0) / TEXT_SCALE;
|
|
275
|
+
w_scaled += measured + cspace * rendered_count as f32;
|
|
276
|
+
has_chars = true;
|
|
277
|
+
prev_cspace = Some(cspace);
|
|
278
|
+
max_seg_size = max_seg_size.max(seg_size);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if !remaining.is_empty() {
|
|
282
|
+
for raw_ch in remaining {
|
|
283
|
+
let glyph = if force_fallback_glyph(raw_ch) {
|
|
284
|
+
None
|
|
285
|
+
} else {
|
|
286
|
+
glyph_for(layer, glyphs, &raw_ch.to_string())
|
|
287
|
+
};
|
|
288
|
+
let glyph_advance_tmp = glyph_advance(
|
|
289
|
+
glyph,
|
|
290
|
+
raw_ch,
|
|
291
|
+
layer.font_size,
|
|
292
|
+
atlas.base_size,
|
|
293
|
+
&layer.font_family,
|
|
294
|
+
) * TEXT_SCALE;
|
|
295
|
+
w_scaled += glyph_advance_tmp / TEXT_SCALE;
|
|
296
|
+
max_cpv_width_tmp = update_cpv_width_for_char(
|
|
297
|
+
max_cpv_width_tmp,
|
|
298
|
+
cpv_xadv_tmp,
|
|
299
|
+
glyph_advance_tmp,
|
|
300
|
+
raw_ch,
|
|
301
|
+
);
|
|
302
|
+
cpv_xadv_tmp += glyph_advance_tmp;
|
|
303
|
+
caret_xadv_tmp += glyph_advance_tmp;
|
|
304
|
+
current_line_advances_tmp.push(glyph_advance_tmp);
|
|
305
|
+
vbounds_max_top = vbounds_max_top.max(layer.font_size * (66.0 / 75.0) * TEXT_SCALE);
|
|
306
|
+
vbounds_min_bottom =
|
|
307
|
+
vbounds_min_bottom.min(-layer.font_size * (9.0 / 75.0) * TEXT_SCALE);
|
|
308
|
+
}
|
|
309
|
+
has_chars = true;
|
|
310
|
+
max_seg_size = max_seg_size.max(layer.font_size);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if max_seg_size < 0.001 {
|
|
314
|
+
let consumed_idx = measure_consumed.iter().rposition(|value| *value > 0);
|
|
315
|
+
let active = consumed_idx
|
|
316
|
+
.and_then(|idx| segments.get(idx))
|
|
317
|
+
.or_else(|| segments.first());
|
|
318
|
+
max_seg_size = active
|
|
319
|
+
.map(|seg| resolve_segment_font_size(&seg.size, layer.font_size))
|
|
320
|
+
.unwrap_or(layer.font_size);
|
|
321
|
+
}
|
|
322
|
+
if let Some(cspace) = prev_cspace {
|
|
323
|
+
w_scaled += cspace;
|
|
324
|
+
}
|
|
325
|
+
line_widths.push(w_scaled);
|
|
326
|
+
rect_widths.push(if has_chars {
|
|
327
|
+
max_cpv_width_tmp / TEXT_SCALE
|
|
328
|
+
} else {
|
|
329
|
+
0.0
|
|
330
|
+
});
|
|
331
|
+
line_max_sizes.push(max_seg_size);
|
|
332
|
+
line_advances_tmp.push(if line_text.chars().any(|ch| !ch.is_whitespace()) {
|
|
333
|
+
current_line_advances_tmp
|
|
334
|
+
} else {
|
|
335
|
+
Vec::new()
|
|
336
|
+
});
|
|
337
|
+
let _ = caret_xadv_tmp;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let mut line_asc = Vec::new();
|
|
341
|
+
let mut line_des = Vec::new();
|
|
342
|
+
for (i, max_size) in line_max_sizes.iter().copied().enumerate() {
|
|
343
|
+
let scale = (max_size / TMP_POINT_SIZE) * TEXT_SCALE;
|
|
344
|
+
let asc = scale * ASCENT_LINE;
|
|
345
|
+
let des = scale * DESCENT_LINE;
|
|
346
|
+
if i == 0 || max_size > 0.001 {
|
|
347
|
+
line_asc.push(asc);
|
|
348
|
+
line_des.push(des);
|
|
349
|
+
} else {
|
|
350
|
+
line_asc.push(*line_asc.last().unwrap_or(&asc));
|
|
351
|
+
line_des.push(*line_des.last().unwrap_or(&des));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let mut line_offsets = vec![0.0; line_max_sizes.len()];
|
|
356
|
+
let lh_override = segments.iter().find_map(|seg| seg.line_height);
|
|
357
|
+
let ls_tmp = layer.line_spacing * layer.font_size * TEXT_SCALE / TMP_POINT_SIZE;
|
|
358
|
+
for i in 1..line_offsets.len() {
|
|
359
|
+
let delta = if let Some(lh) = lh_override {
|
|
360
|
+
lh + ls_tmp
|
|
361
|
+
} else {
|
|
362
|
+
line_asc[i]
|
|
363
|
+
+ line_des[i - 1].abs()
|
|
364
|
+
+ LINE_GAP * ((layer.font_size / TMP_POINT_SIZE) * TEXT_SCALE)
|
|
365
|
+
+ ls_tmp
|
|
366
|
+
};
|
|
367
|
+
line_offsets[i] = line_offsets[i - 1] + delta;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let logical_max_asc = line_asc.first().copied().unwrap_or(0.0);
|
|
371
|
+
let logical_min_des =
|
|
372
|
+
line_des.last().copied().unwrap_or(0.0) - line_offsets.last().copied().unwrap_or(0.0);
|
|
373
|
+
let effective_max_asc = if lh_override.is_none() && vbounds_max_top.is_finite() {
|
|
374
|
+
logical_max_asc.max(vbounds_max_top)
|
|
375
|
+
} else {
|
|
376
|
+
logical_max_asc
|
|
377
|
+
};
|
|
378
|
+
let effective_min_des = if lh_override.is_none() && vbounds_min_bottom.is_finite() {
|
|
379
|
+
logical_min_des.min(vbounds_min_bottom)
|
|
380
|
+
} else {
|
|
381
|
+
logical_min_des
|
|
382
|
+
};
|
|
383
|
+
let anchor_base = (effective_max_asc + effective_min_des) / (2.0 * TEXT_SCALE);
|
|
384
|
+
let max_rw = rect_widths.iter().copied().fold(0.0, f32::max);
|
|
385
|
+
let box_w = max_rw + PAD_ORIGINAL;
|
|
386
|
+
let layout_metrics = LayoutMetrics {
|
|
387
|
+
line_widths,
|
|
388
|
+
rect_widths,
|
|
389
|
+
box_w,
|
|
390
|
+
anchor_base,
|
|
391
|
+
line_offsets,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
let mut render_consumed = vec![0usize; segments.len()];
|
|
395
|
+
let mut instances = Vec::new();
|
|
396
|
+
let mut plain_text_index = 0usize;
|
|
397
|
+
for (i, line_text) in line_texts.iter().enumerate() {
|
|
398
|
+
if i > 0 {
|
|
399
|
+
plain_text_index += 1;
|
|
400
|
+
}
|
|
401
|
+
let sw = *layout_metrics.line_widths.get(i).unwrap_or(&0.0);
|
|
402
|
+
let line_align = line_segs
|
|
403
|
+
.get(i)
|
|
404
|
+
.and_then(|list| list.first())
|
|
405
|
+
.and_then(|seg| seg.align.clone())
|
|
406
|
+
.or_else(|| global.align.clone());
|
|
407
|
+
let effective_align = match line_align.as_deref() {
|
|
408
|
+
Some("center") => 2,
|
|
409
|
+
Some("right") => 4,
|
|
410
|
+
_ => layer.text_type & 0x07,
|
|
411
|
+
};
|
|
412
|
+
let lx = if effective_align == 2 {
|
|
413
|
+
-sw / 2.0
|
|
414
|
+
} else if effective_align == 4 {
|
|
415
|
+
layout_metrics.box_w / 2.0 - sw
|
|
416
|
+
} else {
|
|
417
|
+
-layout_metrics.box_w / 2.0
|
|
418
|
+
};
|
|
419
|
+
let ly = layout_metrics.anchor_base
|
|
420
|
+
+ layout_metrics.line_offsets.get(i).copied().unwrap_or(0.0) / TEXT_SCALE;
|
|
421
|
+
let mut cursor_x = apply_line_indent(
|
|
422
|
+
lx,
|
|
423
|
+
sw,
|
|
424
|
+
layout_metrics.box_w,
|
|
425
|
+
effective_align,
|
|
426
|
+
line_segs.get(i).and_then(|list| list.first()),
|
|
427
|
+
);
|
|
428
|
+
let mut remaining = line_text.chars().collect::<Vec<_>>();
|
|
429
|
+
let mut current_position: Option<Indent> = None;
|
|
430
|
+
|
|
431
|
+
for (si, seg) in segments.iter().enumerate() {
|
|
432
|
+
if remaining.is_empty() {
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
let sc = &seg_cleans[si];
|
|
436
|
+
if sc.is_empty() || render_consumed[si] >= sc.len() {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
let seg_rest = &sc[render_consumed[si]..];
|
|
440
|
+
let part: Vec<char>;
|
|
441
|
+
if starts_with_chars(&remaining, seg_rest) {
|
|
442
|
+
part = seg_rest.to_vec();
|
|
443
|
+
remaining.drain(..seg_rest.len());
|
|
444
|
+
render_consumed[si] = sc.len();
|
|
445
|
+
} else if starts_with_chars(seg_rest, &remaining) {
|
|
446
|
+
part = remaining.clone();
|
|
447
|
+
render_consumed[si] += remaining.len();
|
|
448
|
+
remaining.clear();
|
|
449
|
+
} else {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if part.is_empty() {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let seg_size = resolve_segment_font_size(&seg.size, layer.font_size);
|
|
457
|
+
let seg_scale = seg.scale.unwrap_or(1.0);
|
|
458
|
+
if seg.position != current_position {
|
|
459
|
+
if let Some(pos_shift) =
|
|
460
|
+
resolve_indent_value(&seg.position, seg_size, layout_metrics.box_w)
|
|
461
|
+
{
|
|
462
|
+
cursor_x = lx + pos_shift;
|
|
463
|
+
}
|
|
464
|
+
current_position = seg.position.clone();
|
|
465
|
+
}
|
|
466
|
+
let render_size = if seg.subscript || seg.superscript {
|
|
467
|
+
seg_size * 0.5
|
|
468
|
+
} else {
|
|
469
|
+
seg_size
|
|
470
|
+
};
|
|
471
|
+
let mut baseline_shift = if seg.subscript {
|
|
472
|
+
0.15 * seg_size / TEXT_SCALE
|
|
473
|
+
} else if seg.superscript {
|
|
474
|
+
-0.35 * seg_size / TEXT_SCALE
|
|
475
|
+
} else {
|
|
476
|
+
0.0
|
|
477
|
+
};
|
|
478
|
+
if let Some(voffset) = seg.voffset {
|
|
479
|
+
baseline_shift = -voffset / TEXT_SCALE;
|
|
480
|
+
}
|
|
481
|
+
let cspace_px = seg.cspace.unwrap_or(0.0) / TEXT_SCALE;
|
|
482
|
+
let fill = resolve_fill(layer, seg, &global);
|
|
483
|
+
let outline = layer.outline_color;
|
|
484
|
+
|
|
485
|
+
for raw_ch in part {
|
|
486
|
+
for (rendered_ch, char_scale) in transformed_glyphs(raw_ch, seg) {
|
|
487
|
+
let text = rendered_ch.to_string();
|
|
488
|
+
let glyph = if force_fallback_glyph(rendered_ch) {
|
|
489
|
+
None
|
|
490
|
+
} else {
|
|
491
|
+
glyph_for(layer, glyphs, &text)
|
|
492
|
+
};
|
|
493
|
+
let effective_scale = seg_scale * char_scale;
|
|
494
|
+
let ft_scale = render_size / atlas.base_size;
|
|
495
|
+
let fallback_adv =
|
|
496
|
+
fallback_advance(rendered_ch, render_size, &layer.font_family);
|
|
497
|
+
let pivot_x = glyph
|
|
498
|
+
.map(|glyph| {
|
|
499
|
+
glyph.plane_bearing_x * ft_scale + glyph.plane_width * ft_scale / 2.0
|
|
500
|
+
})
|
|
501
|
+
.unwrap_or(fallback_adv / 2.0);
|
|
502
|
+
let pivot_y = glyph
|
|
503
|
+
.map(|glyph| {
|
|
504
|
+
-(glyph.plane_bearing_y * ft_scale
|
|
505
|
+
- glyph.plane_height * ft_scale / 2.0)
|
|
506
|
+
})
|
|
507
|
+
.unwrap_or(0.0);
|
|
508
|
+
let shear_cx = if let (true, Some(glyph)) = (seg.italic, glyph) {
|
|
509
|
+
0.35 * (glyph.plane_bearing_y - glyph.plane_height - atlas.spread)
|
|
510
|
+
* ft_scale
|
|
511
|
+
} else {
|
|
512
|
+
0.0
|
|
513
|
+
};
|
|
514
|
+
let mono_cell =
|
|
515
|
+
resolve_indent_value(&seg.monospace, seg_size, layout_metrics.box_w);
|
|
516
|
+
let draw_x = if let Some(cell) = mono_cell {
|
|
517
|
+
if cell > 0.0 {
|
|
518
|
+
cursor_x
|
|
519
|
+
+ if seg.duospace && matches_duo(rendered_ch) {
|
|
520
|
+
cell / 4.0
|
|
521
|
+
} else {
|
|
522
|
+
cell / 2.0
|
|
523
|
+
}
|
|
524
|
+
- pivot_x
|
|
525
|
+
} else {
|
|
526
|
+
cursor_x
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
cursor_x
|
|
530
|
+
};
|
|
531
|
+
let op = GlyphOp {
|
|
532
|
+
x: draw_x,
|
|
533
|
+
y: ly + baseline_shift,
|
|
534
|
+
pivot_x,
|
|
535
|
+
pivot_y,
|
|
536
|
+
shear_cx,
|
|
537
|
+
scale_x: effective_scale,
|
|
538
|
+
skew_x: if seg.italic { -0.21 } else { 0.0 },
|
|
539
|
+
rotate_deg: seg.rotate.unwrap_or(0.0),
|
|
540
|
+
};
|
|
541
|
+
instances.push(make_instance(
|
|
542
|
+
layer,
|
|
543
|
+
plain_text_index,
|
|
544
|
+
atlas,
|
|
545
|
+
glyph,
|
|
546
|
+
&text,
|
|
547
|
+
op,
|
|
548
|
+
fill,
|
|
549
|
+
outline,
|
|
550
|
+
layout_metrics.clone(),
|
|
551
|
+
render_size,
|
|
552
|
+
compute_sdf_shader_params(
|
|
553
|
+
render_size,
|
|
554
|
+
seg.bold,
|
|
555
|
+
layer.outline_width,
|
|
556
|
+
effective_vertex_alpha(seg.alpha.or(global.alpha), layer.color[3]),
|
|
557
|
+
),
|
|
558
|
+
));
|
|
559
|
+
if let Some(cell) = mono_cell {
|
|
560
|
+
if cell > 0.0 {
|
|
561
|
+
cursor_x += if seg.duospace && matches_duo(rendered_ch) {
|
|
562
|
+
cell / 2.0
|
|
563
|
+
} else {
|
|
564
|
+
cell
|
|
565
|
+
} + cspace_px;
|
|
566
|
+
} else {
|
|
567
|
+
cursor_x +=
|
|
568
|
+
(glyph.map(|g| g.advance * ft_scale).unwrap_or(fallback_adv))
|
|
569
|
+
* effective_scale
|
|
570
|
+
+ cspace_px;
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
cursor_x += (glyph.map(|g| g.advance * ft_scale).unwrap_or(fallback_adv))
|
|
574
|
+
* effective_scale
|
|
575
|
+
+ cspace_px;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
plain_text_index += 1;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let base_matrix = layer_base_matrix(layer);
|
|
584
|
+
let (dynamic_rotation_deg, dynamic_scale_x) = if layer.transform_matrix.is_some() {
|
|
585
|
+
(
|
|
586
|
+
base_matrix[1].atan2(base_matrix[0]).to_degrees(),
|
|
587
|
+
base_matrix[0].hypot(base_matrix[1]),
|
|
588
|
+
)
|
|
589
|
+
} else {
|
|
590
|
+
(layer.rotation_deg, layer.scale_x)
|
|
591
|
+
};
|
|
592
|
+
let dynamic_program = dynamic_percent.map(|percent| DynamicProgramDescriptor {
|
|
593
|
+
layer_id: layer
|
|
594
|
+
.dynamic_layer_id
|
|
595
|
+
.clone()
|
|
596
|
+
.unwrap_or_else(|| layer.id.clone()),
|
|
597
|
+
percent,
|
|
598
|
+
line_advances_tmp,
|
|
599
|
+
rotation_deg: dynamic_rotation_deg,
|
|
600
|
+
scale_x: dynamic_scale_x,
|
|
601
|
+
});
|
|
602
|
+
(instances, dynamic_program)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
fn make_instance(
|
|
606
|
+
layer: &TextLayer,
|
|
607
|
+
plain_text_index: usize,
|
|
608
|
+
atlas: &AtlasInput,
|
|
609
|
+
glyph: Option<&GlyphInfo>,
|
|
610
|
+
ch: &str,
|
|
611
|
+
op: GlyphOp,
|
|
612
|
+
fill: [f32; 4],
|
|
613
|
+
outline: [f32; 4],
|
|
614
|
+
layout_metrics: LayoutMetrics,
|
|
615
|
+
render_size: f32,
|
|
616
|
+
shader_params: SdfShaderParams,
|
|
617
|
+
) -> GlyphInstance {
|
|
618
|
+
let glyph_scale = render_size / atlas.base_size;
|
|
619
|
+
let mut local = Vec::<[f32; 4]>::new();
|
|
620
|
+
if let Some(glyph) = glyph {
|
|
621
|
+
if glyph.drawable {
|
|
622
|
+
let local_left = (glyph.plane_bearing_x - atlas.spread) * glyph_scale - op.pivot_x;
|
|
623
|
+
let local_top = -(glyph.plane_bearing_y + atlas.spread) * glyph_scale - op.pivot_y;
|
|
624
|
+
let local_right = (glyph.plane_bearing_x + glyph.plane_width + atlas.spread)
|
|
625
|
+
* glyph_scale
|
|
626
|
+
- op.pivot_x;
|
|
627
|
+
let local_bottom = (-glyph.plane_bearing_y + glyph.plane_height + atlas.spread)
|
|
628
|
+
* glyph_scale
|
|
629
|
+
- op.pivot_y;
|
|
630
|
+
local.push([local_left, local_top, glyph.u0, glyph.v0]);
|
|
631
|
+
local.push([local_right, local_top, glyph.u1, glyph.v0]);
|
|
632
|
+
local.push([local_right, local_bottom, glyph.u1, glyph.v1]);
|
|
633
|
+
local.push([local_left, local_bottom, glyph.u0, glyph.v1]);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let glyph_matrix = multiply(
|
|
638
|
+
translate(op.x + op.pivot_x + op.shear_cx, op.y + op.pivot_y),
|
|
639
|
+
multiply(
|
|
640
|
+
rotate(-op.rotate_deg),
|
|
641
|
+
multiply(scale(op.scale_x, 1.0), skew(op.skew_x)),
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
let text_scale = scale(TEXT_SCALE, TEXT_SCALE);
|
|
645
|
+
let outer = layer_base_matrix(layer);
|
|
646
|
+
let matrix = multiply(outer, multiply(text_scale, glyph_matrix));
|
|
647
|
+
let quad = local
|
|
648
|
+
.iter()
|
|
649
|
+
.map(|[x, y, u, v]| {
|
|
650
|
+
let [px, py] = apply(matrix, *x, *y);
|
|
651
|
+
[px, py, *u, *v, atlas.spread * glyph_scale]
|
|
652
|
+
})
|
|
653
|
+
.collect::<Vec<_>>();
|
|
654
|
+
|
|
655
|
+
let hx = op.pivot_x.abs().max(1.0);
|
|
656
|
+
let hy = op.pivot_y.abs().max(1.0);
|
|
657
|
+
let footprint_local = [[-hx, -hy], [hx, -hy], [hx, hy], [-hx, hy]];
|
|
658
|
+
let char_quad = footprint_local
|
|
659
|
+
.iter()
|
|
660
|
+
.map(|[x, y]| {
|
|
661
|
+
let [px, py] = apply(glyph_matrix, *x, *y);
|
|
662
|
+
[px * TEXT_SCALE, -py * TEXT_SCALE]
|
|
663
|
+
})
|
|
664
|
+
.collect::<Vec<_>>();
|
|
665
|
+
let device_char_quad = footprint_local
|
|
666
|
+
.iter()
|
|
667
|
+
.map(|[x, y]| apply(matrix, *x, *y))
|
|
668
|
+
.collect::<Vec<_>>();
|
|
669
|
+
let [device_cx, device_cy] = apply(
|
|
670
|
+
outer,
|
|
671
|
+
(op.x + op.pivot_x + op.shear_cx) * TEXT_SCALE,
|
|
672
|
+
(op.y + op.pivot_y) * TEXT_SCALE,
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
GlyphInstance {
|
|
676
|
+
layer_id: layer.id.clone(),
|
|
677
|
+
plain_text_index,
|
|
678
|
+
char_value: ch.to_string(),
|
|
679
|
+
drawable: glyph.is_some(),
|
|
680
|
+
glyph_key: glyph.map(|glyph| glyph.key.clone()).unwrap_or_default(),
|
|
681
|
+
atlas_page: glyph.map(|glyph| glyph.page).unwrap_or(0),
|
|
682
|
+
z: layer.z,
|
|
683
|
+
quad: quad.clone(),
|
|
684
|
+
char_position: (
|
|
685
|
+
ch.to_string(),
|
|
686
|
+
(op.x + op.pivot_x + op.shear_cx) * TEXT_SCALE,
|
|
687
|
+
-(op.y + op.pivot_y) * TEXT_SCALE,
|
|
688
|
+
op.scale_x,
|
|
689
|
+
op.skew_x,
|
|
690
|
+
op.pivot_x,
|
|
691
|
+
),
|
|
692
|
+
char_op: (
|
|
693
|
+
ch.to_string(),
|
|
694
|
+
op.x,
|
|
695
|
+
op.y,
|
|
696
|
+
op.scale_x,
|
|
697
|
+
op.pivot_x,
|
|
698
|
+
op.pivot_y,
|
|
699
|
+
op.rotate_deg,
|
|
700
|
+
),
|
|
701
|
+
char_quad: (ch.to_string(), char_quad),
|
|
702
|
+
device_char_position: (ch.to_string(), device_cx, device_cy),
|
|
703
|
+
device_char_quad: (ch.to_string(), device_char_quad),
|
|
704
|
+
device_glyph_quad: (
|
|
705
|
+
ch.to_string(),
|
|
706
|
+
quad.iter().map(|row| [row[0], row[1]]).collect(),
|
|
707
|
+
),
|
|
708
|
+
layout_metrics,
|
|
709
|
+
fill,
|
|
710
|
+
outline: if layer.outline_width > 0.0 {
|
|
711
|
+
outline
|
|
712
|
+
} else {
|
|
713
|
+
[0.0, 0.0, 0.0, 0.0]
|
|
714
|
+
},
|
|
715
|
+
outline_width: layer.outline_width,
|
|
716
|
+
shader_font_size: render_size,
|
|
717
|
+
shader_face_scale: shader_params.face_scale,
|
|
718
|
+
shader_face_bias: shader_params.face_bias,
|
|
719
|
+
shader_underlay_scale: shader_params.underlay_scale,
|
|
720
|
+
shader_underlay_bias: shader_params.underlay_bias,
|
|
721
|
+
shader_vertex_alpha: shader_params.vertex_alpha,
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
fn compute_sdf_shader_params(
|
|
726
|
+
point_size: f32,
|
|
727
|
+
is_bold: bool,
|
|
728
|
+
outline_size: f32,
|
|
729
|
+
vertex_alpha: f32,
|
|
730
|
+
) -> SdfShaderParams {
|
|
731
|
+
let uv2_y = runtime_uv2_y(point_size, is_bold);
|
|
732
|
+
let shader_scale = compute_shader_scale(uv2_y);
|
|
733
|
+
let ratio_weight_dilate = WEIGHT_NORMAL.max(WEIGHT_BOLD) * 0.25;
|
|
734
|
+
let selected_weight_dilate = if uv2_y <= 0.0 {
|
|
735
|
+
WEIGHT_BOLD
|
|
736
|
+
} else {
|
|
737
|
+
WEIGHT_NORMAL
|
|
738
|
+
} * 0.25;
|
|
739
|
+
let ratio_face_dilate = FACE_DILATE + ratio_weight_dilate;
|
|
740
|
+
let selected_face_dilate = FACE_DILATE + selected_weight_dilate;
|
|
741
|
+
let face_denom = (OUTLINE_SOFTNESS + OUTLINE_WIDTH + ratio_face_dilate).max(1.0);
|
|
742
|
+
let scale_ratio_a =
|
|
743
|
+
((GRADIENT_SCALE - TMP_SHADER_CLAMP) / (GRADIENT_SCALE * face_denom)).max(0.0);
|
|
744
|
+
let face_softness = OUTLINE_SOFTNESS * scale_ratio_a;
|
|
745
|
+
let face_scale = shader_scale / (1.0 + face_softness * shader_scale);
|
|
746
|
+
let face_base = 0.5 - selected_face_dilate * scale_ratio_a * 0.5;
|
|
747
|
+
let face_bias = face_base * face_scale - 0.5;
|
|
748
|
+
|
|
749
|
+
let underlay_softness = UNDERLAY_SOFTNESS * RUNTIME_SCALE_RATIO_C;
|
|
750
|
+
let underlay_scale = shader_scale / (1.0 + underlay_softness * shader_scale);
|
|
751
|
+
let underlay_bias = face_base * underlay_scale
|
|
752
|
+
- 0.5
|
|
753
|
+
- (outline_size.max(0.0) * RUNTIME_SCALE_RATIO_C) * underlay_scale * 0.5;
|
|
754
|
+
|
|
755
|
+
SdfShaderParams {
|
|
756
|
+
face_scale,
|
|
757
|
+
face_bias,
|
|
758
|
+
underlay_scale,
|
|
759
|
+
underlay_bias,
|
|
760
|
+
vertex_alpha,
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
fn runtime_uv2_y(point_size: f32, is_bold: bool) -> f32 {
|
|
765
|
+
let mag = (point_size.abs() / 20250.0).max(1e-8);
|
|
766
|
+
if is_bold {
|
|
767
|
+
-mag
|
|
768
|
+
} else {
|
|
769
|
+
mag
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fn compute_shader_scale(uv2_y: f32) -> f32 {
|
|
774
|
+
let shader_scale = uv2_y.abs() * RUNTIME_PIXEL_SCALE * GRADIENT_SCALE * (SHARPNESS + 1.0);
|
|
775
|
+
if shader_scale.is_finite() && shader_scale > 0.0001 {
|
|
776
|
+
shader_scale
|
|
777
|
+
} else {
|
|
778
|
+
0.0001
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
fn effective_vertex_alpha(alpha_override: Option<f32>, base_alpha: f32) -> f32 {
|
|
783
|
+
let base_u8 = (base_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
|
|
784
|
+
let alpha_u8 = alpha_override
|
|
785
|
+
.map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8)
|
|
786
|
+
.map(|alpha| alpha.min(base_u8))
|
|
787
|
+
.unwrap_or(base_u8);
|
|
788
|
+
alpha_u8 as f32 / 255.0
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
fn glyph_for<'a>(layer: &TextLayer, glyphs: &'a [GlyphInfo], text: &str) -> Option<&'a GlyphInfo> {
|
|
792
|
+
let key = format!(
|
|
793
|
+
"{}\u{0}{}\u{0}{}\u{0}{}",
|
|
794
|
+
layer.region, layer.font_source_hash, layer.font_family, text
|
|
795
|
+
);
|
|
796
|
+
glyphs.iter().find(|glyph| glyph.key == key)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
fn glyph_advance(
|
|
800
|
+
glyph: Option<&GlyphInfo>,
|
|
801
|
+
ch: char,
|
|
802
|
+
font_size: f32,
|
|
803
|
+
base_size: f32,
|
|
804
|
+
family: &str,
|
|
805
|
+
) -> f32 {
|
|
806
|
+
glyph
|
|
807
|
+
.map(|glyph| glyph.advance * (font_size / base_size))
|
|
808
|
+
.unwrap_or_else(|| fallback_advance(ch, font_size, family))
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
fn fallback_advance(ch: char, font_size: f32, family: &str) -> f32 {
|
|
812
|
+
if ch == '\u{00a0}' {
|
|
813
|
+
return font_size;
|
|
814
|
+
}
|
|
815
|
+
if ch == ' ' {
|
|
816
|
+
return (font_size * space_advance_ratio(family)).round();
|
|
817
|
+
}
|
|
818
|
+
if is_fullwidth(ch) {
|
|
819
|
+
font_size
|
|
820
|
+
} else {
|
|
821
|
+
font_size * 0.5
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
fn force_fallback_glyph(ch: char) -> bool {
|
|
826
|
+
ch == ' ' || ch == '\u{00a0}'
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
fn space_advance_ratio(family: &str) -> f32 {
|
|
830
|
+
if family.contains("FZShaoEr") {
|
|
831
|
+
return 0.25;
|
|
832
|
+
}
|
|
833
|
+
if family.contains("FZZhengHei") || family.contains("SkipPro") {
|
|
834
|
+
4.0 / 15.0
|
|
835
|
+
} else {
|
|
836
|
+
5.0 / 24.0
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
fn is_fullwidth(ch: char) -> bool {
|
|
841
|
+
let cp = ch as u32;
|
|
842
|
+
matches!(
|
|
843
|
+
cp,
|
|
844
|
+
0x2000..=0x206f
|
|
845
|
+
| 0x2190..=0x21ff
|
|
846
|
+
| 0x2200..=0x22ff
|
|
847
|
+
| 0x2300..=0x23ff
|
|
848
|
+
| 0x2460..=0x24ff
|
|
849
|
+
| 0x2500..=0x259f
|
|
850
|
+
| 0x25a0..=0x25ff
|
|
851
|
+
| 0x2600..=0x26ff
|
|
852
|
+
| 0x2700..=0x27bf
|
|
853
|
+
| 0x3000..=0x30ff
|
|
854
|
+
| 0x3400..=0x4dbf
|
|
855
|
+
| 0x4e00..=0x9fff
|
|
856
|
+
| 0xf900..=0xfaff
|
|
857
|
+
| 0xfe30..=0xfe4f
|
|
858
|
+
| 0xff01..=0xff60
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
fn parse_rich_segments(raw: &str) -> Vec<TextSegment> {
|
|
863
|
+
let mut state = ParseState::default();
|
|
864
|
+
let chars = raw.chars().collect::<Vec<_>>();
|
|
865
|
+
let mut i = 0usize;
|
|
866
|
+
while i < chars.len() {
|
|
867
|
+
if state.noparse_depth > 0 {
|
|
868
|
+
if chars[i] == '<' {
|
|
869
|
+
if let Some(end) = find_gt(&chars, i) {
|
|
870
|
+
let tag = chars[i + 1..end].iter().collect::<String>().to_lowercase();
|
|
871
|
+
if tag == "/noparse" && handle_tag(&mut state, &tag) {
|
|
872
|
+
i = end + 1;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
append_char(&mut state, chars[i]);
|
|
878
|
+
i += 1;
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if chars[i] == '<' {
|
|
882
|
+
if let Some(end) = find_gt(&chars, i) {
|
|
883
|
+
let tag = chars[i + 1..end].iter().collect::<String>().to_lowercase();
|
|
884
|
+
if handle_tag(&mut state, &tag) {
|
|
885
|
+
i = end + 1;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
append_char(&mut state, chars[i]);
|
|
891
|
+
i += 1;
|
|
892
|
+
}
|
|
893
|
+
state.segs
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
fn find_gt(chars: &[char], start: usize) -> Option<usize> {
|
|
897
|
+
chars
|
|
898
|
+
.iter()
|
|
899
|
+
.enumerate()
|
|
900
|
+
.skip(start)
|
|
901
|
+
.find_map(|(idx, ch)| (*ch == '>').then_some(idx))
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
#[derive(Default)]
|
|
905
|
+
struct ParseState {
|
|
906
|
+
segs: Vec<TextSegment>,
|
|
907
|
+
color_stack: Vec<ColorFrame>,
|
|
908
|
+
current_color: Option<[f32; 3]>,
|
|
909
|
+
size_stack: Vec<SizeSpec>,
|
|
910
|
+
scale_stack: Vec<f32>,
|
|
911
|
+
alpha_override: Option<f32>,
|
|
912
|
+
bold_depth: i32,
|
|
913
|
+
italic_depth: i32,
|
|
914
|
+
underline_depth: i32,
|
|
915
|
+
strikethrough_depth: i32,
|
|
916
|
+
subscript_depth: i32,
|
|
917
|
+
superscript_depth: i32,
|
|
918
|
+
mark_stack: Vec<[f32; 4]>,
|
|
919
|
+
case_stack: Vec<CaseTransform>,
|
|
920
|
+
smallcaps_depth: i32,
|
|
921
|
+
noparse_depth: i32,
|
|
922
|
+
voffset_stack: Vec<f32>,
|
|
923
|
+
rotate_stack: Vec<f32>,
|
|
924
|
+
cspace_override: Option<f32>,
|
|
925
|
+
line_height_override: Option<f32>,
|
|
926
|
+
line_indent_override: Option<LineIndent>,
|
|
927
|
+
indent_stack: Vec<Indent>,
|
|
928
|
+
position_stack: Vec<Indent>,
|
|
929
|
+
monospace_stack: Vec<Indent>,
|
|
930
|
+
duospace_stack: Vec<bool>,
|
|
931
|
+
align_stack: Vec<String>,
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
#[derive(Clone)]
|
|
935
|
+
struct ColorFrame {
|
|
936
|
+
color: Option<[f32; 3]>,
|
|
937
|
+
alpha: Option<f32>,
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
fn build_segment(state: &ParseState, text: String, fixed_advance: Option<f32>) -> TextSegment {
|
|
941
|
+
TextSegment {
|
|
942
|
+
text,
|
|
943
|
+
fixed_advance,
|
|
944
|
+
color: state.current_color,
|
|
945
|
+
size: state.size_stack.last().cloned(),
|
|
946
|
+
scale: state.scale_stack.last().copied(),
|
|
947
|
+
alpha: state.alpha_override,
|
|
948
|
+
bold: state.bold_depth > 0,
|
|
949
|
+
italic: state.italic_depth > 0,
|
|
950
|
+
underline: state.underline_depth > 0,
|
|
951
|
+
strikethrough: state.strikethrough_depth > 0,
|
|
952
|
+
mark_color: state.mark_stack.last().copied(),
|
|
953
|
+
superscript: state.superscript_depth > 0,
|
|
954
|
+
subscript: state.subscript_depth > 0,
|
|
955
|
+
case_transform: state
|
|
956
|
+
.case_stack
|
|
957
|
+
.last()
|
|
958
|
+
.cloned()
|
|
959
|
+
.unwrap_or(CaseTransform::None),
|
|
960
|
+
smallcaps: state.smallcaps_depth > 0,
|
|
961
|
+
voffset: state.voffset_stack.last().copied(),
|
|
962
|
+
rotate: state.rotate_stack.last().copied(),
|
|
963
|
+
cspace: state.cspace_override,
|
|
964
|
+
line_height: state.line_height_override,
|
|
965
|
+
line_indent: state.line_indent_override.clone(),
|
|
966
|
+
indent: state.indent_stack.last().cloned(),
|
|
967
|
+
position: state.position_stack.last().cloned(),
|
|
968
|
+
monospace: state.monospace_stack.last().cloned(),
|
|
969
|
+
duospace: state.duospace_stack.last().copied().unwrap_or(false),
|
|
970
|
+
align: state.align_stack.last().cloned(),
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
fn append_char(state: &mut ParseState, ch: char) {
|
|
975
|
+
let seg = build_segment(state, ch.to_string(), None);
|
|
976
|
+
if let Some(last) = state.segs.last_mut() {
|
|
977
|
+
if can_merge(last, &seg) {
|
|
978
|
+
last.text.push(ch);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
state.segs.push(seg);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
fn push_text(state: &mut ParseState, text: &str) {
|
|
986
|
+
state
|
|
987
|
+
.segs
|
|
988
|
+
.push(build_segment(state, text.to_string(), None));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
fn handle_tag(state: &mut ParseState, tag: &str) -> bool {
|
|
992
|
+
if let Some(rest) = tag.strip_prefix("color=#") {
|
|
993
|
+
return push_color(state, rest);
|
|
994
|
+
}
|
|
995
|
+
if let Some(rest) = tag.strip_prefix('#') {
|
|
996
|
+
return push_color(state, rest);
|
|
997
|
+
}
|
|
998
|
+
if tag == "/color" {
|
|
999
|
+
if state.color_stack.len() > 1 {
|
|
1000
|
+
state.color_stack.pop();
|
|
1001
|
+
}
|
|
1002
|
+
let prev = state.color_stack.last().cloned().unwrap_or(ColorFrame {
|
|
1003
|
+
color: None,
|
|
1004
|
+
alpha: None,
|
|
1005
|
+
});
|
|
1006
|
+
state.current_color = prev.color;
|
|
1007
|
+
state.alpha_override = prev.alpha;
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
if let Some(rest) = tag.strip_prefix("size=") {
|
|
1011
|
+
if let Some(parsed) = parse_size(rest) {
|
|
1012
|
+
state.size_stack.push(parsed);
|
|
1013
|
+
}
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
if tag == "/size" {
|
|
1017
|
+
state.size_stack.pop();
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
if let Some(rest) = tag.strip_prefix("scale=") {
|
|
1021
|
+
if let Some(value) = parse_loose(&rest.replace('#', "")) {
|
|
1022
|
+
state.scale_stack.push(value);
|
|
1023
|
+
}
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
if tag == "/scale" {
|
|
1027
|
+
state.scale_stack.pop();
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
if tag == "br" || tag == "cr" {
|
|
1031
|
+
push_text(state, "\n");
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
if tag == "nbsp" {
|
|
1035
|
+
append_char(state, '\u{00a0}');
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
if let Some(rest) = tag.strip_prefix("space=") {
|
|
1039
|
+
if let Some(value) = parse_loose(&rest.replace('#', "")) {
|
|
1040
|
+
state
|
|
1041
|
+
.segs
|
|
1042
|
+
.push(build_segment(state, String::new(), Some(value)));
|
|
1043
|
+
}
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
if let Some(rest) = tag.strip_prefix("alpha=") {
|
|
1047
|
+
let hex = rest.replace('#', "");
|
|
1048
|
+
let slice = &hex[..hex.len().min(2)];
|
|
1049
|
+
state.alpha_override = u8::from_str_radix(if slice.is_empty() { "ff" } else { slice }, 16)
|
|
1050
|
+
.ok()
|
|
1051
|
+
.map(|value| value as f32 / 255.0);
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
if let Some(rest) = tag.strip_prefix("voffset=") {
|
|
1055
|
+
return push_number(&mut state.voffset_stack, rest);
|
|
1056
|
+
}
|
|
1057
|
+
if tag == "/voffset" {
|
|
1058
|
+
state.voffset_stack.pop();
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
if let Some(rest) = tag.strip_prefix("rotate=") {
|
|
1062
|
+
return push_number(&mut state.rotate_stack, rest);
|
|
1063
|
+
}
|
|
1064
|
+
if tag == "/rotate" {
|
|
1065
|
+
state.rotate_stack.pop();
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
if let Some(rest) = tag.strip_prefix("cspace=") {
|
|
1069
|
+
state.cspace_override = parse_loose(&rest.replace('#', ""));
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
if tag == "/cspace" {
|
|
1073
|
+
state.cspace_override = None;
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
if let Some(rest) = tag.strip_prefix("line-height=") {
|
|
1077
|
+
state.line_height_override = parse_loose(&rest.replace('#', ""));
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
if tag == "/line-height" {
|
|
1081
|
+
state.line_height_override = None;
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
if let Some(rest) = tag.strip_prefix("line-indent=") {
|
|
1085
|
+
state.line_indent_override = parse_line_indent(rest);
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
if tag == "/line-indent" {
|
|
1089
|
+
state.line_indent_override = None;
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
if let Some(rest) = tag.strip_prefix("indent=") {
|
|
1093
|
+
return push_indent(&mut state.indent_stack, rest);
|
|
1094
|
+
}
|
|
1095
|
+
if tag == "/indent" {
|
|
1096
|
+
state.indent_stack.pop();
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
if let Some(rest) = tag.strip_prefix("pos=") {
|
|
1100
|
+
return push_indent(&mut state.position_stack, rest);
|
|
1101
|
+
}
|
|
1102
|
+
if tag == "/pos" {
|
|
1103
|
+
state.position_stack.pop();
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
if let Some(rest) = tag.strip_prefix("mspace=") {
|
|
1107
|
+
let mut parts = rest.split_whitespace();
|
|
1108
|
+
if let Some(first) = parts.next() {
|
|
1109
|
+
if let Some(parsed) = parse_indent(first) {
|
|
1110
|
+
state.monospace_stack.push(parsed);
|
|
1111
|
+
state
|
|
1112
|
+
.duospace_stack
|
|
1113
|
+
.push(parts.any(|part| part.replace('#', "") == "duospace=1"));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return true;
|
|
1117
|
+
}
|
|
1118
|
+
if tag == "/mspace" {
|
|
1119
|
+
state.monospace_stack.pop();
|
|
1120
|
+
state.duospace_stack.pop();
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
if let Some(rest) = tag.strip_prefix("align=") {
|
|
1124
|
+
if rest == "left" || rest == "center" || rest == "right" {
|
|
1125
|
+
state.align_stack.push(rest.to_string());
|
|
1126
|
+
}
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
if tag == "/align" {
|
|
1130
|
+
state.align_stack.pop();
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
match tag {
|
|
1134
|
+
"b" => state.bold_depth += 1,
|
|
1135
|
+
"/b" => state.bold_depth = (state.bold_depth - 1).max(0),
|
|
1136
|
+
"i" => state.italic_depth += 1,
|
|
1137
|
+
"/i" => state.italic_depth = (state.italic_depth - 1).max(0),
|
|
1138
|
+
"u" => state.underline_depth += 1,
|
|
1139
|
+
"/u" => state.underline_depth = (state.underline_depth - 1).max(0),
|
|
1140
|
+
"s" => state.strikethrough_depth += 1,
|
|
1141
|
+
"/s" => state.strikethrough_depth = (state.strikethrough_depth - 1).max(0),
|
|
1142
|
+
"sub" => state.subscript_depth += 1,
|
|
1143
|
+
"/sub" => state.subscript_depth = (state.subscript_depth - 1).max(0),
|
|
1144
|
+
"sup" => state.superscript_depth += 1,
|
|
1145
|
+
"/sup" => state.superscript_depth = (state.superscript_depth - 1).max(0),
|
|
1146
|
+
"uppercase" | "allcaps" => state.case_stack.push(CaseTransform::Upper),
|
|
1147
|
+
"/uppercase" | "/allcaps" => {
|
|
1148
|
+
state.case_stack.pop();
|
|
1149
|
+
}
|
|
1150
|
+
"lowercase" => state.case_stack.push(CaseTransform::Lower),
|
|
1151
|
+
"/lowercase" => {
|
|
1152
|
+
state.case_stack.pop();
|
|
1153
|
+
}
|
|
1154
|
+
"smallcaps" => state.smallcaps_depth += 1,
|
|
1155
|
+
"/smallcaps" => state.smallcaps_depth = (state.smallcaps_depth - 1).max(0),
|
|
1156
|
+
"noparse" => state.noparse_depth += 1,
|
|
1157
|
+
"/noparse" => state.noparse_depth = (state.noparse_depth - 1).max(0),
|
|
1158
|
+
_ => {}
|
|
1159
|
+
}
|
|
1160
|
+
true
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
fn can_merge(a: &TextSegment, b: &TextSegment) -> bool {
|
|
1164
|
+
let mut aa = a.clone();
|
|
1165
|
+
let mut bb = b.clone();
|
|
1166
|
+
aa.text.clear();
|
|
1167
|
+
bb.text.clear();
|
|
1168
|
+
aa == bb
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
fn segments_to_global(segs: &[TextSegment]) -> GlobalStyle {
|
|
1172
|
+
let first = segs.first();
|
|
1173
|
+
GlobalStyle {
|
|
1174
|
+
color: first.and_then(|seg| seg.color),
|
|
1175
|
+
alpha: first.and_then(|seg| seg.alpha),
|
|
1176
|
+
align: first.and_then(|seg| seg.align.clone()),
|
|
1177
|
+
clean: segs.iter().map(|seg| seg.text.as_str()).collect::<String>(),
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
fn resolve_segment_font_size(size: &Option<SizeSpec>, base: f32) -> f32 {
|
|
1182
|
+
match size {
|
|
1183
|
+
None => base,
|
|
1184
|
+
Some(SizeSpec::Absolute(value)) => *value,
|
|
1185
|
+
Some(SizeSpec::Delta(value)) => base + *value,
|
|
1186
|
+
Some(SizeSpec::Percent(value)) => base * *value / 100.0,
|
|
1187
|
+
Some(SizeSpec::Em(value)) => base * *value,
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
fn resolve_indent_value(spec: &Option<Indent>, base_font_size: f32, box_w: f32) -> Option<f32> {
|
|
1192
|
+
match spec {
|
|
1193
|
+
None => None,
|
|
1194
|
+
Some(Indent::Pixels(value)) => Some(*value / TEXT_SCALE),
|
|
1195
|
+
Some(Indent::Em(value)) => Some(*value * base_font_size / TEXT_SCALE),
|
|
1196
|
+
Some(Indent::Percent(value)) => Some(box_w * *value / 100.0),
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
fn update_cpv_width(current: f32, before: f32, glyph_advance: f32) -> f32 {
|
|
1201
|
+
current.max(before.abs() + glyph_advance)
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
fn update_cpv_width_for_char(current: f32, before: f32, glyph_advance: f32, ch: char) -> f32 {
|
|
1205
|
+
if ch.is_whitespace() {
|
|
1206
|
+
current
|
|
1207
|
+
} else {
|
|
1208
|
+
update_cpv_width(current, before, glyph_advance)
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
fn transform_char(ch: char, seg: &TextSegment) -> (String, f32) {
|
|
1213
|
+
if seg.smallcaps
|
|
1214
|
+
&& ch.to_lowercase().to_string() == ch.to_string()
|
|
1215
|
+
&& ch.to_uppercase().to_string() != ch.to_string()
|
|
1216
|
+
{
|
|
1217
|
+
return (ch.to_uppercase().collect::<String>(), 0.8);
|
|
1218
|
+
}
|
|
1219
|
+
match seg.case_transform {
|
|
1220
|
+
CaseTransform::Upper => (ch.to_uppercase().collect::<String>(), 1.0),
|
|
1221
|
+
CaseTransform::Lower => (ch.to_lowercase().collect::<String>(), 1.0),
|
|
1222
|
+
CaseTransform::None => (ch.to_string(), 1.0),
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
fn transformed_glyphs(ch: char, seg: &TextSegment) -> Vec<(char, f32)> {
|
|
1227
|
+
let (text, scale) = transform_char(ch, seg);
|
|
1228
|
+
text.chars().map(|value| (value, scale)).collect()
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
fn glyph_demand_chars(raw: &str) -> Vec<char> {
|
|
1232
|
+
parse_rich_segments(raw)
|
|
1233
|
+
.iter()
|
|
1234
|
+
.flat_map(|segment| {
|
|
1235
|
+
segment.text.chars().flat_map(|source| {
|
|
1236
|
+
if source == '\n' || source == '\r' || force_fallback_glyph(source) {
|
|
1237
|
+
Vec::new()
|
|
1238
|
+
} else {
|
|
1239
|
+
transformed_glyphs(source, segment)
|
|
1240
|
+
.into_iter()
|
|
1241
|
+
.map(|(value, _)| value)
|
|
1242
|
+
.collect()
|
|
1243
|
+
}
|
|
1244
|
+
})
|
|
1245
|
+
})
|
|
1246
|
+
.collect()
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
fn resolve_fill(layer: &TextLayer, seg: &TextSegment, global: &GlobalStyle) -> [f32; 4] {
|
|
1250
|
+
let color = seg.color.or(global.color).unwrap_or(layer.color_rgb);
|
|
1251
|
+
[color[0] / 255.0, color[1] / 255.0, color[2] / 255.0, 1.0]
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
fn apply_line_indent(
|
|
1255
|
+
lx: f32,
|
|
1256
|
+
sw: f32,
|
|
1257
|
+
_box_w: f32,
|
|
1258
|
+
effective_align: i32,
|
|
1259
|
+
seg: Option<&TextSegment>,
|
|
1260
|
+
) -> f32 {
|
|
1261
|
+
let mut cursor_x = lx;
|
|
1262
|
+
if let Some(seg) = seg {
|
|
1263
|
+
if let Some(indent) = &seg.indent {
|
|
1264
|
+
match indent {
|
|
1265
|
+
Indent::Percent(value) if *value < 100.0 => {
|
|
1266
|
+
cursor_x = percent_indent_cursor(sw, effective_align, *value);
|
|
1267
|
+
}
|
|
1268
|
+
Indent::Pixels(value) => cursor_x += *value / TEXT_SCALE,
|
|
1269
|
+
Indent::Em(value) => {
|
|
1270
|
+
cursor_x += *value * resolve_segment_font_size(&seg.size, 0.0) / TEXT_SCALE;
|
|
1271
|
+
}
|
|
1272
|
+
_ => {}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if let Some(line_indent) = &seg.line_indent {
|
|
1276
|
+
match line_indent {
|
|
1277
|
+
LineIndent::Percent(value) if *value < 100.0 => {
|
|
1278
|
+
cursor_x = percent_indent_cursor(sw, effective_align, *value);
|
|
1279
|
+
}
|
|
1280
|
+
LineIndent::Pixels(value) => cursor_x = lx + *value,
|
|
1281
|
+
_ => {}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
cursor_x
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
fn percent_indent_cursor(sw: f32, effective_align: i32, percent: f32) -> f32 {
|
|
1289
|
+
let pct = percent / 100.0;
|
|
1290
|
+
let rect = (sw * TEXT_SCALE + 64.0) / (1.0 - pct);
|
|
1291
|
+
let resolved_indent = rect * pct / TEXT_SCALE;
|
|
1292
|
+
if effective_align == 2 {
|
|
1293
|
+
(resolved_indent - sw) / 2.0
|
|
1294
|
+
} else if effective_align == 4 {
|
|
1295
|
+
rect / (2.0 * TEXT_SCALE) - sw
|
|
1296
|
+
} else {
|
|
1297
|
+
rect * (pct - 0.5) / TEXT_SCALE
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
fn layer_base_matrix(layer: &TextLayer) -> Mat {
|
|
1302
|
+
layer.transform_matrix.unwrap_or_else(|| {
|
|
1303
|
+
multiply(
|
|
1304
|
+
translate(layer.x, layer.y),
|
|
1305
|
+
multiply(
|
|
1306
|
+
rotate(layer.rotation_deg),
|
|
1307
|
+
scale(layer.scale_x, layer.scale_y),
|
|
1308
|
+
),
|
|
1309
|
+
)
|
|
1310
|
+
})
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
fn line_indent_dynamic_percent(raw: &str, segments: &[TextSegment]) -> Option<f32> {
|
|
1314
|
+
let _ = raw;
|
|
1315
|
+
let mut dynamic_percent = None;
|
|
1316
|
+
for segment in segments
|
|
1317
|
+
.iter()
|
|
1318
|
+
.filter(|segment| segment.text.chars().any(|ch| !ch.is_whitespace()))
|
|
1319
|
+
{
|
|
1320
|
+
let LineIndent::Percent(value) = segment.line_indent.as_ref()? else {
|
|
1321
|
+
return None;
|
|
1322
|
+
};
|
|
1323
|
+
if !value.is_finite()
|
|
1324
|
+
|| dynamic_percent.is_some_and(|current: f32| (current - *value).abs() > f32::EPSILON)
|
|
1325
|
+
{
|
|
1326
|
+
return None;
|
|
1327
|
+
}
|
|
1328
|
+
dynamic_percent = Some(*value);
|
|
1329
|
+
}
|
|
1330
|
+
dynamic_percent
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
type Mat = [f32; 6];
|
|
1334
|
+
|
|
1335
|
+
fn translate(x: f32, y: f32) -> Mat {
|
|
1336
|
+
[1.0, 0.0, 0.0, 1.0, x, y]
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
fn scale(x: f32, y: f32) -> Mat {
|
|
1340
|
+
[x, 0.0, 0.0, y, 0.0, 0.0]
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
fn skew(x: f32) -> Mat {
|
|
1344
|
+
[1.0, 0.0, x, 1.0, 0.0, 0.0]
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
fn rotate(deg: f32) -> Mat {
|
|
1348
|
+
let t = deg.to_radians();
|
|
1349
|
+
let c = t.cos();
|
|
1350
|
+
let s = t.sin();
|
|
1351
|
+
[c, s, -s, c, 0.0, 0.0]
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
fn multiply(a: Mat, b: Mat) -> Mat {
|
|
1355
|
+
[
|
|
1356
|
+
a[0] * b[0] + a[2] * b[1],
|
|
1357
|
+
a[1] * b[0] + a[3] * b[1],
|
|
1358
|
+
a[0] * b[2] + a[2] * b[3],
|
|
1359
|
+
a[1] * b[2] + a[3] * b[3],
|
|
1360
|
+
a[0] * b[4] + a[2] * b[5] + a[4],
|
|
1361
|
+
a[1] * b[4] + a[3] * b[5] + a[5],
|
|
1362
|
+
]
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
fn apply(m: Mat, x: f32, y: f32) -> [f32; 2] {
|
|
1366
|
+
[m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]]
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
fn parse_size(raw: &str) -> Option<SizeSpec> {
|
|
1370
|
+
let value = raw.replace('#', "");
|
|
1371
|
+
if let Some(rest) = value.strip_suffix("em") {
|
|
1372
|
+
parse_as(rest, SizeSpec::Em)
|
|
1373
|
+
} else if let Some(rest) = value.strip_suffix('%') {
|
|
1374
|
+
parse_as(rest, SizeSpec::Percent)
|
|
1375
|
+
} else if let Some(rest) = value.strip_prefix('+') {
|
|
1376
|
+
parse_as(rest, SizeSpec::Delta)
|
|
1377
|
+
} else if let Some(rest) = value.strip_prefix('-') {
|
|
1378
|
+
parse_as(rest, |n| SizeSpec::Delta(-n))
|
|
1379
|
+
} else {
|
|
1380
|
+
parse_as(&value, SizeSpec::Absolute)
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
fn parse_indent(raw: &str) -> Option<Indent> {
|
|
1385
|
+
let value = raw.replace('#', "");
|
|
1386
|
+
if let Some(rest) = value.strip_suffix('%') {
|
|
1387
|
+
parse_as(rest, Indent::Percent)
|
|
1388
|
+
} else if let Some(rest) = value.strip_suffix("em") {
|
|
1389
|
+
parse_as(rest, Indent::Em)
|
|
1390
|
+
} else if let Some(rest) = value.strip_suffix("px") {
|
|
1391
|
+
parse_as(rest, Indent::Pixels)
|
|
1392
|
+
} else {
|
|
1393
|
+
parse_as(&value, Indent::Pixels)
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
fn parse_line_indent(raw: &str) -> Option<LineIndent> {
|
|
1398
|
+
if let Some(rest) = raw.strip_suffix('%') {
|
|
1399
|
+
parse_as(rest, LineIndent::Percent)
|
|
1400
|
+
} else {
|
|
1401
|
+
parse_as(raw, LineIndent::Pixels)
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
fn parse_as<T>(raw: &str, map: impl FnOnce(f32) -> T) -> Option<T> {
|
|
1406
|
+
parse_loose(raw).map(map)
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
fn parse_loose(raw: &str) -> Option<f32> {
|
|
1410
|
+
let trimmed = raw.trim();
|
|
1411
|
+
let mut end = 0usize;
|
|
1412
|
+
for (idx, ch) in trimmed.char_indices() {
|
|
1413
|
+
let ok =
|
|
1414
|
+
ch.is_ascii_digit() || ch == '+' || ch == '-' || ch == '.' || ch == 'e' || ch == 'E';
|
|
1415
|
+
if ok {
|
|
1416
|
+
end = idx + ch.len_utf8();
|
|
1417
|
+
} else {
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if end == 0 {
|
|
1422
|
+
return None;
|
|
1423
|
+
}
|
|
1424
|
+
trimmed[..end]
|
|
1425
|
+
.parse::<f32>()
|
|
1426
|
+
.ok()
|
|
1427
|
+
.filter(|value| value.is_finite())
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
fn push_color(state: &mut ParseState, hex: &str) -> bool {
|
|
1431
|
+
if let Some(parsed) = parse_hex_color(hex) {
|
|
1432
|
+
state.current_color = Some([parsed[0], parsed[1], parsed[2]]);
|
|
1433
|
+
state.alpha_override = if parsed[3].is_nan() {
|
|
1434
|
+
None
|
|
1435
|
+
} else {
|
|
1436
|
+
Some(parsed[3] / 255.0)
|
|
1437
|
+
};
|
|
1438
|
+
state.color_stack.push(ColorFrame {
|
|
1439
|
+
color: state.current_color,
|
|
1440
|
+
alpha: state.alpha_override,
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
true
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
fn parse_hex_color(hex: &str) -> Option<[f32; 4]> {
|
|
1447
|
+
let value = hex.replace('#', "");
|
|
1448
|
+
if value.len() == 3 || value.len() == 4 {
|
|
1449
|
+
let mut nums = value
|
|
1450
|
+
.chars()
|
|
1451
|
+
.filter_map(|ch| ch.to_digit(16).map(|value| value as f32 * 17.0))
|
|
1452
|
+
.collect::<Vec<_>>();
|
|
1453
|
+
if nums.len() < 3 {
|
|
1454
|
+
return None;
|
|
1455
|
+
}
|
|
1456
|
+
while nums.len() < 4 {
|
|
1457
|
+
nums.push(f32::NAN);
|
|
1458
|
+
}
|
|
1459
|
+
return nums.try_into().ok();
|
|
1460
|
+
}
|
|
1461
|
+
if value.len() == 6 || value.len() == 8 {
|
|
1462
|
+
let r = u8::from_str_radix(&value[0..2], 16).ok()? as f32;
|
|
1463
|
+
let g = u8::from_str_radix(&value[2..4], 16).ok()? as f32;
|
|
1464
|
+
let b = u8::from_str_radix(&value[4..6], 16).ok()? as f32;
|
|
1465
|
+
let a = if value.len() == 8 {
|
|
1466
|
+
u8::from_str_radix(&value[6..8], 16).ok()? as f32
|
|
1467
|
+
} else {
|
|
1468
|
+
f32::NAN
|
|
1469
|
+
};
|
|
1470
|
+
return Some([r, g, b, a]);
|
|
1471
|
+
}
|
|
1472
|
+
None
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
fn push_number(stack: &mut Vec<f32>, raw: &str) -> bool {
|
|
1476
|
+
if let Some(value) = parse_loose(&raw.replace('#', "")) {
|
|
1477
|
+
stack.push(value);
|
|
1478
|
+
}
|
|
1479
|
+
true
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
fn push_indent(stack: &mut Vec<Indent>, raw: &str) -> bool {
|
|
1483
|
+
if let Some(value) = parse_indent(raw) {
|
|
1484
|
+
stack.push(value);
|
|
1485
|
+
}
|
|
1486
|
+
true
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
fn matches_duo(ch: char) -> bool {
|
|
1490
|
+
ch == '.' || ch == ':' || ch == ','
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
fn starts_with_chars(left: &[char], prefix: &[char]) -> bool {
|
|
1494
|
+
left.len() >= prefix.len() && left.iter().zip(prefix.iter()).all(|(a, b)| a == b)
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
#[derive(Deserialize)]
|
|
1498
|
+
#[serde(rename_all = "camelCase")]
|
|
1499
|
+
struct LayoutRequest {
|
|
1500
|
+
layers: Vec<TextLayer>,
|
|
1501
|
+
atlas: AtlasInput,
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
#[derive(Deserialize)]
|
|
1505
|
+
#[serde(rename_all = "camelCase")]
|
|
1506
|
+
struct AtlasInput {
|
|
1507
|
+
base_size: f32,
|
|
1508
|
+
spread: f32,
|
|
1509
|
+
glyphs: Vec<GlyphInfo>,
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
#[derive(Clone, Deserialize)]
|
|
1513
|
+
#[serde(rename_all = "camelCase")]
|
|
1514
|
+
struct GlyphInfo {
|
|
1515
|
+
key: String,
|
|
1516
|
+
#[serde(default)]
|
|
1517
|
+
page: u32,
|
|
1518
|
+
u0: f32,
|
|
1519
|
+
v0: f32,
|
|
1520
|
+
u1: f32,
|
|
1521
|
+
v1: f32,
|
|
1522
|
+
advance: f32,
|
|
1523
|
+
plane_bearing_x: f32,
|
|
1524
|
+
plane_bearing_y: f32,
|
|
1525
|
+
plane_width: f32,
|
|
1526
|
+
plane_height: f32,
|
|
1527
|
+
drawable: bool,
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
#[derive(Deserialize)]
|
|
1531
|
+
#[serde(rename_all = "camelCase")]
|
|
1532
|
+
struct TextLayer {
|
|
1533
|
+
id: String,
|
|
1534
|
+
#[serde(default)]
|
|
1535
|
+
dynamic_layer_id: Option<String>,
|
|
1536
|
+
z: f32,
|
|
1537
|
+
text: String,
|
|
1538
|
+
region: String,
|
|
1539
|
+
font_family: String,
|
|
1540
|
+
font_source_hash: String,
|
|
1541
|
+
#[serde(default)]
|
|
1542
|
+
transform_matrix: Option<Mat>,
|
|
1543
|
+
#[serde(default)]
|
|
1544
|
+
x: f32,
|
|
1545
|
+
#[serde(default)]
|
|
1546
|
+
y: f32,
|
|
1547
|
+
#[serde(default)]
|
|
1548
|
+
rotation_deg: f32,
|
|
1549
|
+
#[serde(default = "one")]
|
|
1550
|
+
scale_x: f32,
|
|
1551
|
+
#[serde(default = "one")]
|
|
1552
|
+
scale_y: f32,
|
|
1553
|
+
font_size: f32,
|
|
1554
|
+
color: [f32; 4],
|
|
1555
|
+
outline_color: [f32; 4],
|
|
1556
|
+
color_rgb: [f32; 3],
|
|
1557
|
+
outline_width: f32,
|
|
1558
|
+
line_spacing: f32,
|
|
1559
|
+
text_type: i32,
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
fn one() -> f32 {
|
|
1563
|
+
1.0
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
#[cfg(test)]
|
|
1567
|
+
mod tests {
|
|
1568
|
+
use super::{
|
|
1569
|
+
build_glyph_demand_json, build_layout_json, glyph_demand_chars, parse_rich_segments,
|
|
1570
|
+
transformed_glyphs, update_cpv_width_for_char,
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
#[test]
|
|
1574
|
+
fn cpv_width_excludes_trailing_spaces_but_caret_keeps_advancing() {
|
|
1575
|
+
let mut width = 0.0;
|
|
1576
|
+
let mut xadv = 0.0;
|
|
1577
|
+
|
|
1578
|
+
width = update_cpv_width_for_char(width, xadv, 24.0, ' ');
|
|
1579
|
+
xadv += 24.0;
|
|
1580
|
+
width = update_cpv_width_for_char(width, xadv, 110.0, '●');
|
|
1581
|
+
xadv += 110.0;
|
|
1582
|
+
for _ in 0..5 {
|
|
1583
|
+
width = update_cpv_width_for_char(width, xadv, 24.0, ' ');
|
|
1584
|
+
xadv += 24.0;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
assert!((width - 134.0).abs() < 1e-6);
|
|
1588
|
+
assert!((xadv - 254.0).abs() < 1e-6);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
#[test]
|
|
1592
|
+
fn glyph_demand_uses_tmp_visible_transformed_scalars() {
|
|
1593
|
+
assert_eq!(
|
|
1594
|
+
glyph_demand_chars("<uppercase>aß</uppercase> <noparse><b></noparse>"),
|
|
1595
|
+
vec!['A', 'S', 'S', '<', 'b', '>'],
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
#[test]
|
|
1600
|
+
fn glyph_demand_json_is_deduplicated_and_font_scoped() {
|
|
1601
|
+
let output: serde_json::Value = serde_json::from_str(
|
|
1602
|
+
&build_glyph_demand_json(r#"{"layers":[{"text":"<b>12</b>","region":"en","fontFamily":"Inter","fontSourceHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":"2A","region":"en","fontFamily":"Inter","fontSourceHash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}"#).unwrap(),
|
|
1603
|
+
).unwrap();
|
|
1604
|
+
assert_eq!(output["source"], "wasm-tmp-glyph-demand");
|
|
1605
|
+
assert_eq!(output["requests"].as_array().unwrap().len(), 3);
|
|
1606
|
+
assert_eq!(output["requests"][0]["char"], "1");
|
|
1607
|
+
assert_eq!(output["requests"][1]["char"], "2");
|
|
1608
|
+
assert_eq!(output["requests"][2]["char"], "A");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
#[test]
|
|
1612
|
+
fn case_expansion_is_laid_out_as_scalar_glyphs() {
|
|
1613
|
+
let segments = parse_rich_segments("<uppercase>ß</uppercase>");
|
|
1614
|
+
let glyphs = transformed_glyphs('ß', &segments[0]);
|
|
1615
|
+
assert_eq!(glyphs, vec![('S', 1.0), ('S', 1.0)]);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
#[test]
|
|
1619
|
+
fn layout_compiles_line_indent_program_from_tmp_without_ts_metadata() {
|
|
1620
|
+
let input = serde_json::json!({
|
|
1621
|
+
"layers": [{
|
|
1622
|
+
"id": "text-layer", "z": 0, "text": "<line-indent=50%>12</line-indent>",
|
|
1623
|
+
"region": "en", "fontFamily": "SyntheticSans", "fontSourceHash": "a".repeat(64),
|
|
1624
|
+
"x": 0.0, "y": 0.0, "rotationDeg": 15.0, "scaleX": 2.0, "scaleY": 1.0,
|
|
1625
|
+
"fontSize": 24.0, "color": [1.0,1.0,1.0,1.0], "outlineColor": [0.0,0.0,0.0,0.0],
|
|
1626
|
+
"colorRgb": [255.0,255.0,255.0], "outlineWidth": 0.0, "lineSpacing": 0.0,
|
|
1627
|
+
"textType": 0, "dynamic": null
|
|
1628
|
+
}],
|
|
1629
|
+
"atlas": { "baseSize": 75.0, "spread": 6.0, "glyphs": [] },
|
|
1630
|
+
"tick": 0, "frameMode": "animate"
|
|
1631
|
+
});
|
|
1632
|
+
let output: serde_json::Value =
|
|
1633
|
+
serde_json::from_str(&build_layout_json(&input.to_string()).unwrap()).unwrap();
|
|
1634
|
+
assert_eq!(output["dynamicPrograms"][0]["percent"], 50.0);
|
|
1635
|
+
assert_eq!(output["dynamicPrograms"][0]["rotationDeg"], 15.0);
|
|
1636
|
+
assert_eq!(output["dynamicPrograms"][0]["scaleX"], 2.0);
|
|
1637
|
+
let layout_metrics = &output["instances"][0]["layoutMetrics"];
|
|
1638
|
+
assert!(layout_metrics["lineWidths"].is_array());
|
|
1639
|
+
assert!(layout_metrics["rectWidths"].is_array());
|
|
1640
|
+
assert!(layout_metrics["boxW"].is_number());
|
|
1641
|
+
assert!(layout_metrics["anchorBase"].is_number());
|
|
1642
|
+
assert!(layout_metrics["lineOffsets"].is_array());
|
|
1643
|
+
assert!(layout_metrics.get("line_widths").is_none());
|
|
1644
|
+
assert_eq!(
|
|
1645
|
+
output["dynamicPrograms"][0]["lineAdvancesTmp"][0]
|
|
1646
|
+
.as_array()
|
|
1647
|
+
.unwrap()
|
|
1648
|
+
.len(),
|
|
1649
|
+
2
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
#[test]
|
|
1654
|
+
fn layout_compiles_one_global_line_indent_program_across_hard_breaks() {
|
|
1655
|
+
let input = serde_json::json!({
|
|
1656
|
+
"layers": [{
|
|
1657
|
+
"id": "text-layer", "z": 0,
|
|
1658
|
+
"text": " <line-indent=50%>A\nBB",
|
|
1659
|
+
"region": "en", "fontFamily": "SyntheticSans", "fontSourceHash": "a".repeat(64),
|
|
1660
|
+
"x": 0.0, "y": 0.0, "rotationDeg": 0.0, "scaleX": 1.0, "scaleY": 1.0,
|
|
1661
|
+
"fontSize": 24.0, "color": [1.0,1.0,1.0,1.0], "outlineColor": [0.0,0.0,0.0,0.0],
|
|
1662
|
+
"colorRgb": [255.0,255.0,255.0], "outlineWidth": 0.0, "lineSpacing": 0.0,
|
|
1663
|
+
"textType": 0, "dynamic": null
|
|
1664
|
+
}],
|
|
1665
|
+
"atlas": { "baseSize": 75.0, "spread": 6.0, "glyphs": [] },
|
|
1666
|
+
"tick": 0, "frameMode": "animate"
|
|
1667
|
+
});
|
|
1668
|
+
let output: serde_json::Value =
|
|
1669
|
+
serde_json::from_str(&build_layout_json(&input.to_string()).unwrap()).unwrap();
|
|
1670
|
+
|
|
1671
|
+
let lines = output["dynamicPrograms"][0]["lineAdvancesTmp"]
|
|
1672
|
+
.as_array()
|
|
1673
|
+
.unwrap();
|
|
1674
|
+
assert_eq!(lines.len(), 2);
|
|
1675
|
+
assert_eq!(lines[0].as_array().unwrap().len(), 2);
|
|
1676
|
+
assert_eq!(lines[1].as_array().unwrap().len(), 2);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
#[test]
|
|
1680
|
+
fn local_layout_is_independent_of_frame_mode() {
|
|
1681
|
+
let request = |frame_mode: &str| {
|
|
1682
|
+
serde_json::json!({
|
|
1683
|
+
"layers": [{
|
|
1684
|
+
"id": "text-layer", "z": 0, "text": "<line-indent=96%>12</line-indent>",
|
|
1685
|
+
"region": "en", "fontFamily": "SyntheticSans", "fontSourceHash": "a".repeat(64),
|
|
1686
|
+
"x": 200.0, "y": 100.0, "rotationDeg": 0.0, "scaleX": 1.0, "scaleY": 1.0,
|
|
1687
|
+
"fontSize": 24.0, "color": [1.0,1.0,1.0,1.0], "outlineColor": [0.0,0.0,0.0,0.0],
|
|
1688
|
+
"colorRgb": [255.0,255.0,255.0], "outlineWidth": 0.0, "lineSpacing": 0.0,
|
|
1689
|
+
"textType": 0
|
|
1690
|
+
}],
|
|
1691
|
+
"atlas": { "baseSize": 75.0, "spread": 6.0, "glyphs": [] },
|
|
1692
|
+
"tick": 0, "frameMode": frame_mode
|
|
1693
|
+
})
|
|
1694
|
+
};
|
|
1695
|
+
let animate: serde_json::Value =
|
|
1696
|
+
serde_json::from_str(&build_layout_json(&request("animate").to_string()).unwrap())
|
|
1697
|
+
.unwrap();
|
|
1698
|
+
let final_layout: serde_json::Value =
|
|
1699
|
+
serde_json::from_str(&build_layout_json(&request("final").to_string()).unwrap())
|
|
1700
|
+
.unwrap();
|
|
1701
|
+
|
|
1702
|
+
assert_eq!(animate["instances"], final_layout["instances"]);
|
|
1703
|
+
assert_eq!(animate["dynamicPrograms"], final_layout["dynamicPrograms"]);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
#[test]
|
|
1707
|
+
fn layout_preserves_a_renderer_owned_reflection_matrix() {
|
|
1708
|
+
let input = serde_json::json!({
|
|
1709
|
+
"layers": [{
|
|
1710
|
+
"id": "reflected-text", "z": 0, "text": "A",
|
|
1711
|
+
"region": "en", "fontFamily": "SyntheticSans", "fontSourceHash": "a".repeat(64),
|
|
1712
|
+
"transformMatrix": [-1.0, 0.0, 0.0, 1.0, 100.0, 20.0],
|
|
1713
|
+
"x": 0.0, "y": 0.0, "rotationDeg": 0.0, "scaleX": 1.0, "scaleY": 1.0,
|
|
1714
|
+
"fontSize": 24.0, "color": [1.0,1.0,1.0,1.0], "outlineColor": [0.0,0.0,0.0,0.0],
|
|
1715
|
+
"colorRgb": [255.0,255.0,255.0], "outlineWidth": 0.0, "lineSpacing": 0.0,
|
|
1716
|
+
"textType": 0
|
|
1717
|
+
}],
|
|
1718
|
+
"atlas": { "baseSize": 75.0, "spread": 6.0, "glyphs": [] },
|
|
1719
|
+
"tick": 0, "frameMode": "animate"
|
|
1720
|
+
});
|
|
1721
|
+
let output: serde_json::Value =
|
|
1722
|
+
serde_json::from_str(&build_layout_json(&input.to_string()).unwrap()).unwrap();
|
|
1723
|
+
let quad = output["instances"][0]["deviceCharQuad"][1]
|
|
1724
|
+
.as_array()
|
|
1725
|
+
.unwrap();
|
|
1726
|
+
let point = |index: usize| {
|
|
1727
|
+
let value = quad[index].as_array().unwrap();
|
|
1728
|
+
[value[0].as_f64().unwrap(), value[1].as_f64().unwrap()]
|
|
1729
|
+
};
|
|
1730
|
+
let p0 = point(0);
|
|
1731
|
+
let p1 = point(1);
|
|
1732
|
+
let p2 = point(2);
|
|
1733
|
+
let winding = (p1[0] - p0[0]) * (p2[1] - p1[1]) - (p1[1] - p0[1]) * (p2[0] - p1[0]);
|
|
1734
|
+
assert!(
|
|
1735
|
+
winding < 0.0,
|
|
1736
|
+
"reflection must preserve negative winding: {quad:?}"
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
#[derive(Clone, PartialEq)]
|
|
1742
|
+
enum SizeSpec {
|
|
1743
|
+
Absolute(f32),
|
|
1744
|
+
Delta(f32),
|
|
1745
|
+
Percent(f32),
|
|
1746
|
+
Em(f32),
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
#[derive(Clone, PartialEq)]
|
|
1750
|
+
enum Indent {
|
|
1751
|
+
Percent(f32),
|
|
1752
|
+
Pixels(f32),
|
|
1753
|
+
Em(f32),
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
#[derive(Clone, PartialEq)]
|
|
1757
|
+
enum LineIndent {
|
|
1758
|
+
Percent(f32),
|
|
1759
|
+
Pixels(f32),
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
#[derive(Clone, PartialEq)]
|
|
1763
|
+
enum CaseTransform {
|
|
1764
|
+
None,
|
|
1765
|
+
Upper,
|
|
1766
|
+
Lower,
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
#[derive(Clone, PartialEq)]
|
|
1770
|
+
struct TextSegment {
|
|
1771
|
+
text: String,
|
|
1772
|
+
fixed_advance: Option<f32>,
|
|
1773
|
+
color: Option<[f32; 3]>,
|
|
1774
|
+
size: Option<SizeSpec>,
|
|
1775
|
+
scale: Option<f32>,
|
|
1776
|
+
alpha: Option<f32>,
|
|
1777
|
+
bold: bool,
|
|
1778
|
+
italic: bool,
|
|
1779
|
+
underline: bool,
|
|
1780
|
+
strikethrough: bool,
|
|
1781
|
+
mark_color: Option<[f32; 4]>,
|
|
1782
|
+
superscript: bool,
|
|
1783
|
+
subscript: bool,
|
|
1784
|
+
case_transform: CaseTransform,
|
|
1785
|
+
smallcaps: bool,
|
|
1786
|
+
voffset: Option<f32>,
|
|
1787
|
+
rotate: Option<f32>,
|
|
1788
|
+
cspace: Option<f32>,
|
|
1789
|
+
line_height: Option<f32>,
|
|
1790
|
+
line_indent: Option<LineIndent>,
|
|
1791
|
+
indent: Option<Indent>,
|
|
1792
|
+
position: Option<Indent>,
|
|
1793
|
+
monospace: Option<Indent>,
|
|
1794
|
+
duospace: bool,
|
|
1795
|
+
align: Option<String>,
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
struct GlobalStyle {
|
|
1799
|
+
color: Option<[f32; 3]>,
|
|
1800
|
+
alpha: Option<f32>,
|
|
1801
|
+
align: Option<String>,
|
|
1802
|
+
clean: String,
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
#[derive(Clone, Serialize)]
|
|
1806
|
+
#[serde(rename_all = "camelCase")]
|
|
1807
|
+
pub struct LayoutMetrics {
|
|
1808
|
+
line_widths: Vec<f32>,
|
|
1809
|
+
rect_widths: Vec<f32>,
|
|
1810
|
+
box_w: f32,
|
|
1811
|
+
anchor_base: f32,
|
|
1812
|
+
line_offsets: Vec<f32>,
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
struct GlyphOp {
|
|
1816
|
+
x: f32,
|
|
1817
|
+
y: f32,
|
|
1818
|
+
pivot_x: f32,
|
|
1819
|
+
pivot_y: f32,
|
|
1820
|
+
shear_cx: f32,
|
|
1821
|
+
scale_x: f32,
|
|
1822
|
+
skew_x: f32,
|
|
1823
|
+
rotate_deg: f32,
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
#[derive(Clone, Copy)]
|
|
1827
|
+
struct SdfShaderParams {
|
|
1828
|
+
face_scale: f32,
|
|
1829
|
+
face_bias: f32,
|
|
1830
|
+
underlay_scale: f32,
|
|
1831
|
+
underlay_bias: f32,
|
|
1832
|
+
vertex_alpha: f32,
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
#[derive(Serialize)]
|
|
1836
|
+
#[serde(rename_all = "camelCase")]
|
|
1837
|
+
struct LayoutBatch {
|
|
1838
|
+
version: i32,
|
|
1839
|
+
source: String,
|
|
1840
|
+
instances: Vec<GlyphInstance>,
|
|
1841
|
+
dynamic_programs: Vec<DynamicProgramDescriptor>,
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
#[derive(Serialize)]
|
|
1845
|
+
#[serde(rename_all = "camelCase")]
|
|
1846
|
+
struct DynamicProgramDescriptor {
|
|
1847
|
+
layer_id: String,
|
|
1848
|
+
percent: f32,
|
|
1849
|
+
line_advances_tmp: Vec<Vec<f32>>,
|
|
1850
|
+
rotation_deg: f32,
|
|
1851
|
+
scale_x: f32,
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
#[derive(Serialize)]
|
|
1855
|
+
#[serde(rename_all = "camelCase")]
|
|
1856
|
+
struct GlyphInstance {
|
|
1857
|
+
layer_id: String,
|
|
1858
|
+
plain_text_index: usize,
|
|
1859
|
+
#[serde(rename = "char")]
|
|
1860
|
+
char_value: String,
|
|
1861
|
+
drawable: bool,
|
|
1862
|
+
glyph_key: String,
|
|
1863
|
+
atlas_page: u32,
|
|
1864
|
+
z: f32,
|
|
1865
|
+
quad: Vec<[f32; 5]>,
|
|
1866
|
+
char_position: (String, f32, f32, f32, f32, f32),
|
|
1867
|
+
char_op: (String, f32, f32, f32, f32, f32, f32),
|
|
1868
|
+
char_quad: (String, Vec<[f32; 2]>),
|
|
1869
|
+
device_char_position: (String, f32, f32),
|
|
1870
|
+
device_char_quad: (String, Vec<[f32; 2]>),
|
|
1871
|
+
device_glyph_quad: (String, Vec<[f32; 2]>),
|
|
1872
|
+
layout_metrics: LayoutMetrics,
|
|
1873
|
+
fill: [f32; 4],
|
|
1874
|
+
outline: [f32; 4],
|
|
1875
|
+
outline_width: f32,
|
|
1876
|
+
shader_font_size: f32,
|
|
1877
|
+
shader_face_scale: f32,
|
|
1878
|
+
shader_face_bias: f32,
|
|
1879
|
+
shader_underlay_scale: f32,
|
|
1880
|
+
shader_underlay_bias: f32,
|
|
1881
|
+
shader_vertex_alpha: f32,
|
|
1882
|
+
}
|