diff --git a/packages/platform-common/module.json b/packages/platform-common/module.json new file mode 100644 index 00000000..085be085 --- /dev/null +++ b/packages/platform-common/module.json @@ -0,0 +1,21 @@ +{ + "id": "platform-common", + "name": "@esengine/platform-common", + "displayName": "Platform Common", + "description": "Common platform interfaces | 平台通用接口定义", + "version": "1.0.0", + "category": "Core", + "icon": "Layers", + "tags": ["platform", "common", "interface"], + "isCore": true, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": ["web", "desktop", "mobile"], + "dependencies": [], + "exports": { + "other": ["WasmLibraryLoaderFactory", "IPlatformAdapter"] + }, + "requiresWasm": false, + "outputPath": "dist/index.mjs" +} diff --git a/packages/platform-common/src/index.ts b/packages/platform-common/src/index.ts index 7ecbd62d..ff570888 100644 --- a/packages/platform-common/src/index.ts +++ b/packages/platform-common/src/index.ts @@ -40,3 +40,50 @@ export type { // 系统信息 SystemInfo } from './IPlatformSubsystems'; + +// WASM 库加载器 +export { + PlatformType, + WasmLibraryLoaderFactory +} from './wasm'; + +export type { + WasmLibraryConfig, + PlatformInfo, + IWasmLibraryLoader, + IPlatformWasmLoader +} from './wasm'; + +// Polyfills +export { + installTextDecoderPolyfill, + installTextEncoderPolyfill, + isTextDecoderAvailable, + isTextEncoderAvailable, + installAllPolyfills, + getRequiredPolyfills, + TextDecoderPolyfill, + TextEncoderPolyfill +} from './polyfills'; + +/** + * 检测是否在编辑器环境(Tauri 桌面应用) + * Detect if running in editor environment (Tauri desktop app) + */ +export function isEditorEnvironment(): boolean { + if (typeof window === 'undefined') { + return false; + } + + // Tauri 桌面应用 | Tauri desktop app + if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) { + return true; + } + + // 编辑器标记 | Editor marker + if ('__ESENGINE_EDITOR__' in window) { + return true; + } + + return false; +} diff --git a/packages/platform-common/src/polyfills/TextDecoderPolyfill.ts b/packages/platform-common/src/polyfills/TextDecoderPolyfill.ts new file mode 100644 index 00000000..ebee96d7 --- /dev/null +++ b/packages/platform-common/src/polyfills/TextDecoderPolyfill.ts @@ -0,0 +1,235 @@ +/** + * TextDecoder polyfill + * + * 用于不原生支持 TextDecoder 的平台(如微信小游戏 iOS 环境) + * + * 支持以下编码格式: + * - UTF-8(包含1-4字节字符、代理对) + * - ASCII + * - UTF-16LE + */ +class TextDecoderPolyfill { + /** + * 编码格式 + */ + readonly encoding: string; + + /** + * 是否在遇到无效序列时抛出错误 + */ + readonly fatal: boolean = false; + + /** + * 是否忽略 BOM(字节顺序标记) + */ + readonly ignoreBOM: boolean = false; + + /** + * 创建 TextDecoder 实例 + * + * @param encoding - 编码格式,默认 'utf-8' + * @param options - 解码选项 + */ + constructor(encoding: string = 'utf-8', options?: TextDecoderOptions) { + this.encoding = encoding.toLowerCase().replace('-', ''); + if (options?.fatal) { + this.fatal = options.fatal; + } + if (options?.ignoreBOM) { + this.ignoreBOM = options.ignoreBOM; + } + } + + /** + * 将二进制数据解码为字符串 + * + * @param input - 要解码的二进制数据 + * @param options - 解码选项 + * @returns 解码后的字符串 + */ + decode(input?: BufferSource | null, options?: TextDecodeOptions): string { + if (!input) return ''; + + const bytes = input instanceof Uint8Array + ? input + : input instanceof ArrayBuffer + ? new Uint8Array(input) + : new Uint8Array(input.buffer, input.byteOffset, input.byteLength); + + if (this.encoding === 'utf8' || this.encoding === 'utf-8') { + return this.decodeUTF8(bytes); + } + + if (this.encoding === 'ascii' || this.encoding === 'usascii') { + return this.decodeASCII(bytes); + } + + if (this.encoding === 'utf16le' || this.encoding === 'utf-16le') { + return this.decodeUTF16LE(bytes); + } + + // 降级:作为 ASCII 处理 + return this.decodeASCII(bytes); + } + + /** + * 解码 UTF-8 数据 + * + * @param bytes - 字节数组 + * @returns 解码后的字符串 + */ + private decodeUTF8(bytes: Uint8Array): string { + const result: string[] = []; + let i = 0; + + // 跳过 BOM(如果存在且不忽略) + if (!this.ignoreBOM && bytes.length >= 3 && + bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) { + i = 3; + } + + while (i < bytes.length) { + const byte1 = bytes[i++]; + + if (byte1 < 0x80) { + // 1字节字符(ASCII: 0xxxxxxx) + result.push(String.fromCharCode(byte1)); + } else if ((byte1 & 0xE0) === 0xC0) { + // 2字节字符(110xxxxx 10xxxxxx) + if (i >= bytes.length) { + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + break; + } + const byte2 = bytes[i++]; + if ((byte2 & 0xC0) !== 0x80) { + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + i--; + continue; + } + result.push(String.fromCharCode( + ((byte1 & 0x1F) << 6) | (byte2 & 0x3F) + )); + } else if ((byte1 & 0xF0) === 0xE0) { + // 3字节字符(1110xxxx 10xxxxxx 10xxxxxx) + if (i + 1 >= bytes.length) { + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + break; + } + const byte2 = bytes[i++]; + const byte3 = bytes[i++]; + if ((byte2 & 0xC0) !== 0x80 || (byte3 & 0xC0) !== 0x80) { + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + i -= 2; + continue; + } + result.push(String.fromCharCode( + ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F) + )); + } else if ((byte1 & 0xF8) === 0xF0) { + // 4字节字符(11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) + if (i + 2 >= bytes.length) { + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + break; + } + const byte2 = bytes[i++]; + const byte3 = bytes[i++]; + const byte4 = bytes[i++]; + if ((byte2 & 0xC0) !== 0x80 || (byte3 & 0xC0) !== 0x80 || (byte4 & 0xC0) !== 0x80) { + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + i -= 3; + continue; + } + // 计算码点并转换为代理对 + const codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | + ((byte3 & 0x3F) << 6) | (byte4 & 0x3F); + if (codePoint > 0x10FFFF) { + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + continue; + } + const surrogate = codePoint - 0x10000; + result.push( + String.fromCharCode(0xD800 + (surrogate >> 10)), + String.fromCharCode(0xDC00 + (surrogate & 0x3FF)) + ); + } else { + // 无效字节 + if (this.fatal) throw new TypeError('无效的 UTF-8 序列'); + result.push('\uFFFD'); + } + } + + return result.join(''); + } + + /** + * 解码 ASCII 数据 + * + * @param bytes - 字节数组 + * @returns 解码后的字符串 + */ + private decodeASCII(bytes: Uint8Array): string { + const result: string[] = []; + for (let i = 0; i < bytes.length; i++) { + result.push(String.fromCharCode(bytes[i] & 0x7F)); + } + return result.join(''); + } + + /** + * 解码 UTF-16LE 数据 + * + * @param bytes - 字节数组 + * @returns 解码后的字符串 + */ + private decodeUTF16LE(bytes: Uint8Array): string { + const result: string[] = []; + + // 跳过 BOM(如果存在) + let i = 0; + if (!this.ignoreBOM && bytes.length >= 2 && + bytes[0] === 0xFF && bytes[1] === 0xFE) { + i = 2; + } + + for (; i + 1 < bytes.length; i += 2) { + const codeUnit = bytes[i] | (bytes[i + 1] << 8); + result.push(String.fromCharCode(codeUnit)); + } + + return result.join(''); + } +} + +/** + * 安装 TextDecoder polyfill + * + * 如果当前环境不支持 TextDecoder,则安装 polyfill + * + * @returns 是否安装了 polyfill + */ +export function installTextDecoderPolyfill(): boolean { + if (typeof globalThis.TextDecoder === 'undefined') { + (globalThis as any).TextDecoder = TextDecoderPolyfill; + console.log('[Polyfill] TextDecoder 已安装'); + return true; + } + return false; +} + +/** + * 检查 TextDecoder 是否可用(原生或 polyfill) + * + * @returns 是否可用 + */ +export function isTextDecoderAvailable(): boolean { + return typeof globalThis.TextDecoder !== 'undefined'; +} + +export { TextDecoderPolyfill }; diff --git a/packages/platform-common/src/polyfills/TextEncoderPolyfill.ts b/packages/platform-common/src/polyfills/TextEncoderPolyfill.ts new file mode 100644 index 00000000..adeb9cdf --- /dev/null +++ b/packages/platform-common/src/polyfills/TextEncoderPolyfill.ts @@ -0,0 +1,167 @@ +/** + * TextEncoder polyfill + * + * 用于不原生支持 TextEncoder 的平台(如微信小游戏 iOS 环境) + * + * 支持 UTF-8 编码,包含: + * - ASCII 字符(1字节输出) + * - 扩展 Unicode(2-4字节输出) + * - BMP 外字符的代理对处理 + */ +class TextEncoderPolyfill { + /** + * 编码格式(始终为 'utf-8') + */ + readonly encoding: string = 'utf-8'; + + /** + * 将字符串编码为 UTF-8 字节数组 + * + * @param input - 要编码的字符串 + * @returns UTF-8 编码的字节数组 + */ + encode(input: string = ''): Uint8Array { + const bytes: number[] = []; + + for (let i = 0; i < input.length; i++) { + let codePoint = input.charCodeAt(i); + + // 处理代理对(BMP 外的字符) + if (codePoint >= 0xD800 && codePoint <= 0xDBFF) { + // 高代理项 + if (i + 1 < input.length) { + const next = input.charCodeAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + // 低代理项 - 组合成完整码点 + codePoint = 0x10000 + ((codePoint - 0xD800) << 10) + (next - 0xDC00); + i++; // 跳过低代理项 + } + } + } else if (codePoint >= 0xDC00 && codePoint <= 0xDFFF) { + // 孤立的低代理项 - 替换为替换字符 + codePoint = 0xFFFD; + } + + if (codePoint < 0x80) { + // 1字节字符(ASCII) + bytes.push(codePoint); + } else if (codePoint < 0x800) { + // 2字节字符 + bytes.push(0xC0 | (codePoint >> 6)); + bytes.push(0x80 | (codePoint & 0x3F)); + } else if (codePoint < 0x10000) { + // 3字节字符 + bytes.push(0xE0 | (codePoint >> 12)); + bytes.push(0x80 | ((codePoint >> 6) & 0x3F)); + bytes.push(0x80 | (codePoint & 0x3F)); + } else if (codePoint <= 0x10FFFF) { + // 4字节字符 + bytes.push(0xF0 | (codePoint >> 18)); + bytes.push(0x80 | ((codePoint >> 12) & 0x3F)); + bytes.push(0x80 | ((codePoint >> 6) & 0x3F)); + bytes.push(0x80 | (codePoint & 0x3F)); + } else { + // 无效码点 - 使用替换字符 + bytes.push(0xEF, 0xBF, 0xBD); // U+FFFD 的 UTF-8 编码 + } + } + + return new Uint8Array(bytes); + } + + /** + * 将字符串编码到目标缓冲区 + * + * 尽可能多地将源字符串编码到目标缓冲区中 + * + * @param source - 要编码的字符串 + * @param destination - 目标缓冲区 + * @returns 包含已读取字符数和已写入字节数的对象 + */ + encodeInto(source: string, destination: Uint8Array): TextEncoderEncodeIntoResult { + let read = 0; + let written = 0; + + for (let i = 0; i < source.length; i++) { + let codePoint = source.charCodeAt(i); + let bytesNeeded: number; + + // 处理代理对 + if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i + 1 < source.length) { + const next = source.charCodeAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + codePoint = 0x10000 + ((codePoint - 0xD800) << 10) + (next - 0xDC00); + } + } + + // 计算所需字节数 + if (codePoint < 0x80) { + bytesNeeded = 1; + } else if (codePoint < 0x800) { + bytesNeeded = 2; + } else if (codePoint < 0x10000) { + bytesNeeded = 3; + } else { + bytesNeeded = 4; + } + + // 检查是否有足够空间 + if (written + bytesNeeded > destination.length) { + break; + } + + // 写入字节 + if (codePoint < 0x80) { + destination[written++] = codePoint; + } else if (codePoint < 0x800) { + destination[written++] = 0xC0 | (codePoint >> 6); + destination[written++] = 0x80 | (codePoint & 0x3F); + } else if (codePoint < 0x10000) { + destination[written++] = 0xE0 | (codePoint >> 12); + destination[written++] = 0x80 | ((codePoint >> 6) & 0x3F); + destination[written++] = 0x80 | (codePoint & 0x3F); + } else { + destination[written++] = 0xF0 | (codePoint >> 18); + destination[written++] = 0x80 | ((codePoint >> 12) & 0x3F); + destination[written++] = 0x80 | ((codePoint >> 6) & 0x3F); + destination[written++] = 0x80 | (codePoint & 0x3F); + } + + read++; + // 如果处理了代理对,跳过低代理项 + if (codePoint >= 0x10000) { + read++; + i++; + } + } + + return { read, written }; + } +} + +/** + * 安装 TextEncoder polyfill + * + * 如果当前环境不支持 TextEncoder,则安装 polyfill + * + * @returns 是否安装了 polyfill + */ +export function installTextEncoderPolyfill(): boolean { + if (typeof globalThis.TextEncoder === 'undefined') { + (globalThis as any).TextEncoder = TextEncoderPolyfill; + console.log('[Polyfill] TextEncoder 已安装'); + return true; + } + return false; +} + +/** + * 检查 TextEncoder 是否可用(原生或 polyfill) + * + * @returns 是否可用 + */ +export function isTextEncoderAvailable(): boolean { + return typeof globalThis.TextEncoder !== 'undefined'; +} + +export { TextEncoderPolyfill }; diff --git a/packages/platform-common/src/polyfills/index.ts b/packages/platform-common/src/polyfills/index.ts new file mode 100644 index 00000000..cfdb9d57 --- /dev/null +++ b/packages/platform-common/src/polyfills/index.ts @@ -0,0 +1,58 @@ +/** + * 平台 polyfills + * + * 提供跨平台兼容性支持,用于填补不同平台的 API 差异 + */ + +export { + TextDecoderPolyfill, + installTextDecoderPolyfill, + isTextDecoderAvailable +} from './TextDecoderPolyfill'; + +export { + TextEncoderPolyfill, + installTextEncoderPolyfill, + isTextEncoderAvailable +} from './TextEncoderPolyfill'; + +import { installTextDecoderPolyfill } from './TextDecoderPolyfill'; +import { installTextEncoderPolyfill } from './TextEncoderPolyfill'; + +/** + * 安装当前平台所需的所有 polyfills + * + * @returns 已安装的 polyfill 列表 + */ +export function installAllPolyfills(): { installed: string[] } { + const installed: string[] = []; + + if (installTextDecoderPolyfill()) { + installed.push('TextDecoder'); + } + + if (installTextEncoderPolyfill()) { + installed.push('TextEncoder'); + } + + return { installed }; +} + +/** + * 检查当前平台需要哪些 polyfills + * + * @returns 需要的 polyfill 名称列表 + */ +export function getRequiredPolyfills(): string[] { + const required: string[] = []; + + if (typeof globalThis.TextDecoder === 'undefined') { + required.push('TextDecoder'); + } + + if (typeof globalThis.TextEncoder === 'undefined') { + required.push('TextEncoder'); + } + + return required; +} diff --git a/packages/platform-common/src/wasm/IWasmLibraryLoader.ts b/packages/platform-common/src/wasm/IWasmLibraryLoader.ts new file mode 100644 index 00000000..acb093d5 --- /dev/null +++ b/packages/platform-common/src/wasm/IWasmLibraryLoader.ts @@ -0,0 +1,240 @@ +/** + * WASM 库平台适配层 + * + * 提供统一的 WASM 库加载接口,屏蔽不同平台的差异 + * + * 支持的平台: + * - Web 浏览器(标准 WebAssembly API) + * - 微信小游戏(WXWebAssembly) + * - 字节跳动小游戏 + * - 支付宝小游戏 + * - 百度小游戏 + */ + +/** + * 平台类型枚举 + */ +export enum PlatformType { + /** Web 浏览器 */ + Web = 'web', + /** 微信小游戏 */ + WeChatMiniGame = 'wechat-minigame', + /** 字节跳动小游戏 */ + ByteDanceMiniGame = 'bytedance-minigame', + /** 支付宝小游戏 */ + AlipayMiniGame = 'alipay-minigame', + /** 百度小游戏 */ + BaiduMiniGame = 'baidu-minigame', + /** Node.js */ + NodeJS = 'nodejs', + /** 未知平台 */ + Unknown = 'unknown' +} + +/** + * WASM 库加载配置 + * + * @example + * ```typescript + * const config: WasmLibraryConfig = { + * name: 'Rapier2D', + * web: { + * useCompat: true, // Web 使用 compat 版本 + * }, + * minigame: { + * wasmPath: 'wasm/rapier2d_bg.wasm', + * needsTextDecoderPolyfill: true, + * } + * }; + * ``` + */ +export interface WasmLibraryConfig { + /** + * 库名称(用于日志和错误提示) + */ + name: string; + + /** + * Web 平台配置 + */ + web?: { + /** + * 使用 -compat 版本(WASM 以 base64 嵌入 JS) + * + * 优点:无需额外配置,开箱即用 + * 缺点:包体积较大,首次加载慢 + */ + useCompat?: boolean; + + /** + * 模块路径(非 compat 版本时使用) + */ + modulePath?: string; + + /** + * WASM 文件路径(非 compat 版本时使用) + */ + wasmPath?: string; + }; + + /** + * 小游戏平台配置 + */ + minigame?: { + /** + * WASM 文件路径(相对于小游戏根目录) + */ + wasmPath: string; + + /** + * JS glue 文件路径(可选) + */ + gluePath?: string; + + /** + * 是否需要 TextDecoder polyfill + * + * iOS 微信小游戏通常需要此 polyfill + */ + needsTextDecoderPolyfill?: boolean; + + /** + * 是否需要 TextEncoder polyfill + * + * iOS 微信小游戏通常需要此 polyfill + */ + needsTextEncoderPolyfill?: boolean; + }; + + /** + * 自定义初始化函数 + * + * 用于库特定的初始化逻辑 + * + * @param wasmInstance - WASM 实例 + * @returns 初始化后的模块 + */ + customInit?: (wasmInstance: any) => Promise; +} + +/** + * 平台信息 + * Platform information + */ +export interface PlatformInfo { + /** 平台类型 | Platform type */ + type: PlatformType; + + /** 是否支持 WebAssembly | Supports WebAssembly */ + supportsWasm: boolean; + + /** 是否支持 SharedArrayBuffer | Supports SharedArrayBuffer */ + supportsSharedArrayBuffer: boolean; + + /** 需要安装的 polyfills 列表 | Required polyfills */ + needsPolyfills: string[]; + + /** + * 是否在编辑器环境(Tauri 桌面应用) + * Whether running in editor environment (Tauri desktop app) + */ + isEditor: boolean; +} + +/** + * WASM 库加载器接口 + * + * 每个 WASM 库需要实现此接口以支持跨平台加载 + * + * @typeParam T - WASM 库模块类型 + * + * @example + * ```typescript + * class Rapier2DLoader implements IWasmLibraryLoader { + * async load(): Promise { + * const RAPIER = await import('@dimforge/rapier2d-compat'); + * await RAPIER.init(); + * return RAPIER; + * } + * + * isSupported(): boolean { + * return typeof WebAssembly !== 'undefined'; + * } + * + * getPlatformInfo(): PlatformInfo { + * return { + * type: PlatformType.Web, + * supportsWasm: true, + * supportsSharedArrayBuffer: true, + * needsPolyfills: [] + * }; + * } + * } + * ``` + */ +export interface IWasmLibraryLoader { + /** + * 加载 WASM 库 + * + * @returns 加载完成的库模块 + * @throws 如果加载失败或平台不支持 + */ + load(): Promise; + + /** + * 检查当前平台是否支持此库 + * + * @returns 是否支持 + */ + isSupported(): boolean; + + /** + * 获取当前平台信息 + * + * @returns 平台信息 + */ + getPlatformInfo(): PlatformInfo; + + /** + * 获取库配置 + * + * @returns 库配置 + */ + getConfig(): WasmLibraryConfig; +} + +/** + * 平台特定 WASM 加载器接口 + * + * 提供平台级别的 WASM 加载能力 + */ +export interface IPlatformWasmLoader { + /** + * 平台类型 + */ + readonly platformType: PlatformType; + + /** + * 加载 WASM 模块 + * + * @param wasmPath - WASM 文件路径 + * @param imports - WASM 导入对象 + * @returns WASM 实例 + */ + loadWasmModule( + wasmPath: string, + imports?: WebAssembly.Imports + ): Promise; + + /** + * 检查是否支持 WASM + * + * @returns 是否支持 + */ + isSupported(): boolean; + + /** + * 安装必要的 polyfills + */ + installPolyfills(): void; +} diff --git a/packages/platform-common/src/wasm/WasmLibraryLoaderFactory.ts b/packages/platform-common/src/wasm/WasmLibraryLoaderFactory.ts new file mode 100644 index 00000000..265554aa --- /dev/null +++ b/packages/platform-common/src/wasm/WasmLibraryLoaderFactory.ts @@ -0,0 +1,241 @@ +/** + * WASM 库加载器工厂 + * + * 提供自动平台检测和加载器创建功能 + * + * @example + * ```typescript + * // 注册加载器 + * WasmLibraryLoaderFactory.registerLoader( + * 'rapier2d', + * PlatformType.Web, + * () => new WebRapier2DLoader(config) + * ); + * + * // 创建加载器(自动选择平台) + * const loader = WasmLibraryLoaderFactory.createLoader('rapier2d'); + * const rapier = await loader.load(); + * ``` + */ + +import { PlatformType, IWasmLibraryLoader } from './IWasmLibraryLoader'; + +/** + * 加载器创建函数类型 + */ +type LoaderFactory = () => IWasmLibraryLoader; + +/** + * 已注册的加载器映射 + * + * 结构:libraryName -> platformType -> loaderFactory + */ +const registeredLoaders = new Map>>(); + +/** + * 缓存的平台检测结果 + */ +let detectedPlatform: PlatformType | null = null; + +/** + * WASM 库加载器工厂 + */ +export class WasmLibraryLoaderFactory { + /** + * 注册 WASM 库加载器 + * + * @param libraryName - 库名称(如 'rapier2d') + * @param platform - 目标平台 + * @param factory - 加载器工厂函数 + * + * @example + * ```typescript + * WasmLibraryLoaderFactory.registerLoader( + * 'rapier2d', + * PlatformType.Web, + * () => new WebRapier2DLoader(config) + * ); + * ``` + */ + static registerLoader( + libraryName: string, + platform: PlatformType, + factory: LoaderFactory + ): void { + if (!registeredLoaders.has(libraryName)) { + registeredLoaders.set(libraryName, new Map()); + } + registeredLoaders.get(libraryName)!.set(platform, factory); + } + + /** + * 检测当前运行平台 + * + * 检测顺序: + * 1. 微信小游戏(wx 全局对象) + * 2. 字节跳动小游戏(tt 全局对象) + * 3. 支付宝小游戏(my 全局对象) + * 4. 百度小游戏(swan 全局对象) + * 5. Node.js(process 对象) + * 6. Web 浏览器(window + document) + * + * @returns 检测到的平台类型 + */ + static detectPlatform(): PlatformType { + if (detectedPlatform !== null) { + return detectedPlatform; + } + + // 微信小游戏 + if (typeof (globalThis as any).wx !== 'undefined') { + const wx = (globalThis as any).wx; + if (wx.getSystemInfo && wx.createCanvas) { + detectedPlatform = PlatformType.WeChatMiniGame; + return detectedPlatform; + } + } + + // 字节跳动小游戏 + if (typeof (globalThis as any).tt !== 'undefined') { + const tt = (globalThis as any).tt; + if (tt.getSystemInfo) { + detectedPlatform = PlatformType.ByteDanceMiniGame; + return detectedPlatform; + } + } + + // 支付宝小游戏 + if (typeof (globalThis as any).my !== 'undefined') { + const my = (globalThis as any).my; + if (my.getSystemInfo) { + detectedPlatform = PlatformType.AlipayMiniGame; + return detectedPlatform; + } + } + + // 百度小游戏 + if (typeof (globalThis as any).swan !== 'undefined') { + const swan = (globalThis as any).swan; + if (swan.getSystemInfo) { + detectedPlatform = PlatformType.BaiduMiniGame; + return detectedPlatform; + } + } + + // Node.js + if (typeof process !== 'undefined' && process.versions?.node) { + detectedPlatform = PlatformType.NodeJS; + return detectedPlatform; + } + + // Web 浏览器 + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + detectedPlatform = PlatformType.Web; + return detectedPlatform; + } + + detectedPlatform = PlatformType.Unknown; + return detectedPlatform; + } + + /** + * 创建 WASM 库加载器 + * + * 自动检测平台并选择对应的加载器 + * + * @param libraryName - 库名称 + * @returns 对应平台的加载器实例 + * @throws 如果库未注册或平台不支持 + * + * @example + * ```typescript + * const loader = WasmLibraryLoaderFactory.createLoader('rapier2d'); + * const rapier = await loader.load(); + * ``` + */ + static createLoader(libraryName: string): IWasmLibraryLoader { + const platform = this.detectPlatform(); + const libraryLoaders = registeredLoaders.get(libraryName); + + if (!libraryLoaders) { + throw new Error(`[WasmLibraryLoaderFactory] 未注册的库: ${libraryName}`); + } + + const factory = libraryLoaders.get(platform); + + if (!factory) { + // 尝试使用 Web 加载器作为降级方案 + const webFactory = libraryLoaders.get(PlatformType.Web); + if (webFactory && platform !== PlatformType.Unknown) { + console.warn( + `[WasmLibraryLoaderFactory] 平台 ${platform} 没有专用加载器,使用 Web 加载器作为降级方案` + ); + return webFactory() as IWasmLibraryLoader; + } + + throw new Error( + `[WasmLibraryLoaderFactory] 库 "${libraryName}" 不支持平台: ${platform}` + ); + } + + return factory() as IWasmLibraryLoader; + } + + /** + * 检查指定库是否支持当前平台 + * + * @param libraryName - 库名称 + * @returns 是否支持 + */ + static isLibrarySupported(libraryName: string): boolean { + const platform = this.detectPlatform(); + const libraryLoaders = registeredLoaders.get(libraryName); + + if (!libraryLoaders) { + return false; + } + + return libraryLoaders.has(platform) || libraryLoaders.has(PlatformType.Web); + } + + /** + * 获取库支持的所有平台 + * + * @param libraryName - 库名称 + * @returns 支持的平台列表 + */ + static getSupportedPlatforms(libraryName: string): PlatformType[] { + const libraryLoaders = registeredLoaders.get(libraryName); + if (!libraryLoaders) { + return []; + } + return Array.from(libraryLoaders.keys()); + } + + /** + * 获取所有已注册的库名称 + * + * @returns 库名称列表 + */ + static getRegisteredLibraries(): string[] { + return Array.from(registeredLoaders.keys()); + } + + /** + * 清除平台检测缓存 + * + * 主要用于测试 + */ + static clearPlatformCache(): void { + detectedPlatform = null; + } + + /** + * 清除所有已注册的加载器 + * + * 主要用于测试 + */ + static clearAllLoaders(): void { + registeredLoaders.clear(); + } +} diff --git a/packages/platform-common/src/wasm/index.ts b/packages/platform-common/src/wasm/index.ts new file mode 100644 index 00000000..148d3979 --- /dev/null +++ b/packages/platform-common/src/wasm/index.ts @@ -0,0 +1,16 @@ +/** + * WASM 库加载器 + * + * 提供跨平台的 WASM 库加载支持 + */ + +export { PlatformType } from './IWasmLibraryLoader'; + +export type { + WasmLibraryConfig, + PlatformInfo, + IWasmLibraryLoader, + IPlatformWasmLoader +} from './IWasmLibraryLoader'; + +export { WasmLibraryLoaderFactory } from './WasmLibraryLoaderFactory';