@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.
Files changed (130) hide show
  1. package/LICENSE +235 -0
  2. package/LICENSE-EXCEPTION +32 -0
  3. package/NOTICE +11 -0
  4. package/README.en.md +456 -0
  5. package/README.md +458 -0
  6. package/dist/allium_renderer_wasm.js +14 -0
  7. package/dist/allium_renderer_wasm.wasm +0 -0
  8. package/dist/authoring.d.ts +14 -0
  9. package/dist/authoring.js +34 -0
  10. package/dist/cache/glyphPersistentCache.d.ts +145 -0
  11. package/dist/cache/glyphPersistentCache.js +346 -0
  12. package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
  13. package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
  14. package/dist/cache/sessionImageResourceCache.d.ts +39 -0
  15. package/dist/cache/sessionImageResourceCache.js +156 -0
  16. package/dist/emscripten.d.ts +11 -0
  17. package/dist/emscripten.js +1 -0
  18. package/dist/fontProvider.d.ts +36 -0
  19. package/dist/fontProvider.js +84 -0
  20. package/dist/fontSdfAtlas.d.ts +92 -0
  21. package/dist/fontSdfAtlas.js +296 -0
  22. package/dist/gpu/browserSemanticResources.d.ts +64 -0
  23. package/dist/gpu/browserSemanticResources.js +232 -0
  24. package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
  25. package/dist/gpu/generalTextRenderPlacement.js +131 -0
  26. package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
  27. package/dist/gpu/previewTransformTextureLayout.js +12 -0
  28. package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
  29. package/dist/gpu/semanticCommandGeometry.js +226 -0
  30. package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
  31. package/dist/gpu/semanticCommandPlanner.js +203 -0
  32. package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
  33. package/dist/gpu/semanticTextGlyphBridge.js +22 -0
  34. package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
  35. package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
  36. package/dist/gpu/slotRanges.d.ts +5 -0
  37. package/dist/gpu/slotRanges.js +14 -0
  38. package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
  39. package/dist/gpu/webglSdfAtlasTexture.js +72 -0
  40. package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
  41. package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
  42. package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
  43. package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
  44. package/dist/index.d.ts +18 -0
  45. package/dist/index.js +8 -0
  46. package/dist/interaction/numericTextRegions.d.ts +46 -0
  47. package/dist/interaction/numericTextRegions.js +44 -0
  48. package/dist/localizationProvider.d.ts +33 -0
  49. package/dist/localizationProvider.js +74 -0
  50. package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
  51. package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
  52. package/dist/prebuiltSdfAtlas.d.ts +36 -0
  53. package/dist/prebuiltSdfAtlas.js +261 -0
  54. package/dist/protocol.d.ts +456 -0
  55. package/dist/protocol.js +1 -0
  56. package/dist/renderer.d.ts +218 -0
  57. package/dist/renderer.js +641 -0
  58. package/dist/resourceProvider.d.ts +24 -0
  59. package/dist/resourceProvider.js +41 -0
  60. package/dist/telemetry/rendererTelemetry.d.ts +158 -0
  61. package/dist/telemetry/rendererTelemetry.js +232 -0
  62. package/dist/third-party/freetype/FTL.txt +169 -0
  63. package/dist/types/atlas.d.ts +90 -0
  64. package/dist/types/atlas.js +1 -0
  65. package/dist/types/authoring.d.ts +81 -0
  66. package/dist/types/authoring.js +1 -0
  67. package/dist/types/core.d.ts +128 -0
  68. package/dist/types/core.js +1 -0
  69. package/dist/types/freeType.d.ts +54 -0
  70. package/dist/types/freeType.js +13 -0
  71. package/dist/types/glyph.d.ts +33 -0
  72. package/dist/types/glyph.js +1 -0
  73. package/dist/types/layout.d.ts +25 -0
  74. package/dist/types/layout.js +1 -0
  75. package/dist/types/text.d.ts +28 -0
  76. package/dist/types/text.js +1 -0
  77. package/dist/worker/glyphWorkScheduler.d.ts +29 -0
  78. package/dist/worker/glyphWorkScheduler.js +107 -0
  79. package/dist/worker-client.d.ts +114 -0
  80. package/dist/worker-client.js +413 -0
  81. package/dist/worker.d.ts +1 -0
  82. package/dist/worker.js +708 -0
  83. package/package.json +46 -0
  84. package/src/atlas.rs +652 -0
  85. package/src/authoring.ts +42 -0
  86. package/src/authoring_runtime.rs +303 -0
  87. package/src/cache/glyphPersistentCache.ts +463 -0
  88. package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
  89. package/src/cache/sessionImageResourceCache.ts +186 -0
  90. package/src/edt.rs +97 -0
  91. package/src/emscripten.ts +17 -0
  92. package/src/fontProvider.ts +127 -0
  93. package/src/fontSdfAtlas.ts +431 -0
  94. package/src/geometry.rs +149 -0
  95. package/src/glyph_plan.rs +226 -0
  96. package/src/gpu/browserSemanticResources.ts +294 -0
  97. package/src/gpu/generalTextRenderPlacement.ts +175 -0
  98. package/src/gpu/previewTransformTextureLayout.ts +12 -0
  99. package/src/gpu/semanticCommandGeometry.ts +260 -0
  100. package/src/gpu/semanticCommandPlanner.ts +280 -0
  101. package/src/gpu/semanticTextGlyphBridge.ts +35 -0
  102. package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
  103. package/src/gpu/slotRanges.ts +15 -0
  104. package/src/gpu/webglSdfAtlasTexture.ts +69 -0
  105. package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
  106. package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
  107. package/src/index.ts +84 -0
  108. package/src/interaction/numericTextRegions.ts +80 -0
  109. package/src/layout.rs +1882 -0
  110. package/src/lib.rs +1443 -0
  111. package/src/localizationProvider.ts +113 -0
  112. package/src/main.rs +55 -0
  113. package/src/masterdata_runtime.rs +496 -0
  114. package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
  115. package/src/prebuiltSdfAtlas.ts +306 -0
  116. package/src/protocol.ts +150 -0
  117. package/src/renderer.ts +792 -0
  118. package/src/resourceProvider.ts +69 -0
  119. package/src/scene.rs +665 -0
  120. package/src/telemetry/rendererTelemetry.ts +358 -0
  121. package/src/types/atlas.ts +80 -0
  122. package/src/types/authoring.ts +85 -0
  123. package/src/types/core.ts +110 -0
  124. package/src/types/freeType.ts +63 -0
  125. package/src/types/glyph.ts +33 -0
  126. package/src/types/layout.ts +28 -0
  127. package/src/types/text.ts +28 -0
  128. package/src/worker/glyphWorkScheduler.ts +130 -0
  129. package/src/worker-client.ts +484 -0
  130. 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
+ }