@empty-sekai/renderer-wasm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib.rs ADDED
@@ -0,0 +1,528 @@
1
+ //! 浏览器 wasm 导出层(emscripten C ABI)。
2
+ //!
3
+ //! wasm-bindgen 不支持 `wasm32-unknown-emscripten`,互操作走 `extern "C"`:
4
+ //! JS 侧用 cwrap 包装,TS wrapper 负责类型与 Worker 调度。
5
+ //!
6
+ //! 导出函数约定:
7
+ //! - 字符串入参为 UTF-8 指针 + 长度;
8
+ //! - 二进制出参经 `alr_alloc`/`alr_free` 管理的线性内存传递;
9
+ //! - 返回 0 表示成功,非 0 为错误码,错误文本经 `alr_last_error` 取回。
10
+
11
+ use std::cell::RefCell;
12
+ use std::collections::HashMap;
13
+ use std::os::raw::{c_char, c_int};
14
+ use std::sync::Arc;
15
+
16
+ use allium_renderer::assets::AssetStore;
17
+ use allium_renderer::renderer::CustomProfileRenderer;
18
+ use allium_renderer_host::JsonMasterDataProvider;
19
+
20
+ // wasm(emscripten)单线程运行,全局可变状态用 thread_local 管理。
21
+ thread_local! {
22
+ static STATE: RefCell<WasmState> = RefCell::new(WasmState::default());
23
+ }
24
+
25
+ #[derive(Default)]
26
+ struct WasmState {
27
+ provider: Option<JsonMasterDataProvider>,
28
+ renderer: Option<CustomProfileRenderer>,
29
+ assets: Option<Arc<AssetStore>>,
30
+ last_error: String,
31
+ /// 待注入的表(renderer 构建前缓存)
32
+ pending_tables: HashMap<String, String>,
33
+ }
34
+
35
+ fn set_error(message: impl Into<String>) -> c_int {
36
+ STATE.with(|state| state.borrow_mut().last_error = message.into());
37
+ 1
38
+ }
39
+
40
+ /// 分配 wasm 线性内存(JS 侧写入入参用)。
41
+ #[no_mangle]
42
+ pub extern "C" fn alr_alloc(size: usize) -> *mut u8 {
43
+ let mut buf = Vec::<u8>::with_capacity(size);
44
+ let ptr = buf.as_mut_ptr();
45
+ std::mem::forget(buf);
46
+ ptr
47
+ }
48
+
49
+ /// 释放 `alr_alloc` 或渲染输出的内存。
50
+ ///
51
+ /// # Safety
52
+ /// `ptr` 必须来自 `alr_alloc(size)` 或本库返回的输出缓冲。
53
+ #[no_mangle]
54
+ pub unsafe extern "C" fn alr_free(ptr: *mut u8, size: usize) {
55
+ if !ptr.is_null() {
56
+ drop(Vec::from_raw_parts(ptr, 0, size));
57
+ }
58
+ }
59
+
60
+ /// 取最近一次错误文本。返回指针 + 写出长度(*len)。
61
+ ///
62
+ /// # Safety
63
+ /// `len` 必须指向合法的 usize。返回的指针在下一次 API 调用前有效。
64
+ #[no_mangle]
65
+ pub unsafe extern "C" fn alr_last_error(len: *mut usize) -> *const c_char {
66
+ STATE.with(|state| {
67
+ let state = state.borrow();
68
+ *len = state.last_error.len();
69
+ state.last_error.as_ptr() as *const c_char
70
+ })
71
+ }
72
+
73
+ unsafe fn slice_arg<'a>(ptr: *const u8, len: usize) -> Result<&'a [u8], String> {
74
+ if ptr.is_null() {
75
+ return Err("空指针入参".into());
76
+ }
77
+ Ok(std::slice::from_raw_parts(ptr, len))
78
+ }
79
+
80
+ unsafe fn str_arg<'a>(ptr: *const u8, len: usize) -> Result<&'a str, String> {
81
+ std::str::from_utf8(slice_arg(ptr, len)?).map_err(|e| format!("入参不是合法 UTF-8: {e}"))
82
+ }
83
+
84
+ /// 注入一张 masterdata 表(JSON 字符串)。需在 `alr_init` 前调用。
85
+ ///
86
+ /// # Safety
87
+ /// 指针/长度必须描述合法的 UTF-8 缓冲。
88
+ #[no_mangle]
89
+ pub unsafe extern "C" fn alr_load_masterdata(
90
+ name_ptr: *const u8,
91
+ name_len: usize,
92
+ json_ptr: *const u8,
93
+ json_len: usize,
94
+ ) -> c_int {
95
+ let (name, json) = match (str_arg(name_ptr, name_len), str_arg(json_ptr, json_len)) {
96
+ (Ok(name), Ok(json)) => (name.to_string(), json.to_string()),
97
+ (Err(e), _) | (_, Err(e)) => return set_error(e),
98
+ };
99
+ STATE.with(|state| {
100
+ state.borrow_mut().pending_tables.insert(name, json);
101
+ });
102
+ 0
103
+ }
104
+
105
+ /// 注册内存字体(family + 字体字节)。
106
+ ///
107
+ /// # Safety
108
+ /// 指针/长度必须描述合法缓冲;family 必须是 UTF-8。
109
+ #[no_mangle]
110
+ pub unsafe extern "C" fn alr_register_font(
111
+ family_ptr: *const u8,
112
+ family_len: usize,
113
+ bytes_ptr: *const u8,
114
+ bytes_len: usize,
115
+ ) -> c_int {
116
+ #[cfg(feature = "skia")]
117
+ {
118
+ let family = match str_arg(family_ptr, family_len) {
119
+ Ok(f) => f.to_string(),
120
+ Err(e) => return set_error(e),
121
+ };
122
+ let bytes = match slice_arg(bytes_ptr, bytes_len) {
123
+ Ok(b) => b.to_vec(),
124
+ Err(e) => return set_error(e),
125
+ };
126
+ allium_renderer::sdf::outline::register_font_bytes(&family, bytes);
127
+ 0
128
+ }
129
+ #[cfg(not(feature = "skia"))]
130
+ {
131
+ let _ = (family_ptr, family_len, bytes_ptr, bytes_len);
132
+ set_error("此构建未启用 skia")
133
+ }
134
+ }
135
+
136
+ /// 用已注入的表初始化渲染器。重复调用会重建(等效热替换 masterdata)。
137
+ #[no_mangle]
138
+ pub extern "C" fn alr_init() -> c_int {
139
+ STATE.with(|state| {
140
+ let mut state = state.borrow_mut();
141
+ let mut provider = JsonMasterDataProvider::empty();
142
+ let tables = std::mem::take(&mut state.pending_tables);
143
+ for (name, json) in &tables {
144
+ if let Err(e) = provider.insert_table(name, json) {
145
+ state.pending_tables = tables.clone();
146
+ state.last_error = e;
147
+ return 1;
148
+ }
149
+ }
150
+ let assets = state
151
+ .assets
152
+ .get_or_insert_with(|| Arc::new(AssetStore::new(128)))
153
+ .clone();
154
+ let missing = provider.missing_tables();
155
+ if !missing.is_empty() {
156
+ // 缺表不是致命错误:对应元素按缺映射渲染。记录供 JS 侧诊断。
157
+ state.last_error = format!("缺失表: {missing:?}");
158
+ }
159
+ let shared = Arc::new(provider);
160
+ match &state.renderer {
161
+ Some(renderer) => renderer.swap_masterdata(shared),
162
+ None => {
163
+ state.renderer = Some(CustomProfileRenderer::new(shared).with_assets(assets));
164
+ }
165
+ }
166
+ state.provider = None;
167
+ 0
168
+ })
169
+ }
170
+
171
+ /// 收集名片所需素材 key,返回 JSON 数组字符串。
172
+ ///
173
+ /// 出参:`out_ptr`/`out_len` 写出缓冲指针与长度,调用方用 `alr_free` 释放。
174
+ ///
175
+ /// # Safety
176
+ /// 所有指针必须合法;card JSON 必须是 UTF-8。
177
+ #[no_mangle]
178
+ pub unsafe extern "C" fn alr_collect_asset_keys(
179
+ card_ptr: *const u8,
180
+ card_len: usize,
181
+ out_ptr: *mut *mut u8,
182
+ out_len: *mut usize,
183
+ ) -> c_int {
184
+ let card_json = match str_arg(card_ptr, card_len) {
185
+ Ok(j) => j,
186
+ Err(e) => return set_error(e),
187
+ };
188
+ let card: allium_renderer::types::CustomProfileCard = match serde_json::from_str(card_json) {
189
+ Ok(card) => card,
190
+ Err(e) => return set_error(format!("解析名片失败: {e}")),
191
+ };
192
+ STATE.with(|state| {
193
+ let state = state.borrow();
194
+ let Some(renderer) = &state.renderer else {
195
+ drop(state);
196
+ return set_error("尚未调用 alr_init");
197
+ };
198
+ let md = renderer.snapshot_masterdata();
199
+ let keys = allium_renderer::asset_keys::collect_card_asset_keys(&card, &md);
200
+ let json = serde_json::to_vec(&keys).unwrap_or_else(|_| b"[]".to_vec());
201
+ write_out(json, out_ptr, out_len);
202
+ 0
203
+ })
204
+ }
205
+
206
+ /// 注入素材(key + 编码图片字节)。
207
+ ///
208
+ /// # Safety
209
+ /// 指针/长度必须描述合法缓冲;key 必须是 UTF-8。
210
+ #[no_mangle]
211
+ pub unsafe extern "C" fn alr_put_asset(
212
+ key_ptr: *const u8,
213
+ key_len: usize,
214
+ bytes_ptr: *const u8,
215
+ bytes_len: usize,
216
+ ) -> c_int {
217
+ let key = match str_arg(key_ptr, key_len) {
218
+ Ok(k) => k.to_string(),
219
+ Err(e) => return set_error(e),
220
+ };
221
+ let bytes = match slice_arg(bytes_ptr, bytes_len) {
222
+ Ok(b) => b.to_vec(),
223
+ Err(e) => return set_error(e),
224
+ };
225
+ STATE.with(|state| {
226
+ let mut state = state.borrow_mut();
227
+ let assets = state
228
+ .assets
229
+ .get_or_insert_with(|| Arc::new(AssetStore::new(128)))
230
+ .clone();
231
+ assets.put(key, bytes);
232
+ 0
233
+ })
234
+ }
235
+
236
+ /// 渲染名片。format:0=JPEG 1=PNG 2=PNG透明底。
237
+ ///
238
+ /// 出参:`out_ptr`/`out_len` 写出编码图片字节,调用方用 `alr_free` 释放。
239
+ ///
240
+ /// # Safety
241
+ /// 所有指针必须合法;card/profile JSON 必须是 UTF-8。profile 可传空指针。
242
+ #[no_mangle]
243
+ pub unsafe extern "C" fn alr_render(
244
+ card_ptr: *const u8,
245
+ card_len: usize,
246
+ profile_ptr: *const u8,
247
+ profile_len: usize,
248
+ format: c_int,
249
+ out_ptr: *mut *mut u8,
250
+ out_len: *mut usize,
251
+ ) -> c_int {
252
+ let card_json = match str_arg(card_ptr, card_len) {
253
+ Ok(j) => j,
254
+ Err(e) => return set_error(e),
255
+ };
256
+ let mut card: allium_renderer::types::CustomProfileCard = match serde_json::from_str(card_json)
257
+ {
258
+ Ok(card) => card,
259
+ Err(e) => return set_error(format!("解析名片失败: {e}")),
260
+ };
261
+ let profile_body: Option<serde_json::Value> = if profile_ptr.is_null() || profile_len == 0 {
262
+ None
263
+ } else {
264
+ match str_arg(profile_ptr, profile_len) {
265
+ Ok(json) => match serde_json::from_str(json) {
266
+ Ok(value) => Some(value),
267
+ Err(e) => return set_error(format!("解析 profile 失败: {e}")),
268
+ },
269
+ Err(e) => return set_error(e),
270
+ }
271
+ };
272
+
273
+ STATE.with(|state| {
274
+ let state = state.borrow();
275
+ let Some(renderer) = &state.renderer else {
276
+ drop(state);
277
+ return set_error("尚未调用 alr_init");
278
+ };
279
+ let profile = profile_body.as_ref().map(|body| {
280
+ let profile = allium_renderer::profile::ProfileData::from_json(body);
281
+ let (honor_levels, bonds_levels, char_ranks) =
282
+ allium_renderer::profile::build_honor_maps(body);
283
+ renderer.enrich_honor_levels(&mut card, &honor_levels, &bonds_levels, &char_ranks);
284
+ profile
285
+ });
286
+ let result = match format {
287
+ 0 => renderer.render_page_with_profile(&card, profile.as_ref()),
288
+ 1 => renderer.render_page_png_with_profile(&card, profile.as_ref()),
289
+ 2 => renderer.render_page_png_transparent_with_profile(&card, profile.as_ref()),
290
+ other => Err(format!("不支持的格式码: {other}")),
291
+ };
292
+ drop(state);
293
+ match result {
294
+ Ok(data) => {
295
+ write_out(data, out_ptr, out_len);
296
+ 0
297
+ }
298
+ Err(e) => set_error(e),
299
+ }
300
+ })
301
+ }
302
+
303
+ /// 分层裁剪渲染:所有可见元素绘到透明画布 → 裁剪不透明包围盒 → WebP 编码。
304
+ ///
305
+ /// 出参:`out_ptr`/`out_len` 写出 WebP 字节(`alr_free` 释放);`out_rect`
306
+ /// 指向 4 个连续 u32,依次写入裁剪框 `x, y, width, height`(画布坐标系)。
307
+ ///
308
+ /// # Safety
309
+ /// 所有指针必须合法;card/profile JSON 必须是 UTF-8;profile 可传空指针。
310
+ /// `out_rect` 必须指向至少 16 字节(4×u32)的可写缓冲。
311
+ #[no_mangle]
312
+ pub unsafe extern "C" fn alr_render_layer_cropped(
313
+ card_ptr: *const u8,
314
+ card_len: usize,
315
+ profile_ptr: *const u8,
316
+ profile_len: usize,
317
+ quality: c_int,
318
+ out_ptr: *mut *mut u8,
319
+ out_len: *mut usize,
320
+ out_rect: *mut u32,
321
+ ) -> c_int {
322
+ #[cfg(feature = "skia")]
323
+ {
324
+ let card_json = match str_arg(card_ptr, card_len) {
325
+ Ok(j) => j,
326
+ Err(e) => return set_error(e),
327
+ };
328
+ let mut card: allium_renderer::types::CustomProfileCard =
329
+ match serde_json::from_str(card_json) {
330
+ Ok(card) => card,
331
+ Err(e) => return set_error(format!("解析名片失败: {e}")),
332
+ };
333
+ let profile_body: Option<serde_json::Value> = if profile_ptr.is_null() || profile_len == 0 {
334
+ None
335
+ } else {
336
+ match str_arg(profile_ptr, profile_len) {
337
+ Ok(json) => match serde_json::from_str(json) {
338
+ Ok(value) => Some(value),
339
+ Err(e) => return set_error(format!("解析 profile 失败: {e}")),
340
+ },
341
+ Err(e) => return set_error(e),
342
+ }
343
+ };
344
+
345
+ STATE.with(|state| {
346
+ let state = state.borrow();
347
+ let Some(renderer) = &state.renderer else {
348
+ drop(state);
349
+ return set_error("尚未调用 alr_init");
350
+ };
351
+ let profile = profile_body.as_ref().map(|body| {
352
+ let profile = allium_renderer::profile::ProfileData::from_json(body);
353
+ let (honor_levels, bonds_levels, char_ranks) =
354
+ allium_renderer::profile::build_honor_maps(body);
355
+ renderer.enrich_honor_levels(&mut card, &honor_levels, &bonds_levels, &char_ranks);
356
+ profile
357
+ });
358
+ let result = renderer.render_element_layer_cropped(
359
+ &card,
360
+ profile.as_ref(),
361
+ quality.max(0) as u32,
362
+ );
363
+ drop(state);
364
+ match result {
365
+ Ok(layer) => {
366
+ *out_rect.add(0) = layer.x;
367
+ *out_rect.add(1) = layer.y;
368
+ *out_rect.add(2) = layer.width;
369
+ *out_rect.add(3) = layer.height;
370
+ write_out(layer.data, out_ptr, out_len);
371
+ 0
372
+ }
373
+ Err(e) => set_error(e),
374
+ }
375
+ })
376
+ }
377
+ #[cfg(not(feature = "skia"))]
378
+ {
379
+ let _ = (
380
+ card_ptr,
381
+ card_len,
382
+ profile_ptr,
383
+ profile_len,
384
+ quality,
385
+ out_ptr,
386
+ out_len,
387
+ out_rect,
388
+ );
389
+ set_error("此构建未启用 skia")
390
+ }
391
+ }
392
+
393
+ /// 批量分层裁剪渲染:所有元素按 layer 升序逐层渲染为裁剪 WebP。
394
+ ///
395
+ /// 出参(一次 FFI 拿全部 N 层):
396
+ /// - `out_meta_ptr/len`:UTF-8 JSON 数组,每项 `{z, type, original_visible, x, y, w, h,
397
+ /// byte_offset, byte_length, properties?}`。`properties` 仅 `include_properties=true`
398
+ /// 时填充;不可见层的 `byte_length=0`。
399
+ /// - `out_blob_ptr/len`:所有可见层 WebP 字节首尾相接的一整块;按 meta 中
400
+ /// `byte_offset`/`byte_length` 切片。
401
+ ///
402
+ /// 两块缓冲都用 `alr_free` 释放。
403
+ ///
404
+ /// # Safety
405
+ /// 所有指针必须合法;card/profile JSON 必须是 UTF-8;profile 可传空指针。
406
+ #[no_mangle]
407
+ pub unsafe extern "C" fn alr_render_all_layers(
408
+ card_ptr: *const u8,
409
+ card_len: usize,
410
+ profile_ptr: *const u8,
411
+ profile_len: usize,
412
+ quality: c_int,
413
+ include_properties: c_int,
414
+ out_meta_ptr: *mut *mut u8,
415
+ out_meta_len: *mut usize,
416
+ out_blob_ptr: *mut *mut u8,
417
+ out_blob_len: *mut usize,
418
+ ) -> c_int {
419
+ #[cfg(feature = "skia")]
420
+ {
421
+ let card_json = match str_arg(card_ptr, card_len) {
422
+ Ok(j) => j,
423
+ Err(e) => return set_error(e),
424
+ };
425
+ let mut card: allium_renderer::types::CustomProfileCard =
426
+ match serde_json::from_str(card_json) {
427
+ Ok(card) => card,
428
+ Err(e) => return set_error(format!("解析名片失败: {e}")),
429
+ };
430
+ let profile_body: Option<serde_json::Value> = if profile_ptr.is_null() || profile_len == 0 {
431
+ None
432
+ } else {
433
+ match str_arg(profile_ptr, profile_len) {
434
+ Ok(json) => match serde_json::from_str(json) {
435
+ Ok(value) => Some(value),
436
+ Err(e) => return set_error(format!("解析 profile 失败: {e}")),
437
+ },
438
+ Err(e) => return set_error(e),
439
+ }
440
+ };
441
+
442
+ STATE.with(|state| {
443
+ let state = state.borrow();
444
+ let Some(renderer) = &state.renderer else {
445
+ drop(state);
446
+ return set_error("尚未调用 alr_init");
447
+ };
448
+ let profile = profile_body.as_ref().map(|body| {
449
+ let profile = allium_renderer::profile::ProfileData::from_json(body);
450
+ let (honor_levels, bonds_levels, char_ranks) =
451
+ allium_renderer::profile::build_honor_maps(body);
452
+ renderer.enrich_honor_levels(&mut card, &honor_levels, &bonds_levels, &char_ranks);
453
+ profile
454
+ });
455
+ let result = renderer.render_all_layers_cropped(
456
+ &card,
457
+ profile.as_ref(),
458
+ quality.max(0) as u32,
459
+ include_properties != 0,
460
+ );
461
+ drop(state);
462
+ match result {
463
+ Ok(layers) => {
464
+ // 拼接 blob + 构造 meta
465
+ let mut blob: Vec<u8> = Vec::new();
466
+ let mut meta_arr: Vec<serde_json::Value> = Vec::with_capacity(layers.len());
467
+ for layer in &layers {
468
+ let byte_offset = blob.len();
469
+ let byte_length = layer.data.len();
470
+ blob.extend_from_slice(&layer.data);
471
+ let mut entry = serde_json::json!({
472
+ "z": layer.z,
473
+ "type": layer.element_type,
474
+ "original_visible": layer.original_visible,
475
+ "x": layer.x,
476
+ "y": layer.y,
477
+ "width": layer.width,
478
+ "height": layer.height,
479
+ "byte_offset": byte_offset,
480
+ "byte_length": byte_length,
481
+ });
482
+ if let Some(props) = &layer.properties {
483
+ entry
484
+ .as_object_mut()
485
+ .unwrap()
486
+ .insert("properties".into(), props.clone());
487
+ }
488
+ meta_arr.push(entry);
489
+ }
490
+ let meta_json = match serde_json::to_vec(&meta_arr) {
491
+ Ok(j) => j,
492
+ Err(e) => return set_error(format!("序列化 meta 失败: {e}")),
493
+ };
494
+ write_out(meta_json, out_meta_ptr, out_meta_len);
495
+ write_out(blob, out_blob_ptr, out_blob_len);
496
+ 0
497
+ }
498
+ Err(e) => set_error(e),
499
+ }
500
+ })
501
+ }
502
+ #[cfg(not(feature = "skia"))]
503
+ {
504
+ let _ = (
505
+ card_ptr,
506
+ card_len,
507
+ profile_ptr,
508
+ profile_len,
509
+ quality,
510
+ include_properties,
511
+ out_meta_ptr,
512
+ out_meta_len,
513
+ out_blob_ptr,
514
+ out_blob_len,
515
+ );
516
+ set_error("此构建未启用 skia")
517
+ }
518
+ }
519
+
520
+ unsafe fn write_out(data: Vec<u8>, out_ptr: *mut *mut u8, out_len: *mut usize) {
521
+ let mut data = data;
522
+ data.shrink_to_fit();
523
+ let len = data.len();
524
+ let ptr = data.as_mut_ptr();
525
+ std::mem::forget(data);
526
+ *out_ptr = ptr;
527
+ *out_len = len;
528
+ }
package/src/main.rs ADDED
@@ -0,0 +1,38 @@
1
+ //! emscripten 链接驱动壳。
2
+ //!
3
+ //! C ABI 导出定义在 `lib.rs`。此处用 `#[path]` 把同一份源**直接编进 bin**
4
+ //! (而非作为 rlib 依赖链接)——否则 thin-LTO 跨 rlib→bin 边界会把
5
+ //! `#[no_mangle]` 符号内部化,导致 wasm-ld 的 `--export=alr_*` 找不到符号。
6
+ //! lib 目标仍保留,供 native `cargo check` 复用。
7
+ //!
8
+ //! emscripten 以 `MODULARIZE` 工厂导出运行时,Module 初始化后不调用 main,
9
+ //! 故 main 仅强引用导出符号防止链接前死代码消除。
10
+
11
+ #[path = "lib.rs"]
12
+ mod exports;
13
+
14
+ /// 强引用导出符号,阻止链接前的死代码消除。
15
+ fn keep_exports() {
16
+ let anchors: &[*const ()] = &[
17
+ exports::alr_alloc as *const (),
18
+ exports::alr_free as *const (),
19
+ exports::alr_last_error as *const (),
20
+ exports::alr_load_masterdata as *const (),
21
+ exports::alr_register_font as *const (),
22
+ exports::alr_init as *const (),
23
+ exports::alr_collect_asset_keys as *const (),
24
+ exports::alr_put_asset as *const (),
25
+ exports::alr_render as *const (),
26
+ exports::alr_render_layer_cropped as *const (),
27
+ exports::alr_render_all_layers as *const (),
28
+ ];
29
+ for &f in anchors {
30
+ unsafe {
31
+ core::ptr::read_volatile(&f);
32
+ }
33
+ }
34
+ }
35
+
36
+ fn main() {
37
+ keep_exports();
38
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Worker 协议:主线程 ↔ 渲染 Worker 的消息类型。
3
+ *
4
+ * 渲染(skia CPU 光栅化)是同步阻塞,放进 Worker 避免卡主线程 UI。
5
+ * 字节经 Transferable(ArrayBuffer)传递,避免结构化克隆复制大缓冲。
6
+ */
7
+
8
+ import { ImageFormat } from "./renderer.js";
9
+
10
+ export { ImageFormat };
11
+ export type { CroppedLayerOutput, LayerCrop } from "./renderer.js";
12
+
13
+ /** 创建 Worker 时传入的初始化参数。 */
14
+ export interface InitPayload {
15
+ /** wasm 工厂模块 URL(Worker 内 `import()` 加载)。 */
16
+ moduleUrl: string;
17
+ /** `.wasm` 文件 URL(可选,默认相对 moduleUrl 解析)。 */
18
+ wasmUrl?: string;
19
+ }
20
+
21
+ /** 一次渲染请求的全部输入(一次性注入,渲染后 Worker 状态可复用)。 */
22
+ export interface RenderRequest {
23
+ cardJson: string;
24
+ profileJson?: string;
25
+ format?: ImageFormat;
26
+ /** 分层裁剪渲染的 WebP 质量(0-100,默认 80)。仅 renderLayerCropped / renderAllLayers 使用。 */
27
+ quality?: number;
28
+ /** renderAllLayers 是否填充每层 properties(默认 true)。 */
29
+ includeProperties?: boolean;
30
+ /** masterdata 表:name → JSON 文本。 */
31
+ masterData: Record<string, string>;
32
+ /** 字体:family → 字节。 */
33
+ fonts: Array<{ family: string; bytes: Uint8Array }>;
34
+ /** 素材:key → 字节。未提供的 key 渲染时按缺素材处理。 */
35
+ assets: Array<{ key: string; bytes: Uint8Array }>;
36
+ }
37
+
38
+ export type RequestMessage =
39
+ | { id: number; kind: "init"; payload: InitPayload }
40
+ | { id: number; kind: "render"; payload: RenderRequest }
41
+ | { id: number; kind: "renderLayerCropped"; payload: RenderRequest }
42
+ | { id: number; kind: "renderAllLayers"; payload: RenderRequest }
43
+ | { id: number; kind: "collectAssetKeys"; payload: { cardJson: string; masterData: Record<string, string> } };
44
+
45
+ export type ResponseMessage =
46
+ | { id: number; ok: true; result?: unknown }
47
+ | { id: number; ok: false; error: string };