@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/scene.rs ADDED
@@ -0,0 +1,665 @@
1
+ use std::cell::RefCell;
2
+ use std::collections::BTreeMap;
3
+
4
+ use allium_renderer_core::{
5
+ AuthoredElementKind, BlendMode, GlyphSource, InteractionRegionSource, LayerId, LayerKind,
6
+ LayerSource, LineIndentSource, Matrix2d, ParameterValue, Quad, Rect, RenderMaskOverride, Scene,
7
+ SceneSource, SemanticCommandPayload, SemanticCommandSource, StableId,
8
+ };
9
+ use serde::{Deserialize, Serialize};
10
+
11
+ thread_local! {
12
+ static SCENES: RefCell<SceneTable> = RefCell::new(SceneTable::default());
13
+ }
14
+
15
+ #[derive(Default)]
16
+ struct SceneTable {
17
+ next_handle: u32,
18
+ scenes: BTreeMap<u32, Scene>,
19
+ }
20
+
21
+ #[derive(Serialize)]
22
+ struct CreateResponse {
23
+ handle: u32,
24
+ layer_bindings: Vec<LayerBinding>,
25
+ snapshot: allium_renderer_core::SceneSnapshot,
26
+ }
27
+
28
+ #[derive(Serialize)]
29
+ struct LayerBinding {
30
+ source_key: String,
31
+ layer_id: LayerId,
32
+ }
33
+
34
+ #[derive(Deserialize)]
35
+ struct MaskRequest {
36
+ layer_id: LayerId,
37
+ visible: bool,
38
+ }
39
+
40
+ #[derive(Deserialize)]
41
+ struct BulkMaskRequest {
42
+ expected_layer_table_revision: u64,
43
+ overrides: Vec<MaskOverrideRequest>,
44
+ }
45
+
46
+ #[derive(Deserialize)]
47
+ struct MaskOverrideRequest {
48
+ layer_id: LayerId,
49
+ mask_override: RenderMaskOverride,
50
+ }
51
+
52
+ #[derive(Deserialize)]
53
+ struct TabRequest {
54
+ control_id: StableId,
55
+ value: String,
56
+ }
57
+
58
+ #[derive(Deserialize)]
59
+ struct ScrollRequest {
60
+ control_id: StableId,
61
+ #[serde(default)]
62
+ offset: Option<f32>,
63
+ #[serde(default)]
64
+ delta: Option<f32>,
65
+ }
66
+
67
+ #[derive(Deserialize)]
68
+ #[serde(rename_all = "camelCase")]
69
+ struct SceneCreateInput {
70
+ scene_key: String,
71
+ region: String,
72
+ font_engine_fingerprint: String,
73
+ raster_contract: String,
74
+ layers: Vec<LayerCreateInput>,
75
+ #[serde(default)]
76
+ glyphs: Vec<GlyphCreateInput>,
77
+ #[serde(default)]
78
+ semantic_commands: Vec<SemanticCommandCreateInput>,
79
+ #[serde(default)]
80
+ interaction_regions: Vec<InteractionRegionCreateInput>,
81
+ #[serde(default)]
82
+ component_controls: Vec<allium_renderer_core::profile_scene::ComponentControlSource>,
83
+ }
84
+
85
+ #[derive(Deserialize)]
86
+ #[serde(rename_all = "camelCase")]
87
+ struct ResolvedProfileSceneCreateInput {
88
+ document_key: String,
89
+ #[serde(default = "default_region")]
90
+ region: String,
91
+ card: allium_renderer_core::profile_source::CustomProfileCard,
92
+ snapshot: allium_renderer_core::profile_scene::ProfileResolveSnapshot,
93
+ }
94
+
95
+ fn default_region() -> String {
96
+ "cn".into()
97
+ }
98
+
99
+ #[derive(Deserialize)]
100
+ #[serde(rename_all = "camelCase")]
101
+ struct SemanticCommandCreateInput {
102
+ layer_source_key: String,
103
+ ordinal: u32,
104
+ role: String,
105
+ bounds: Rect,
106
+ matrix: Matrix2d,
107
+ hit_geometry: Quad,
108
+ blend_mode: BlendMode,
109
+ #[serde(default)]
110
+ clip: Option<Quad>,
111
+ #[serde(default)]
112
+ control_bindings: Vec<allium_renderer_core::CommandControlBinding>,
113
+ #[serde(default)]
114
+ metadata: BTreeMap<String, ParameterValue>,
115
+ payload: SemanticCommandPayload,
116
+ }
117
+
118
+ #[derive(Deserialize)]
119
+ #[serde(rename_all = "camelCase")]
120
+ struct InteractionRegionCreateInput {
121
+ layer_source_key: String,
122
+ ordinal: u32,
123
+ role: String,
124
+ bounds: Rect,
125
+ quad: Quad,
126
+ matrix: Matrix2d,
127
+ hit_geometry: Quad,
128
+ #[serde(default)]
129
+ clip: Option<Quad>,
130
+ #[serde(default)]
131
+ control_bindings: Vec<allium_renderer_core::CommandControlBinding>,
132
+ #[serde(default)]
133
+ resolved_data: BTreeMap<String, ParameterValue>,
134
+ #[serde(default)]
135
+ capabilities: Vec<String>,
136
+ }
137
+
138
+ #[derive(Deserialize)]
139
+ #[serde(rename_all = "camelCase")]
140
+ struct GlyphCreateInput {
141
+ layer_source_key: String,
142
+ output_ordinal: u32,
143
+ source_span: [u32; 2],
144
+ bounds: Rect,
145
+ quad: Quad,
146
+ }
147
+
148
+ #[derive(Deserialize)]
149
+ #[serde(rename_all = "camelCase")]
150
+ struct LayerCreateInput {
151
+ source_key: String,
152
+ parent_source_key: Option<String>,
153
+ kind: LayerKind,
154
+ #[serde(default)]
155
+ authored_kind: AuthoredElementKind,
156
+ #[serde(default)]
157
+ authored_index: u32,
158
+ z: i32,
159
+ authored_visible: bool,
160
+ source_content: String,
161
+ #[serde(default)]
162
+ resolved_parameters: BTreeMap<String, ParameterValue>,
163
+ bounds: Rect,
164
+ quad: Quad,
165
+ matrix: Matrix2d,
166
+ hit_geometry: Quad,
167
+ line_indent: Option<LineIndentCreateInput>,
168
+ }
169
+
170
+ #[derive(Deserialize)]
171
+ #[serde(rename_all = "camelCase")]
172
+ struct LineIndentCreateInput {
173
+ percent: f32,
174
+ line_advances_tmp: Vec<Vec<f32>>,
175
+ rotation_deg: f32,
176
+ scale_x: f32,
177
+ }
178
+
179
+ pub fn create(input: &str) -> Result<String, String> {
180
+ let input: SceneCreateInput =
181
+ serde_json::from_str(input).map_err(|error| format!("parse core scene failed: {error}"))?;
182
+ let scene_id = StableId::derive("scene-v1", input.scene_key.as_bytes());
183
+ let ids = input
184
+ .layers
185
+ .iter()
186
+ .map(|layer| {
187
+ let key = format!("{}\0{}", input.scene_key, layer.source_key);
188
+ (
189
+ layer.source_key.clone(),
190
+ StableId::derive("layer-v1", key.as_bytes()),
191
+ )
192
+ })
193
+ .collect::<BTreeMap<_, _>>();
194
+ let layer_bindings = ids
195
+ .iter()
196
+ .map(|(source_key, layer_id)| LayerBinding {
197
+ source_key: source_key.clone(),
198
+ layer_id: *layer_id,
199
+ })
200
+ .collect();
201
+ let layers = input
202
+ .layers
203
+ .into_iter()
204
+ .map(|layer| {
205
+ let id = ids[&layer.source_key];
206
+ let parent_id = layer
207
+ .parent_source_key
208
+ .as_ref()
209
+ .and_then(|key| ids.get(key))
210
+ .copied();
211
+ LayerSource {
212
+ id,
213
+ parent_id,
214
+ kind: layer.kind,
215
+ authored_kind: layer.authored_kind,
216
+ authored_index: layer.authored_index,
217
+ game_layer: layer.z,
218
+ z: layer.z,
219
+ authored_visible: layer.authored_visible,
220
+ source_content: layer.source_content,
221
+ resolved_parameters: layer.resolved_parameters,
222
+ bounds: layer.bounds,
223
+ quad: layer.quad,
224
+ matrix: layer.matrix,
225
+ hit_geometry: layer.hit_geometry,
226
+ line_indent: layer.line_indent.map(|program| LineIndentSource {
227
+ percent: program.percent,
228
+ line_advances_tmp: program.line_advances_tmp,
229
+ rotation_deg: program.rotation_deg,
230
+ scale_x: program.scale_x,
231
+ }),
232
+ }
233
+ })
234
+ .collect();
235
+ let glyphs = input
236
+ .glyphs
237
+ .into_iter()
238
+ .map(|glyph| {
239
+ let layer_id = ids[&glyph.layer_source_key];
240
+ let key = format!(
241
+ "{}\0{}\0{}\0{}:{}",
242
+ input.scene_key,
243
+ glyph.layer_source_key,
244
+ glyph.output_ordinal,
245
+ glyph.source_span[0],
246
+ glyph.source_span[1],
247
+ );
248
+ GlyphSource {
249
+ id: StableId::derive("glyph-instance-v1", key.as_bytes()),
250
+ layer_id,
251
+ output_ordinal: glyph.output_ordinal,
252
+ source_span: glyph.source_span,
253
+ bounds: glyph.bounds,
254
+ quad: glyph.quad,
255
+ }
256
+ })
257
+ .collect();
258
+ let semantic_commands = input
259
+ .semantic_commands
260
+ .into_iter()
261
+ .map(|command| {
262
+ let layer_id = ids[&command.layer_source_key];
263
+ let key = format!(
264
+ "{}\0{}\0{}\0{}",
265
+ input.scene_key, command.layer_source_key, command.role, command.ordinal
266
+ );
267
+ SemanticCommandSource {
268
+ id: StableId::derive("semantic-command-v1", key.as_bytes()),
269
+ layer_id,
270
+ role: command.role,
271
+ bounds: command.bounds,
272
+ matrix: command.matrix,
273
+ hit_geometry: command.hit_geometry,
274
+ blend_mode: command.blend_mode,
275
+ clip: command.clip,
276
+ control_bindings: command.control_bindings,
277
+ metadata: command.metadata,
278
+ numeric_text_runs: allium_renderer_core::tmp_text::numeric_text_runs(
279
+ match &command.payload {
280
+ SemanticCommandPayload::Text { source, .. } => match source {
281
+ allium_renderer_core::TextSource::Authored { value }
282
+ | allium_renderer_core::TextSource::ProfileField { value, .. }
283
+ | allium_renderer_core::TextSource::MasterData { value, .. }
284
+ | allium_renderer_core::TextSource::Localized { value, .. } => value,
285
+ },
286
+ _ => "",
287
+ },
288
+ ),
289
+ render_placement: None,
290
+ payload: command.payload,
291
+ }
292
+ })
293
+ .collect();
294
+ let interaction_regions = input
295
+ .interaction_regions
296
+ .into_iter()
297
+ .map(|region| {
298
+ let layer_id = ids[&region.layer_source_key];
299
+ let key = format!(
300
+ "{}\0{}\0{}\0{}",
301
+ input.scene_key, region.layer_source_key, region.role, region.ordinal
302
+ );
303
+ InteractionRegionSource {
304
+ id: StableId::derive("interaction-region-v1", key.as_bytes()),
305
+ layer_id,
306
+ role: region.role,
307
+ bounds: region.bounds,
308
+ quad: region.quad,
309
+ matrix: region.matrix,
310
+ hit_geometry: region.hit_geometry,
311
+ clip: region.clip,
312
+ control_bindings: region.control_bindings,
313
+ resolved_data: region.resolved_data,
314
+ capabilities: region.capabilities,
315
+ }
316
+ })
317
+ .collect();
318
+ let source = SceneSource {
319
+ scene_id,
320
+ region: input.region,
321
+ font_engine_fingerprint: input.font_engine_fingerprint,
322
+ raster_contract: input.raster_contract,
323
+ layers,
324
+ glyphs,
325
+ semantic_commands,
326
+ interaction_regions,
327
+ component_controls: input.component_controls,
328
+ };
329
+ let scene = Scene::new(source).map_err(|error| error.to_string())?;
330
+ SCENES.with(|table| {
331
+ let mut table = table.borrow_mut();
332
+ table.next_handle = table.next_handle.wrapping_add(1).max(1);
333
+ let handle = table.next_handle;
334
+ let snapshot = scene.snapshot();
335
+ table.scenes.insert(handle, scene);
336
+ serde_json::to_string(&CreateResponse {
337
+ handle,
338
+ layer_bindings,
339
+ snapshot,
340
+ })
341
+ .map_err(|error| error.to_string())
342
+ })
343
+ }
344
+
345
+ pub fn create_resolved_profile(input: &str) -> Result<String, String> {
346
+ let input: ResolvedProfileSceneCreateInput = serde_json::from_str(input)
347
+ .map_err(|error| format!("parse resolved profile scene failed: {error}"))?;
348
+ let resolved = allium_renderer_core::profile_scene::resolve_profile_scene(
349
+ &input.card,
350
+ &input.document_key,
351
+ &input.snapshot,
352
+ )
353
+ .map_err(|error| error.to_string())?;
354
+ create_compiled_profile(&input.document_key, &input.region, resolved, false)
355
+ }
356
+
357
+ pub fn create_compiled_profile(
358
+ document_key: &str,
359
+ region: &str,
360
+ resolved: allium_renderer_core::profile_scene::ResolvedProfileScene,
361
+ static_final: bool,
362
+ ) -> Result<String, String> {
363
+ let layer_bindings = resolved
364
+ .layers
365
+ .iter()
366
+ .map(|layer| LayerBinding {
367
+ source_key: format!(
368
+ "{}:{}",
369
+ allium_renderer_core::profile_scene::authored_kind_name(layer.authored_kind),
370
+ layer.authored_index
371
+ ),
372
+ layer_id: layer.id,
373
+ })
374
+ .collect();
375
+ let mut scene = Scene::new(SceneSource {
376
+ scene_id: StableId::derive("profile-scene-v2", document_key.as_bytes()),
377
+ region: region.to_owned(),
378
+ font_engine_fingerprint: "freetype-wasm-profile-v2".into(),
379
+ raster_contract: "sdf-edt-v2".into(),
380
+ layers: resolved.layers,
381
+ glyphs: Vec::new(),
382
+ semantic_commands: resolved.commands,
383
+ interaction_regions: resolved.interaction_regions,
384
+ component_controls: resolved.controls,
385
+ })
386
+ .map_err(|error| error.to_string())?;
387
+ if static_final {
388
+ scene.advance_to_static_final();
389
+ }
390
+ SCENES.with(|table| {
391
+ let mut table = table.borrow_mut();
392
+ table.next_handle = table.next_handle.wrapping_add(1).max(1);
393
+ let handle = table.next_handle;
394
+ let snapshot = scene.snapshot();
395
+ table.scenes.insert(handle, scene);
396
+ serde_json::to_string(&CreateResponse {
397
+ handle,
398
+ layer_bindings,
399
+ snapshot,
400
+ })
401
+ .map_err(|error| error.to_string())
402
+ })
403
+ }
404
+
405
+ pub fn advance(handle: u32, tick: u64) -> Result<String, String> {
406
+ with_scene(handle, |scene| {
407
+ serde_json::to_string(&scene.advance_to_tick(tick)).map_err(|error| error.to_string())
408
+ })
409
+ }
410
+
411
+ pub unsafe fn advance_binary(
412
+ handle: u32,
413
+ tick: u64,
414
+ output: *mut u8,
415
+ capacity: usize,
416
+ ) -> Result<usize, String> {
417
+ with_scene(handle, |scene| {
418
+ let maximum = 40 + scene.layer_count() * 24;
419
+ if output.is_null() || capacity < maximum {
420
+ return Err(format!(
421
+ "core delta buffer too small: capacity={capacity}, required={maximum}"
422
+ ));
423
+ }
424
+ let delta = scene.advance_to_tick(tick);
425
+ let required = 40 + delta.patches.len() * 24;
426
+ let bytes = unsafe { std::slice::from_raw_parts_mut(output, required) };
427
+ bytes.fill(0);
428
+ put_u32(bytes, 0, 0x3144_4341);
429
+ put_u16(bytes, 4, delta.schema_major);
430
+ put_u16(bytes, 6, delta.schema_minor);
431
+ let dirty = (delta.dirty.mask as u32)
432
+ | ((delta.dirty.transform as u32) << 1)
433
+ | ((delta.dirty.material as u32) << 2)
434
+ | ((delta.dirty.layout as u32) << 3)
435
+ | ((delta.dirty.command as u32) << 4)
436
+ | ((delta.dirty.atlas as u32) << 5);
437
+ put_u32(bytes, 8, dirty);
438
+ put_u32(bytes, 12, delta.patches.len() as u32);
439
+ put_u64(bytes, 16, delta.tick);
440
+ put_u64(bytes, 24, delta.base_revision);
441
+ put_u64(bytes, 32, delta.revision);
442
+ for (index, patch) in delta.patches.iter().enumerate() {
443
+ let offset = 40 + index * 24;
444
+ put_u64(bytes, offset, patch.layer_id.0);
445
+ let flags =
446
+ (patch.render_mask.is_some() as u32) | ((patch.transform.is_some() as u32) << 1);
447
+ put_u32(bytes, offset + 8, flags);
448
+ put_u32(
449
+ bytes,
450
+ offset + 12,
451
+ patch.render_mask.unwrap_or(false) as u32,
452
+ );
453
+ let transform = patch.transform.unwrap_or_default();
454
+ put_f32(bytes, offset + 16, transform.dx);
455
+ put_f32(bytes, offset + 20, transform.dy);
456
+ }
457
+ Ok(required)
458
+ })
459
+ }
460
+
461
+ fn put_u16(output: &mut [u8], offset: usize, value: u16) {
462
+ output[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
463
+ }
464
+
465
+ fn put_u32(output: &mut [u8], offset: usize, value: u32) {
466
+ output[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
467
+ }
468
+
469
+ fn put_u64(output: &mut [u8], offset: usize, value: u64) {
470
+ output[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
471
+ }
472
+
473
+ fn put_f32(output: &mut [u8], offset: usize, value: f32) {
474
+ output[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
475
+ }
476
+
477
+ pub fn set_mask(handle: u32, input: &str) -> Result<String, String> {
478
+ let request: MaskRequest = serde_json::from_str(input)
479
+ .map_err(|error| format!("parse mask request failed: {error}"))?;
480
+ with_scene(handle, |scene| {
481
+ let delta = scene
482
+ .set_render_mask(request.layer_id, request.visible)
483
+ .map_err(|error| error.to_string())?;
484
+ serde_json::to_string(&delta).map_err(|error| error.to_string())
485
+ })
486
+ }
487
+
488
+ pub fn set_masks(handle: u32, input: &str) -> Result<String, String> {
489
+ let request: BulkMaskRequest = serde_json::from_str(input)
490
+ .map_err(|error| format!("parse bulk mask request failed: {error}"))?;
491
+ let overrides = request
492
+ .overrides
493
+ .into_iter()
494
+ .map(|value| (value.layer_id, value.mask_override))
495
+ .collect::<Vec<_>>();
496
+ with_scene(handle, |scene| {
497
+ let delta = scene
498
+ .set_render_mask_overrides(request.expected_layer_table_revision, &overrides)
499
+ .map_err(|error| error.to_string())?;
500
+ serde_json::to_string(&delta).map_err(|error| error.to_string())
501
+ })
502
+ }
503
+
504
+ pub fn set_tab(handle: u32, input: &str) -> Result<String, String> {
505
+ let request: TabRequest = serde_json::from_str(input)
506
+ .map_err(|error| format!("parse tab request failed: {error}"))?;
507
+ with_scene(handle, |scene| {
508
+ let delta = scene
509
+ .set_tab(request.control_id, &request.value)
510
+ .map_err(|error| error.to_string())?;
511
+ serde_json::to_string(&delta).map_err(|error| error.to_string())
512
+ })
513
+ }
514
+
515
+ pub fn scroll(handle: u32, input: &str) -> Result<String, String> {
516
+ let request: ScrollRequest = serde_json::from_str(input)
517
+ .map_err(|error| format!("parse scroll request failed: {error}"))?;
518
+ with_scene(handle, |scene| {
519
+ let delta = match (request.offset, request.delta) {
520
+ (Some(offset), None) => scene.set_scroll_offset(request.control_id, offset),
521
+ (None, Some(delta)) => scene.scroll_by(request.control_id, delta),
522
+ _ => return Err("scroll request requires exactly one of offset or delta".into()),
523
+ }
524
+ .map_err(|error| error.to_string())?;
525
+ serde_json::to_string(&delta).map_err(|error| error.to_string())
526
+ })
527
+ }
528
+
529
+ pub fn dump(handle: u32) -> Result<String, String> {
530
+ with_scene(handle, |scene| {
531
+ serde_json::to_string(&scene.dump()).map_err(|error| error.to_string())
532
+ })
533
+ }
534
+
535
+ pub fn destroy(handle: u32) -> bool {
536
+ SCENES.with(|table| table.borrow_mut().scenes.remove(&handle).is_some())
537
+ }
538
+
539
+ fn with_scene<T>(
540
+ handle: u32,
541
+ call: impl FnOnce(&mut Scene) -> Result<T, String>,
542
+ ) -> Result<T, String> {
543
+ SCENES.with(|table| {
544
+ let mut table = table.borrow_mut();
545
+ let scene = table
546
+ .scenes
547
+ .get_mut(&handle)
548
+ .ok_or_else(|| format!("unknown core scene handle {handle}"))?;
549
+ call(scene)
550
+ })
551
+ }
552
+
553
+ #[cfg(test)]
554
+ mod tests {
555
+ use super::*;
556
+ use serde_json::{json, Value};
557
+
558
+ #[test]
559
+ fn wasm_create_and_dump_preserve_tab_and_scroll_region_bindings() {
560
+ let scene_key = "interaction-contract";
561
+ let layer_key = "component";
562
+ let layer_id = StableId::derive("layer-v1", format!("{scene_key}\0{layer_key}").as_bytes());
563
+ let tab_id = StableId(0x6100);
564
+ let scroll_id = StableId(0x6200);
565
+ let rect = json!({ "x": 0.0, "y": 0.0, "width": 20.0, "height": 20.0 });
566
+ let quad = json!([[0.0, 0.0], [20.0, 0.0], [20.0, 20.0], [0.0, 20.0]]);
567
+ let input = json!({
568
+ "sceneKey": scene_key,
569
+ "region": "cn",
570
+ "fontEngineFingerprint": "ft",
571
+ "rasterContract": "sdf",
572
+ "layers": [{
573
+ "sourceKey": layer_key,
574
+ "parentSourceKey": null,
575
+ "kind": "shape",
576
+ "z": 0,
577
+ "authoredVisible": true,
578
+ "sourceContent": "",
579
+ "bounds": rect,
580
+ "quad": quad,
581
+ "matrix": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
582
+ "hitGeometry": quad,
583
+ "lineIndent": null
584
+ }],
585
+ "interactionRegions": [
586
+ {
587
+ "layerSourceKey": layer_key,
588
+ "ordinal": 0,
589
+ "role": "rank-tab",
590
+ "bounds": rect,
591
+ "quad": quad,
592
+ "matrix": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
593
+ "hitGeometry": quad,
594
+ "controlBindings": [{
595
+ "kind": "tab_option",
596
+ "control_id": tab_id,
597
+ "value": "rank"
598
+ }],
599
+ "capabilities": ["activate"]
600
+ },
601
+ {
602
+ "layerSourceKey": layer_key,
603
+ "ordinal": 1,
604
+ "role": "rank-list",
605
+ "bounds": rect,
606
+ "quad": quad,
607
+ "matrix": [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
608
+ "hitGeometry": quad,
609
+ "controlBindings": [{
610
+ "kind": "scroll_content",
611
+ "control_id": scroll_id
612
+ }],
613
+ "capabilities": ["inspect"]
614
+ }
615
+ ],
616
+ "componentControls": [
617
+ {
618
+ "id": tab_id,
619
+ "layer_id": layer_id,
620
+ "role": "mode",
621
+ "state": { "kind": "tabs", "options": ["rank", "challenge"], "active": "rank" }
622
+ },
623
+ {
624
+ "id": scroll_id,
625
+ "layer_id": layer_id,
626
+ "role": "list",
627
+ "state": {
628
+ "kind": "scroll",
629
+ "offset": 0.0,
630
+ "min": 0.0,
631
+ "max": 100.0,
632
+ "viewport_extent": 100.0,
633
+ "content_extent": 200.0,
634
+ "step": 20.0
635
+ }
636
+ }
637
+ ]
638
+ });
639
+ let created: Value = serde_json::from_str(&create(&input.to_string()).unwrap()).unwrap();
640
+ let handle = created["handle"].as_u64().unwrap() as u32;
641
+ let snapshot_regions = created["snapshot"]["interaction_regions"]
642
+ .as_array()
643
+ .unwrap();
644
+ assert_eq!(
645
+ snapshot_regions[0]["control_bindings"][0]["kind"],
646
+ "tab_option"
647
+ );
648
+ assert_eq!(
649
+ snapshot_regions[1]["control_bindings"][0]["kind"],
650
+ "scroll_content"
651
+ );
652
+
653
+ let dumped: Value = serde_json::from_str(&dump(handle).unwrap()).unwrap();
654
+ let dump_regions = dumped["interaction_regions"].as_array().unwrap();
655
+ assert_eq!(
656
+ dump_regions[0]["control_bindings"],
657
+ snapshot_regions[0]["control_bindings"]
658
+ );
659
+ assert_eq!(
660
+ dump_regions[1]["control_bindings"],
661
+ snapshot_regions[1]["control_bindings"]
662
+ );
663
+ assert!(destroy(handle));
664
+ }
665
+ }