@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
@@ -0,0 +1,113 @@
1
+ export type LocalizationRequest = {
2
+ region: string;
3
+ locale: string;
4
+ key: string;
5
+ };
6
+
7
+ export type LocalizationContext = {
8
+ signal: AbortSignal;
9
+ };
10
+
11
+ /** Resolves renderer-owned UI text through arbitrary caller-owned logic. */
12
+ export interface LocalizationProvider {
13
+ provide(
14
+ request: LocalizationRequest,
15
+ context: LocalizationContext,
16
+ ): Promise<string | null>;
17
+ }
18
+
19
+ export type LocalizationProviderManagerOptions = {
20
+ provider: LocalizationProvider;
21
+ concurrency?: number;
22
+ };
23
+
24
+ export type LocalizationProviderStats = {
25
+ requested: number;
26
+ unique: number;
27
+ resolved: number;
28
+ failures: number;
29
+ active: number;
30
+ peakActive: number;
31
+ resolveMs: number;
32
+ };
33
+
34
+ export class LocalizationProviderManager {
35
+ private readonly provider: LocalizationProvider;
36
+ private readonly concurrency: number;
37
+ private readonly counters: LocalizationProviderStats = {
38
+ requested: 0,
39
+ unique: 0,
40
+ resolved: 0,
41
+ failures: 0,
42
+ active: 0,
43
+ peakActive: 0,
44
+ resolveMs: 0,
45
+ };
46
+
47
+ constructor(options: LocalizationProviderManagerOptions) {
48
+ if (!options.provider || typeof options.provider.provide !== "function") {
49
+ throw new Error("localization provider is required");
50
+ }
51
+ const concurrency = options.concurrency ?? 8;
52
+ if (!Number.isInteger(concurrency) || concurrency <= 0) {
53
+ throw new Error("localization concurrency must be a positive integer");
54
+ }
55
+ this.provider = options.provider;
56
+ this.concurrency = concurrency;
57
+ }
58
+
59
+ async resolve(
60
+ requests: readonly LocalizationRequest[],
61
+ signal: AbortSignal = new AbortController().signal,
62
+ ): Promise<Record<string, string>> {
63
+ const started = performance.now();
64
+ const unique = [...new Map(requests.map((request) => [
65
+ `${request.region}\0${request.locale}\0${request.key}`,
66
+ request,
67
+ ] as const)).values()];
68
+ this.counters.requested += requests.length;
69
+ this.counters.unique += unique.length;
70
+ const values = new Map<string, string>();
71
+ let cursor = 0;
72
+ const worker = async () => {
73
+ while (cursor < unique.length) {
74
+ if (signal.aborted) throw abortReason(signal);
75
+ const request = unique[cursor++];
76
+ this.counters.active += 1;
77
+ this.counters.peakActive = Math.max(this.counters.peakActive, this.counters.active);
78
+ let value: string | null;
79
+ try {
80
+ value = await this.provider.provide(request, { signal });
81
+ } catch (error) {
82
+ this.counters.failures += 1;
83
+ throw error;
84
+ } finally {
85
+ this.counters.active -= 1;
86
+ }
87
+ if (typeof value !== "string") {
88
+ this.counters.failures += 1;
89
+ throw new Error(`localization provider returned no text for ${request.locale}:${request.key}`);
90
+ }
91
+ this.counters.resolved += 1;
92
+ values.set(request.key, value);
93
+ }
94
+ };
95
+ try {
96
+ await Promise.all(Array.from(
97
+ { length: Math.min(this.concurrency, unique.length) },
98
+ worker,
99
+ ));
100
+ return Object.fromEntries(values);
101
+ } finally {
102
+ this.counters.resolveMs += performance.now() - started;
103
+ }
104
+ }
105
+
106
+ stats(): LocalizationProviderStats {
107
+ return { ...this.counters };
108
+ }
109
+ }
110
+
111
+ function abortReason(signal: AbortSignal): unknown {
112
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
113
+ }
package/src/main.rs ADDED
@@ -0,0 +1,55 @@
1
+ //! Emscripten link driver for the browser renderer runtime.
2
+ //!
3
+ //! The C ABI is compiled directly into this binary so link-time optimization
4
+ //! cannot internalize the exported symbols across an rlib boundary.
5
+
6
+ #[path = "lib.rs"]
7
+ mod exports;
8
+
9
+ fn keep_exports() {
10
+ let anchors: &[*const ()] = &[
11
+ exports::sdf_layout_freetype_probe as *const (),
12
+ exports::sdf_layout_freetype_contract_json as *const (),
13
+ exports::sdf_layout_freetype_map_glyphs_json as *const (),
14
+ exports::sdf_layout_freetype_plan_glyphs_json as *const (),
15
+ exports::sdf_layout_freetype_build_glyph_json as *const (),
16
+ exports::sdf_layout_freetype_build_glyph_json_edt as *const (),
17
+ exports::sdf_layout_freetype_build_mask_json as *const (),
18
+ exports::sdf_layout_freetype_build_layout_json as *const (),
19
+ exports::sdf_layout_freetype_glyph_demand_json as *const (),
20
+ exports::sdf_atlas_create_json as *const (),
21
+ exports::sdf_atlas_resolve_json as *const (),
22
+ exports::sdf_atlas_pages_since_json as *const (),
23
+ exports::sdf_atlas_page_pixels_ptr as *const (),
24
+ exports::sdf_atlas_page_pixels_len as *const (),
25
+ exports::sdf_atlas_release as *const (),
26
+ exports::sdf_atlas_destroy as *const (),
27
+ exports::sdf_renderer_core_scene_create_json as *const (),
28
+ exports::sdf_renderer_core_profile_scene_create_json as *const (),
29
+ exports::sdf_renderer_core_masterdata_create_json as *const (),
30
+ exports::sdf_renderer_core_masterdata_put_table_json as *const (),
31
+ exports::sdf_renderer_core_masterdata_seal_json as *const (),
32
+ exports::sdf_renderer_core_profile_prepare_json as *const (),
33
+ exports::sdf_renderer_core_profile_create_json as *const (),
34
+ exports::sdf_renderer_core_masterdata_stats_json as *const (),
35
+ exports::sdf_renderer_core_masterdata_destroy as *const (),
36
+ exports::sdf_renderer_core_scene_advance_json as *const (),
37
+ exports::sdf_renderer_core_scene_advance_binary as *const (),
38
+ exports::sdf_renderer_core_scene_set_mask_json as *const (),
39
+ exports::sdf_renderer_core_scene_set_masks_json as *const (),
40
+ exports::sdf_renderer_core_scene_set_tab_json as *const (),
41
+ exports::sdf_renderer_core_scene_scroll_json as *const (),
42
+ exports::sdf_renderer_core_scene_dump_json as *const (),
43
+ exports::sdf_renderer_core_scene_destroy as *const (),
44
+ exports::sdf_renderer_core_resolve_locale_json as *const (),
45
+ exports::sdf_renderer_core_resolve_profile_json as *const (),
46
+ exports::sdf_layout_freetype_free_string as *const (),
47
+ ];
48
+ for &function in anchors {
49
+ unsafe { core::ptr::read_volatile(&function) };
50
+ }
51
+ }
52
+
53
+ fn main() {
54
+ keep_exports();
55
+ }
@@ -0,0 +1,496 @@
1
+ use std::cell::RefCell;
2
+ use std::collections::BTreeMap;
3
+
4
+ use allium_renderer_core::masterdata::{JsonMasterData, PROFILE_MASTERDATA_TABLES};
5
+ use allium_renderer_core::profile_data::ProfileData;
6
+ use allium_renderer_core::profile_resolve::{
7
+ compile_profile_scene, compile_profile_scene_with_localizations, prepare_profile,
8
+ prepare_profile_with_localizations, ResourceAvailability, ResourceMetadata, ResourceMetric,
9
+ };
10
+ use allium_renderer_core::profile_source::CustomProfileCard;
11
+ use allium_renderer_core::{LineIndentSource, ResourceKey};
12
+ use serde::{Deserialize, Serialize};
13
+ use serde_json::Value;
14
+
15
+ thread_local! {
16
+ static MASTERDATA: RefCell<MasterDataTable> = RefCell::new(MasterDataTable::default());
17
+ }
18
+
19
+ #[derive(Default)]
20
+ struct MasterDataTable {
21
+ next_handle: u32,
22
+ sessions: BTreeMap<u32, MasterDataSession>,
23
+ }
24
+
25
+ struct MasterDataSession {
26
+ region: String,
27
+ revision: String,
28
+ data: JsonMasterData,
29
+ sealed: bool,
30
+ }
31
+
32
+ #[derive(Deserialize)]
33
+ #[serde(rename_all = "camelCase")]
34
+ struct CreateRequest {
35
+ region: String,
36
+ revision: String,
37
+ }
38
+
39
+ #[derive(Serialize)]
40
+ struct CreateResponse {
41
+ handle: u32,
42
+ region: String,
43
+ revision: String,
44
+ required_tables: &'static [&'static str],
45
+ }
46
+
47
+ #[derive(Deserialize)]
48
+ struct PutTableRequest {
49
+ name: String,
50
+ table: Value,
51
+ }
52
+
53
+ #[derive(Deserialize)]
54
+ #[serde(rename_all = "camelCase")]
55
+ struct PrepareRequest {
56
+ document_key: String,
57
+ card: CustomProfileCard,
58
+ #[serde(default)]
59
+ profile: Option<Value>,
60
+ #[serde(default)]
61
+ locale: Option<String>,
62
+ #[serde(default)]
63
+ demand_only: bool,
64
+ #[serde(default)]
65
+ localized_text: Option<BTreeMap<String, String>>,
66
+ }
67
+
68
+ #[derive(Deserialize)]
69
+ #[serde(rename_all = "camelCase")]
70
+ struct CompileRequest {
71
+ document_key: String,
72
+ card: CustomProfileCard,
73
+ #[serde(default)]
74
+ profile: Option<Value>,
75
+ #[serde(default)]
76
+ locale: Option<String>,
77
+ #[serde(default)]
78
+ localized_text: Option<BTreeMap<String, String>>,
79
+ #[serde(default)]
80
+ resource_metrics: Vec<ResourceMetricInput>,
81
+ #[serde(default)]
82
+ dynamic_programs: Vec<DynamicProgramInput>,
83
+ #[serde(default)]
84
+ frame_mode: Option<String>,
85
+ }
86
+
87
+ #[derive(Deserialize)]
88
+ #[serde(rename_all = "camelCase")]
89
+ struct DynamicProgramInput {
90
+ layer_id: String,
91
+ percent: f32,
92
+ line_advances_tmp: Vec<Vec<f32>>,
93
+ rotation_deg: f32,
94
+ scale_x: f32,
95
+ }
96
+
97
+ #[derive(Deserialize)]
98
+ struct ResourceMetricInput {
99
+ namespace: String,
100
+ key: String,
101
+ width: f32,
102
+ height: f32,
103
+ #[serde(default = "yes")]
104
+ available: bool,
105
+ }
106
+
107
+ #[derive(Clone, Copy)]
108
+ struct ResourceMetricState {
109
+ metric: ResourceMetric,
110
+ available: bool,
111
+ }
112
+
113
+ struct ResourceMetricMap(BTreeMap<(String, String), ResourceMetricState>);
114
+
115
+ impl ResourceMetadata for ResourceMetricMap {
116
+ fn metric(&self, resource: &ResourceKey) -> Option<ResourceMetric> {
117
+ self.0
118
+ .get(&(resource.namespace.clone(), resource.key.clone()))
119
+ .filter(|state| state.available)
120
+ .map(|state| state.metric)
121
+ }
122
+
123
+ fn availability(&self, resource: &ResourceKey) -> ResourceAvailability {
124
+ match self
125
+ .0
126
+ .get(&(resource.namespace.clone(), resource.key.clone()))
127
+ {
128
+ Some(state) if state.available => ResourceAvailability::Available,
129
+ Some(_) => ResourceAvailability::Unavailable,
130
+ None => ResourceAvailability::Unknown,
131
+ }
132
+ }
133
+ }
134
+
135
+ fn yes() -> bool {
136
+ true
137
+ }
138
+
139
+ #[derive(Serialize)]
140
+ struct SessionStats<'a> {
141
+ handle: u32,
142
+ region: &'a str,
143
+ revision: &'a str,
144
+ sealed: bool,
145
+ tables: Vec<&'a str>,
146
+ }
147
+
148
+ pub fn create(input: &str) -> Result<String, String> {
149
+ let request: CreateRequest = serde_json::from_str(input)
150
+ .map_err(|error| format!("parse master-data session failed: {error}"))?;
151
+ if request.region.is_empty() || request.revision.is_empty() {
152
+ return Err("master-data region and revision are required".into());
153
+ }
154
+ MASTERDATA.with(|table| {
155
+ let mut table = table.borrow_mut();
156
+ table.next_handle = table.next_handle.wrapping_add(1).max(1);
157
+ let handle = table.next_handle;
158
+ let response = CreateResponse {
159
+ handle,
160
+ region: request.region.clone(),
161
+ revision: request.revision.clone(),
162
+ required_tables: PROFILE_MASTERDATA_TABLES,
163
+ };
164
+ table.sessions.insert(
165
+ handle,
166
+ MasterDataSession {
167
+ data: JsonMasterData::new(&request.region),
168
+ region: request.region,
169
+ revision: request.revision,
170
+ sealed: false,
171
+ },
172
+ );
173
+ serde_json::to_string(&response).map_err(|error| error.to_string())
174
+ })
175
+ }
176
+
177
+ pub fn put_table(handle: u32, input: &str) -> Result<String, String> {
178
+ let request: PutTableRequest = serde_json::from_str(input)
179
+ .map_err(|error| format!("parse master-data table failed: {error}"))?;
180
+ with_session_mut(handle, |session| {
181
+ if session.sealed {
182
+ return Err("master-data session is sealed".into());
183
+ }
184
+ session
185
+ .data
186
+ .insert_value(&request.name, request.table)
187
+ .map_err(|error| error.to_string())?;
188
+ serde_json::to_string(&serde_json::json!({
189
+ "handle": handle,
190
+ "loadedTables": session.data.loaded_tables().count(),
191
+ }))
192
+ .map_err(|error| error.to_string())
193
+ })
194
+ }
195
+
196
+ pub fn seal(handle: u32) -> Result<String, String> {
197
+ with_session_mut(handle, |session| {
198
+ session.sealed = true;
199
+ stats_value(handle, session)
200
+ })
201
+ }
202
+
203
+ pub fn prepare(handle: u32, input: &str) -> Result<String, String> {
204
+ let request: PrepareRequest = serde_json::from_str(input)
205
+ .map_err(|error| format!("parse profile preparation failed: {error}"))?;
206
+ with_session(handle, |session| {
207
+ if !session.sealed {
208
+ return Err("master-data session must be sealed before profile preparation".into());
209
+ }
210
+ let locale = request.locale.as_deref().unwrap_or(&session.region);
211
+ if request.demand_only {
212
+ return serde_json::to_string(&serde_json::json!({
213
+ "localizationDemands": allium_renderer_core::locale::profile_localization_demands(
214
+ &request.card,
215
+ &session.region,
216
+ locale,
217
+ )
218
+ }))
219
+ .map_err(|error| error.to_string());
220
+ }
221
+ let profile = request.profile.as_ref().map(ProfileData::from_json);
222
+ let prepared = match request.localized_text.as_ref() {
223
+ Some(localized_text) => prepare_profile_with_localizations(
224
+ &request.card,
225
+ profile.as_ref(),
226
+ &session.data,
227
+ &request.document_key,
228
+ locale,
229
+ localized_text,
230
+ ),
231
+ None => prepare_profile(
232
+ &request.card,
233
+ profile.as_ref(),
234
+ &session.data,
235
+ &request.document_key,
236
+ locale,
237
+ ),
238
+ }
239
+ .map_err(|error| error.to_string())?;
240
+ serde_json::to_string(&prepared).map_err(|error| error.to_string())
241
+ })
242
+ }
243
+
244
+ pub fn stats(handle: u32) -> Result<String, String> {
245
+ with_session(handle, |session| stats_value(handle, session))
246
+ }
247
+
248
+ pub fn create_scene(handle: u32, input: &str) -> Result<String, String> {
249
+ let request: CompileRequest = serde_json::from_str(input)
250
+ .map_err(|error| format!("parse profile compile request failed: {error}"))?;
251
+ with_session(handle, |session| {
252
+ if !session.sealed {
253
+ return Err("master-data session must be sealed before scene creation".into());
254
+ }
255
+ let static_final = match request.frame_mode.as_deref().unwrap_or("animate") {
256
+ "animate" => false,
257
+ "final" => true,
258
+ value => return Err(format!("unsupported frame mode {value}")),
259
+ };
260
+ let metrics = ResourceMetricMap(
261
+ request
262
+ .resource_metrics
263
+ .into_iter()
264
+ .map(|entry| {
265
+ (
266
+ (entry.namespace, entry.key),
267
+ ResourceMetricState {
268
+ metric: ResourceMetric {
269
+ width: entry.width,
270
+ height: entry.height,
271
+ },
272
+ available: entry.available,
273
+ },
274
+ )
275
+ })
276
+ .collect(),
277
+ );
278
+ let profile = request.profile.as_ref().map(ProfileData::from_json);
279
+ let mut line_indent = BTreeMap::new();
280
+ for program in request.dynamic_programs {
281
+ let source = LineIndentSource {
282
+ percent: program.percent,
283
+ line_advances_tmp: program.line_advances_tmp,
284
+ rotation_deg: program.rotation_deg,
285
+ scale_x: program.scale_x,
286
+ };
287
+ if line_indent
288
+ .insert(program.layer_id.clone(), source)
289
+ .is_some()
290
+ {
291
+ return Err(format!(
292
+ "duplicate dynamic program for layer {}",
293
+ program.layer_id
294
+ ));
295
+ }
296
+ }
297
+ let locale = request.locale.as_deref().unwrap_or(&session.region);
298
+ let resolved = match request.localized_text.as_ref() {
299
+ Some(localized_text) => compile_profile_scene_with_localizations(
300
+ &request.card,
301
+ profile.as_ref(),
302
+ &session.data,
303
+ &request.document_key,
304
+ locale,
305
+ &metrics,
306
+ line_indent,
307
+ localized_text,
308
+ ),
309
+ None => compile_profile_scene(
310
+ &request.card,
311
+ profile.as_ref(),
312
+ &session.data,
313
+ &request.document_key,
314
+ locale,
315
+ &metrics,
316
+ line_indent,
317
+ ),
318
+ }
319
+ .map_err(|error| error.to_string())?;
320
+ super::scene::create_compiled_profile(
321
+ &request.document_key,
322
+ &session.region,
323
+ resolved,
324
+ static_final,
325
+ )
326
+ })
327
+ }
328
+
329
+ pub fn destroy(handle: u32) -> bool {
330
+ MASTERDATA.with(|table| table.borrow_mut().sessions.remove(&handle).is_some())
331
+ }
332
+
333
+ fn stats_value(handle: u32, session: &MasterDataSession) -> Result<String, String> {
334
+ serde_json::to_string(&SessionStats {
335
+ handle,
336
+ region: &session.region,
337
+ revision: &session.revision,
338
+ sealed: session.sealed,
339
+ tables: session.data.loaded_tables().collect(),
340
+ })
341
+ .map_err(|error| error.to_string())
342
+ }
343
+
344
+ #[cfg(test)]
345
+ mod localization_tests {
346
+ use super::*;
347
+
348
+ #[test]
349
+ fn profile_prepare_has_a_demand_phase_and_uses_the_external_localization_snapshot() {
350
+ let created: Value = serde_json::from_str(
351
+ &create(r#"{"region":"en","revision":"localization-test"}"#).unwrap(),
352
+ )
353
+ .unwrap();
354
+ let handle = created["handle"].as_u64().unwrap() as u32;
355
+ put_table(
356
+ handle,
357
+ r#"{"name":"customProfileTextFonts","table":[{"id":1,"fontName":"SyntheticSans"}]}"#,
358
+ )
359
+ .unwrap();
360
+ seal(handle).unwrap();
361
+ let card = serde_json::json!({
362
+ "generals": [{ "objectData": object(4), "type": 4 }]
363
+ });
364
+ let profile = serde_json::json!({
365
+ "user": { "name": "Player", "rank": 1 },
366
+ "userProfile": { "word": "Original profile text" }
367
+ });
368
+ let demand: Value = serde_json::from_str(
369
+ &prepare(
370
+ handle,
371
+ &serde_json::json!({
372
+ "documentKey": "localization-demand",
373
+ "card": card,
374
+ "profile": profile,
375
+ "locale": "en-US",
376
+ "demandOnly": true
377
+ })
378
+ .to_string(),
379
+ )
380
+ .unwrap(),
381
+ )
382
+ .unwrap();
383
+ assert_eq!(
384
+ demand["localizationDemands"][0]["key"],
385
+ "custom_profile.general.comment.title"
386
+ );
387
+ let prepared: Value = serde_json::from_str(
388
+ &prepare(
389
+ handle,
390
+ &serde_json::json!({
391
+ "documentKey": "localized-profile",
392
+ "card": card,
393
+ "profile": profile,
394
+ "locale": "en-US",
395
+ "localizedText": {
396
+ "custom_profile.general.comment.title": "External Bio"
397
+ }
398
+ })
399
+ .to_string(),
400
+ )
401
+ .unwrap(),
402
+ )
403
+ .unwrap();
404
+ assert!(prepared["glyph_layers"]
405
+ .as_array()
406
+ .unwrap()
407
+ .iter()
408
+ .any(|entry| entry["text"] == "External Bio"));
409
+ }
410
+
411
+ fn object(layer: i32) -> Value {
412
+ serde_json::json!({
413
+ "layer": layer,
414
+ "lock": false,
415
+ "position": { "x": 0.0, "y": 0.0, "z": 0.0 },
416
+ "rotation": { "w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0 },
417
+ "scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
418
+ "visible": true
419
+ })
420
+ }
421
+ }
422
+
423
+ fn with_session<T>(
424
+ handle: u32,
425
+ call: impl FnOnce(&MasterDataSession) -> Result<T, String>,
426
+ ) -> Result<T, String> {
427
+ MASTERDATA.with(|table| {
428
+ let table = table.borrow();
429
+ let session = table
430
+ .sessions
431
+ .get(&handle)
432
+ .ok_or_else(|| format!("unknown master-data session {handle}"))?;
433
+ call(session)
434
+ })
435
+ }
436
+
437
+ fn with_session_mut<T>(
438
+ handle: u32,
439
+ call: impl FnOnce(&mut MasterDataSession) -> Result<T, String>,
440
+ ) -> Result<T, String> {
441
+ MASTERDATA.with(|table| {
442
+ let mut table = table.borrow_mut();
443
+ let session = table
444
+ .sessions
445
+ .get_mut(&handle)
446
+ .ok_or_else(|| format!("unknown master-data session {handle}"))?;
447
+ call(session)
448
+ })
449
+ }
450
+
451
+ #[cfg(test)]
452
+ mod tests {
453
+ #[test]
454
+ fn masterdata_session_reuses_tables_and_prepares_raw_authored_cards() {
455
+ let created = super::create(r#"{"region":"cn","revision":"synthetic-v1"}"#).unwrap();
456
+ assert!(created.contains("customProfileTextFonts"));
457
+ let handle = serde_json::from_str::<serde_json::Value>(&created).unwrap()["handle"]
458
+ .as_u64()
459
+ .unwrap() as u32;
460
+ super::put_table(handle, r#"{"name":"customProfileTextFonts","table":[{"id":1,"fontName":"FOT-RodinNTLGPro-DB"}]}"#).unwrap();
461
+ super::put_table(handle, r##"{"name":"customProfileTextColors","table":[{"id":1,"colorCode":"#ffffff"},{"id":2,"colorCode":"#00000000"}]}"##).unwrap();
462
+ super::seal(handle).unwrap();
463
+ let card = serde_json::json!({"texts":[{"objectData":{"layer":1,"lock":false,"position":{"x":0.0,"y":0.0,"z":0.0},"rotation":{"w":1.0,"x":0.0,"y":0.0,"z":0.0},"scale":{"x":1.0,"y":1.0,"z":1.0},"visible":true},"colorId":1,"fontId":1,"lineSpacing":0.0,"outlineColorId":2,"outlineSize":0.0,"size":32.0,"text":"<line-indent=50%>42</line-indent>","type":0}]});
464
+ let prepared = super::prepare(
465
+ handle,
466
+ &serde_json::json!({"documentKey":"session-card","card":card}).to_string(),
467
+ )
468
+ .unwrap();
469
+ assert!(prepared.contains("FZLanTingHei-DB-GBK"));
470
+ let prepared_value: serde_json::Value = serde_json::from_str(&prepared).unwrap();
471
+ let source_key = prepared_value["layout_layers"][0]["dynamicLayerId"]
472
+ .as_str()
473
+ .unwrap();
474
+ let response: serde_json::Value = serde_json::from_str(&super::create_scene(handle, &serde_json::json!({
475
+ "documentKey": "session-card",
476
+ "card": card,
477
+ "frameMode": "final",
478
+ "dynamicPrograms": [{ "layerId": source_key, "percent": 50.0, "lineAdvancesTmp": [[24.0, 24.0]], "rotationDeg": 0.0, "scaleX": 1.0 }]
479
+ }).to_string()).unwrap()).unwrap();
480
+ assert_eq!(
481
+ response["snapshot"]["layer_sources"][0]["line_indent"]["percent"],
482
+ 50.0
483
+ );
484
+ assert!(response["snapshot"]["tick"].as_u64().unwrap() > 0);
485
+ assert_ne!(
486
+ response["snapshot"]["layer_commands"][0]["transform"],
487
+ serde_json::json!({ "dx": 0.0, "dy": 0.0 })
488
+ );
489
+ assert!(super::super::scene::destroy(
490
+ response["handle"].as_u64().unwrap() as u32
491
+ ));
492
+ let stats = super::stats(handle).unwrap();
493
+ assert!(stats.contains("synthetic-v1"));
494
+ assert!(super::destroy(handle));
495
+ }
496
+ }