feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 * feat: 增强编辑器UI功能与跨平台支持 * fix: 修复CI测试和类型检查问题 * fix: 修复CI问题并提高测试覆盖率 * fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
54
packages/platform-web/package.json
Normal file
54
packages/platform-web/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@esengine/platform-web",
|
||||
"version": "1.0.0",
|
||||
"description": "Web/H5 平台适配器",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"build:npm": "npm run build",
|
||||
"clean": "rimraf dist",
|
||||
"type-check": "npx tsc --noEmit",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"web",
|
||||
"h5",
|
||||
"platform",
|
||||
"adapter"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@esengine/platform-common": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/platform-web"
|
||||
}
|
||||
}
|
||||
42
packages/platform-web/rollup.config.js
Normal file
42
packages/platform-web/rollup.config.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
const external = ['@esengine/platform-common'];
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.mjs',
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
},
|
||||
{
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
external,
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
declaration: false
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'esm'
|
||||
},
|
||||
external,
|
||||
plugins: [dts()]
|
||||
}
|
||||
];
|
||||
254
packages/platform-web/src/EngineBridge.ts
Normal file
254
packages/platform-web/src/EngineBridge.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Rust 引擎桥接层
|
||||
* 负责在 Web 环境中初始化和管理 Rust WASM 引擎
|
||||
*/
|
||||
|
||||
import type { IPlatformCanvas, CanvasContextAttributes } from '@esengine/platform-common';
|
||||
import { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem';
|
||||
|
||||
/**
|
||||
* 引擎配置
|
||||
*/
|
||||
export interface EngineBridgeConfig {
|
||||
wasmPath: string;
|
||||
canvasId?: string;
|
||||
canvasWidth?: number;
|
||||
canvasHeight?: number;
|
||||
contextAttributes?: CanvasContextAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* GameEngine WASM 模块导出接口
|
||||
*/
|
||||
interface GameEngineExports {
|
||||
memory: WebAssembly.Memory;
|
||||
new: (canvasIdPtr: number, canvasIdLen: number) => number;
|
||||
fromExternal: (glContext: any, width: number, height: number) => any;
|
||||
clear: (engine: any, r: number, g: number, b: number, a: number) => void;
|
||||
render: (engine: any) => void;
|
||||
width: (engine: any) => number;
|
||||
height: (engine: any) => number;
|
||||
submitSpriteBatch: (
|
||||
engine: any,
|
||||
transforms: any,
|
||||
textureIds: any,
|
||||
uvs: any,
|
||||
colors: any
|
||||
) => void;
|
||||
loadTexture: (engine: any, id: number, urlPtr: number, urlLen: number) => void;
|
||||
isKeyDown: (engine: any, keyCodePtr: number, keyCodeLen: number) => boolean;
|
||||
updateInput: (engine: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 引擎桥接层
|
||||
* 将 Web 平台能力桥接到 Rust WASM 引擎
|
||||
*/
|
||||
export class EngineBridge {
|
||||
private _canvasSubsystem: WebCanvasSubsystem;
|
||||
private _canvas: IPlatformCanvas;
|
||||
private _gl: WebGL2RenderingContext | null = null;
|
||||
private _wasmModule: WebAssembly.Module | null = null;
|
||||
private _wasmInstance: WebAssembly.Instance | null = null;
|
||||
private _gameEngine: any = null;
|
||||
private _config: EngineBridgeConfig;
|
||||
|
||||
constructor(config: EngineBridgeConfig) {
|
||||
this._config = config;
|
||||
this._canvasSubsystem = new WebCanvasSubsystem();
|
||||
|
||||
const width = config.canvasWidth ?? window.innerWidth;
|
||||
const height = config.canvasHeight ?? window.innerHeight;
|
||||
|
||||
if (config.canvasId) {
|
||||
const existingCanvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
|
||||
if (existingCanvas) {
|
||||
existingCanvas.width = width;
|
||||
existingCanvas.height = height;
|
||||
this._canvas = this._wrapExistingCanvas(existingCanvas);
|
||||
} else {
|
||||
this._canvas = this._canvasSubsystem.createCanvas(width, height);
|
||||
}
|
||||
} else {
|
||||
this._canvas = this._canvasSubsystem.createCanvas(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
private _wrapExistingCanvas(canvas: HTMLCanvasElement): IPlatformCanvas {
|
||||
return {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
getContext: (type: string, attrs: any) => canvas.getContext(type, attrs as WebGLContextAttributes),
|
||||
toDataURL: () => canvas.toDataURL(),
|
||||
toTempFilePath: () => {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
} as IPlatformCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化引擎
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
this._gl = this._getWebGLContext();
|
||||
if (!this._gl) {
|
||||
throw new Error('无法获取 WebGL2 上下文');
|
||||
}
|
||||
|
||||
const imports = this._createWASMImports();
|
||||
const response = await fetch(this._config.wasmPath);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
const result = await WebAssembly.instantiate(buffer, imports);
|
||||
this._wasmModule = result.module;
|
||||
this._wasmInstance = result.instance;
|
||||
|
||||
const exports = this._wasmInstance.exports as unknown as GameEngineExports;
|
||||
|
||||
if (typeof exports.fromExternal === 'function') {
|
||||
this._gameEngine = exports.fromExternal(
|
||||
this._gl,
|
||||
this._canvas.width,
|
||||
this._canvas.height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebGL2 上下文
|
||||
*/
|
||||
private _getWebGLContext(): WebGL2RenderingContext | null {
|
||||
const attrs = this._config.contextAttributes ?? {
|
||||
alpha: false,
|
||||
antialias: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
premultipliedAlpha: true,
|
||||
preserveDrawingBuffer: false
|
||||
};
|
||||
|
||||
return this._canvas.getContext('webgl2', attrs) as WebGL2RenderingContext | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 WASM 导入对象
|
||||
*/
|
||||
private _createWASMImports(): WebAssembly.Imports {
|
||||
return {
|
||||
env: {
|
||||
memory: new WebAssembly.Memory({ initial: 256, maximum: 16384 }),
|
||||
|
||||
platform_log: (ptr: number, len: number) => {
|
||||
const message = this._readString(ptr, len);
|
||||
console.log('[Engine]', message);
|
||||
},
|
||||
|
||||
platform_error: (ptr: number, len: number) => {
|
||||
const message = this._readString(ptr, len);
|
||||
console.error('[Engine]', message);
|
||||
},
|
||||
|
||||
platform_now: () => {
|
||||
return performance.now();
|
||||
}
|
||||
},
|
||||
wbg: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 WASM 内存读取字符串
|
||||
*/
|
||||
private _readString(ptr: number, len: number): string {
|
||||
if (!this._wasmInstance) return '';
|
||||
|
||||
const memory = this._wasmInstance.exports.memory as WebAssembly.Memory;
|
||||
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Canvas
|
||||
*/
|
||||
get canvas(): IPlatformCanvas {
|
||||
return this._canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebGL 上下文
|
||||
*/
|
||||
get gl(): WebGL2RenderingContext | null {
|
||||
return this._gl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WASM 实例
|
||||
*/
|
||||
get wasmInstance(): WebAssembly.Instance | null {
|
||||
return this._wasmInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GameEngine 实例
|
||||
*/
|
||||
get gameEngine(): any {
|
||||
return this._gameEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清屏
|
||||
*/
|
||||
clear(r: number, g: number, b: number, a: number): void {
|
||||
if (this._gl) {
|
||||
this._gl.clearColor(r, g, b, a);
|
||||
this._gl.clear(this._gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染一帧
|
||||
*/
|
||||
render(): void {
|
||||
if (this._wasmInstance && this._gameEngine) {
|
||||
const exports = this._wasmInstance.exports as unknown as GameEngineExports;
|
||||
if (exports.render) {
|
||||
exports.render(this._gameEngine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布宽度
|
||||
*/
|
||||
get width(): number {
|
||||
return this._canvas.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布高度
|
||||
*/
|
||||
get height(): number {
|
||||
return this._canvas.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整画布大小
|
||||
*/
|
||||
resize(width: number, height: number): void {
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
if (this._gl) {
|
||||
this._gl.viewport(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁引擎
|
||||
*/
|
||||
dispose(): void {
|
||||
this._gameEngine = null;
|
||||
this._wasmInstance = null;
|
||||
this._wasmModule = null;
|
||||
this._gl = null;
|
||||
}
|
||||
}
|
||||
19
packages/platform-web/src/index.ts
Normal file
19
packages/platform-web/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Web/H5 平台适配器包
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// 引擎桥接
|
||||
export { EngineBridge } from './EngineBridge';
|
||||
export type { EngineBridgeConfig } from './EngineBridge';
|
||||
|
||||
// 子系统
|
||||
export { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem';
|
||||
export { WebInputSubsystem } from './subsystems/WebInputSubsystem';
|
||||
export { WebStorageSubsystem } from './subsystems/WebStorageSubsystem';
|
||||
export { WebWASMSubsystem } from './subsystems/WebWASMSubsystem';
|
||||
|
||||
// 工具
|
||||
export function isWebPlatform(): boolean {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
||||
}
|
||||
174
packages/platform-web/src/subsystems/WebCanvasSubsystem.ts
Normal file
174
packages/platform-web/src/subsystems/WebCanvasSubsystem.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Web 平台 Canvas 子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformCanvasSubsystem,
|
||||
IPlatformCanvas,
|
||||
IPlatformImage,
|
||||
TempFilePathOptions,
|
||||
CanvasContextAttributes
|
||||
} from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* Web Canvas 包装
|
||||
*/
|
||||
class WebCanvas implements IPlatformCanvas {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this._canvas = canvas;
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this._canvas.width;
|
||||
}
|
||||
|
||||
set width(value: number) {
|
||||
this._canvas.width = value;
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this._canvas.height;
|
||||
}
|
||||
|
||||
set height(value: number) {
|
||||
this._canvas.height = value;
|
||||
}
|
||||
|
||||
getContext(
|
||||
contextType: '2d' | 'webgl' | 'webgl2',
|
||||
contextAttributes?: CanvasContextAttributes
|
||||
): RenderingContext | null {
|
||||
const attrs: WebGLContextAttributes | undefined = contextAttributes ? {
|
||||
alpha: typeof contextAttributes.alpha === 'number'
|
||||
? contextAttributes.alpha > 0
|
||||
: contextAttributes.alpha,
|
||||
antialias: contextAttributes.antialias,
|
||||
depth: contextAttributes.depth,
|
||||
stencil: contextAttributes.stencil,
|
||||
premultipliedAlpha: contextAttributes.premultipliedAlpha,
|
||||
preserveDrawingBuffer: contextAttributes.preserveDrawingBuffer,
|
||||
failIfMajorPerformanceCaveat: contextAttributes.failIfMajorPerformanceCaveat,
|
||||
powerPreference: contextAttributes.powerPreference
|
||||
} : undefined;
|
||||
return this._canvas.getContext(contextType, attrs);
|
||||
}
|
||||
|
||||
toDataURL(): string {
|
||||
return this._canvas.toDataURL();
|
||||
}
|
||||
|
||||
toTempFilePath(_options: TempFilePathOptions): void {
|
||||
throw new Error('toTempFilePath is not supported on Web platform');
|
||||
}
|
||||
|
||||
getNativeCanvas(): HTMLCanvasElement {
|
||||
return this._canvas;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Image 包装
|
||||
*/
|
||||
class WebImage implements IPlatformImage {
|
||||
private _image: HTMLImageElement;
|
||||
|
||||
constructor() {
|
||||
this._image = new Image();
|
||||
}
|
||||
|
||||
get src(): string {
|
||||
return this._image.src;
|
||||
}
|
||||
|
||||
set src(value: string) {
|
||||
this._image.src = value;
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this._image.width;
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this._image.height;
|
||||
}
|
||||
|
||||
get onload(): (() => void) | null {
|
||||
return this._image.onload as (() => void) | null;
|
||||
}
|
||||
|
||||
set onload(value: (() => void) | null) {
|
||||
this._image.onload = value;
|
||||
}
|
||||
|
||||
get onerror(): ((error: any) => void) | null {
|
||||
return this._image.onerror as ((error: any) => void) | null;
|
||||
}
|
||||
|
||||
set onerror(value: ((error: any) => void) | null) {
|
||||
this._image.onerror = value;
|
||||
}
|
||||
|
||||
getNativeImage(): HTMLImageElement {
|
||||
return this._image;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web 平台 Canvas 子系统实现
|
||||
*/
|
||||
export class WebCanvasSubsystem implements IPlatformCanvasSubsystem {
|
||||
private _mainCanvas: WebCanvas | null = null;
|
||||
|
||||
createCanvas(width?: number, height?: number): IPlatformCanvas {
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
if (width !== undefined) {
|
||||
canvas.width = width;
|
||||
}
|
||||
if (height !== undefined) {
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
const wrappedCanvas = new WebCanvas(canvas);
|
||||
|
||||
if (!this._mainCanvas) {
|
||||
this._mainCanvas = wrappedCanvas;
|
||||
}
|
||||
|
||||
return wrappedCanvas;
|
||||
}
|
||||
|
||||
createImage(): IPlatformImage {
|
||||
return new WebImage();
|
||||
}
|
||||
|
||||
createImageData(width: number, height: number): ImageData {
|
||||
return new ImageData(width, height);
|
||||
}
|
||||
|
||||
getScreenWidth(): number {
|
||||
return window.screen.width;
|
||||
}
|
||||
|
||||
getScreenHeight(): number {
|
||||
return window.screen.height;
|
||||
}
|
||||
|
||||
getDevicePixelRatio(): number {
|
||||
return window.devicePixelRatio || 1;
|
||||
}
|
||||
|
||||
getMainCanvas(): IPlatformCanvas | null {
|
||||
return this._mainCanvas;
|
||||
}
|
||||
|
||||
getWindowWidth(): number {
|
||||
return window.innerWidth;
|
||||
}
|
||||
|
||||
getWindowHeight(): number {
|
||||
return window.innerHeight;
|
||||
}
|
||||
}
|
||||
102
packages/platform-web/src/subsystems/WebInputSubsystem.ts
Normal file
102
packages/platform-web/src/subsystems/WebInputSubsystem.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Web 平台输入子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformInputSubsystem,
|
||||
TouchHandler,
|
||||
TouchEvent
|
||||
} from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* Web 平台输入子系统实现
|
||||
*/
|
||||
export class WebInputSubsystem implements IPlatformInputSubsystem {
|
||||
private _touchStartHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
||||
private _touchMoveHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
||||
private _touchEndHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
||||
private _touchCancelHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
||||
|
||||
onTouchStart(handler: TouchHandler): void {
|
||||
const nativeHandler = (e: globalThis.TouchEvent) => {
|
||||
handler(this.convertTouchEvent(e));
|
||||
};
|
||||
this._touchStartHandlers.set(handler, nativeHandler);
|
||||
window.addEventListener('touchstart', nativeHandler);
|
||||
}
|
||||
|
||||
onTouchMove(handler: TouchHandler): void {
|
||||
const nativeHandler = (e: globalThis.TouchEvent) => {
|
||||
handler(this.convertTouchEvent(e));
|
||||
};
|
||||
this._touchMoveHandlers.set(handler, nativeHandler);
|
||||
window.addEventListener('touchmove', nativeHandler);
|
||||
}
|
||||
|
||||
onTouchEnd(handler: TouchHandler): void {
|
||||
const nativeHandler = (e: globalThis.TouchEvent) => {
|
||||
handler(this.convertTouchEvent(e));
|
||||
};
|
||||
this._touchEndHandlers.set(handler, nativeHandler);
|
||||
window.addEventListener('touchend', nativeHandler);
|
||||
}
|
||||
|
||||
onTouchCancel(handler: TouchHandler): void {
|
||||
const nativeHandler = (e: globalThis.TouchEvent) => {
|
||||
handler(this.convertTouchEvent(e));
|
||||
};
|
||||
this._touchCancelHandlers.set(handler, nativeHandler);
|
||||
window.addEventListener('touchcancel', nativeHandler);
|
||||
}
|
||||
|
||||
offTouchStart(handler: TouchHandler): void {
|
||||
const nativeHandler = this._touchStartHandlers.get(handler);
|
||||
if (nativeHandler) {
|
||||
window.removeEventListener('touchstart', nativeHandler);
|
||||
this._touchStartHandlers.delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
offTouchMove(handler: TouchHandler): void {
|
||||
const nativeHandler = this._touchMoveHandlers.get(handler);
|
||||
if (nativeHandler) {
|
||||
window.removeEventListener('touchmove', nativeHandler);
|
||||
this._touchMoveHandlers.delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
offTouchEnd(handler: TouchHandler): void {
|
||||
const nativeHandler = this._touchEndHandlers.get(handler);
|
||||
if (nativeHandler) {
|
||||
window.removeEventListener('touchend', nativeHandler);
|
||||
this._touchEndHandlers.delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
offTouchCancel(handler: TouchHandler): void {
|
||||
const nativeHandler = this._touchCancelHandlers.get(handler);
|
||||
if (nativeHandler) {
|
||||
window.removeEventListener('touchcancel', nativeHandler);
|
||||
this._touchCancelHandlers.delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
supportsPressure(): boolean {
|
||||
return 'force' in Touch.prototype;
|
||||
}
|
||||
|
||||
private convertTouchEvent(e: globalThis.TouchEvent): TouchEvent {
|
||||
const convertTouch = (touch: globalThis.Touch) => ({
|
||||
identifier: touch.identifier,
|
||||
x: touch.clientX,
|
||||
y: touch.clientY,
|
||||
force: (touch as any).force
|
||||
});
|
||||
|
||||
return {
|
||||
touches: Array.from(e.touches).map(convertTouch),
|
||||
changedTouches: Array.from(e.changedTouches).map(convertTouch),
|
||||
timeStamp: e.timeStamp
|
||||
};
|
||||
}
|
||||
}
|
||||
77
packages/platform-web/src/subsystems/WebStorageSubsystem.ts
Normal file
77
packages/platform-web/src/subsystems/WebStorageSubsystem.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Web 平台存储子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformStorageSubsystem,
|
||||
StorageInfo
|
||||
} from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* Web 平台存储子系统实现
|
||||
*/
|
||||
export class WebStorageSubsystem implements IPlatformStorageSubsystem {
|
||||
getStorageSync<T = any>(key: string): T | undefined {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value === null) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
setStorageSync<T = any>(key: string, value: T): void {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
removeStorageSync(key: string): void {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
clearStorageSync(): void {
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
getStorageInfoSync(): StorageInfo {
|
||||
const keys: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
let currentSize = 0;
|
||||
for (const key of keys) {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value) {
|
||||
currentSize += key.length + value.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
keys,
|
||||
currentSize: Math.ceil(currentSize / 1024),
|
||||
limitSize: 5 * 1024
|
||||
};
|
||||
}
|
||||
|
||||
async getStorage<T = any>(key: string): Promise<T | undefined> {
|
||||
return this.getStorageSync<T>(key);
|
||||
}
|
||||
|
||||
async setStorage<T = any>(key: string, value: T): Promise<void> {
|
||||
this.setStorageSync(key, value);
|
||||
}
|
||||
|
||||
async removeStorage(key: string): Promise<void> {
|
||||
this.removeStorageSync(key);
|
||||
}
|
||||
|
||||
async clearStorage(): Promise<void> {
|
||||
this.clearStorageSync();
|
||||
}
|
||||
}
|
||||
44
packages/platform-web/src/subsystems/WebWASMSubsystem.ts
Normal file
44
packages/platform-web/src/subsystems/WebWASMSubsystem.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Web 平台 WASM 子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformWASMSubsystem,
|
||||
IWASMInstance,
|
||||
WASMImports,
|
||||
WASMExports
|
||||
} from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* Web 平台 WASM 子系统实现
|
||||
*/
|
||||
export class WebWASMSubsystem implements IPlatformWASMSubsystem {
|
||||
async instantiate(path: string, imports?: WASMImports): Promise<IWASMInstance> {
|
||||
const response = await fetch(path);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const result = await WebAssembly.instantiate(buffer, imports);
|
||||
|
||||
return {
|
||||
exports: result.instance.exports as WASMExports
|
||||
};
|
||||
}
|
||||
|
||||
isSupported(): boolean {
|
||||
return typeof WebAssembly !== 'undefined';
|
||||
}
|
||||
|
||||
createMemory(initial: number, maximum?: number): WebAssembly.Memory {
|
||||
return new WebAssembly.Memory({
|
||||
initial,
|
||||
maximum
|
||||
});
|
||||
}
|
||||
|
||||
createTable(initial: number, maximum?: number): WebAssembly.Table {
|
||||
return new WebAssembly.Table({
|
||||
element: 'anyfunc',
|
||||
initial,
|
||||
maximum
|
||||
});
|
||||
}
|
||||
}
|
||||
24
packages/platform-web/tsconfig.json
Normal file
24
packages/platform-web/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user