feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)

* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
This commit is contained in:
YHH
2025-12-03 22:15:22 +08:00
committed by GitHub
parent caf7622aa0
commit 63f006ab62
496 changed files with 77601 additions and 4067 deletions

View File

@@ -3,14 +3,9 @@
* 二进制资产加载器
*/
import {
AssetType,
IAssetLoadOptions,
IAssetMetadata,
IAssetLoadResult,
AssetLoadError
} from '../types/AssetTypes';
import { IAssetLoader, IBinaryAsset } from '../interfaces/IAssetLoader';
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, IBinaryAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* Binary loader implementation
@@ -22,144 +17,27 @@ export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
'.bin', '.dat', '.raw', '.bytes',
'.wasm', '.so', '.dll', '.dylib'
];
readonly contentType: AssetContentType = 'binary';
/**
* Load binary asset
* 加载二进制资产
* Parse binary from content.
* 从内容解析二进制
*/
async load(
path: string,
metadata: IAssetMetadata,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<IBinaryAsset>> {
const startTime = performance.now();
try {
const response = await this.fetchWithTimeout(path, options?.timeout);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 获取MIME类型 / Get MIME type
const mimeType = response.headers.get('content-type') || undefined;
// 获取总大小用于进度回调 / Get total size for progress callback
const contentLength = response.headers.get('content-length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
// 读取响应 / Read response
let data: ArrayBuffer;
if (options?.onProgress && total > 0) {
data = await this.readResponseWithProgress(response, total, options.onProgress);
} else {
data = await response.arrayBuffer();
}
const asset: IBinaryAsset = {
data,
mimeType
};
return {
asset,
handle: 0,
metadata,
loadTime: performance.now() - startTime
};
} catch (error) {
if (error instanceof Error) {
throw new AssetLoadError(
`Failed to load binary: ${error.message}`,
metadata.guid,
AssetType.Binary,
error
);
}
throw AssetLoadError.fileNotFound(metadata.guid, path);
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IBinaryAsset> {
if (!content.binary) {
throw new Error('Binary content is empty');
}
return {
data: content.binary
};
}
/**
* Fetch with timeout
* 带超时的fetch
*/
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
mode: 'cors',
credentials: 'same-origin'
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Read response with progress
* 带进度读取响应
*/
private async readResponseWithProgress(
response: Response,
total: number,
onProgress: (progress: number) => void
): Promise<ArrayBuffer> {
const reader = response.body?.getReader();
if (!reader) {
return response.arrayBuffer();
}
const chunks: Uint8Array[] = [];
let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
// 报告进度 / Report progress
onProgress(receivedLength / total);
}
// 合并chunks到ArrayBuffer / Merge chunks into ArrayBuffer
const result = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
result.set(chunk, position);
position += chunk.length;
}
return result.buffer;
}
/**
* Validate if the loader can handle this asset
* 验证加载器是否可以处理此资产
*/
canLoad(path: string, _metadata: IAssetMetadata): boolean {
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
return this.supportedExtensions.includes(ext);
}
/**
* Estimate memory usage for the asset
* 估算资产的内存使用量
*/
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: IBinaryAsset): void {
// ArrayBuffer无法直接释放但可以清空引用 / Can't directly release ArrayBuffer, but clear reference
(asset as any).data = null;
}
}

View File

@@ -3,14 +3,9 @@
* JSON资产加载器
*/
import {
AssetType,
IAssetLoadOptions,
IAssetMetadata,
IAssetLoadResult,
AssetLoadError
} from '../types/AssetTypes';
import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader';
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, IJsonAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* JSON loader implementation
@@ -19,144 +14,27 @@ import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader';
export class JsonLoader implements IAssetLoader<IJsonAsset> {
readonly supportedType = AssetType.Json;
readonly supportedExtensions = ['.json', '.jsonc'];
readonly contentType: AssetContentType = 'text';
/**
* Load JSON asset
* 加载JSON资产
* Parse JSON from text content.
* 从文本内容解析JSON
*/
async load(
path: string,
metadata: IAssetMetadata,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<IJsonAsset>> {
const startTime = performance.now();
try {
const response = await this.fetchWithTimeout(path, options?.timeout);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 获取总大小用于进度回调 / Get total size for progress callback
const contentLength = response.headers.get('content-length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
// 读取响应 / Read response
let jsonData: unknown;
if (options?.onProgress && total > 0) {
jsonData = await this.readResponseWithProgress(response, total, options.onProgress);
} else {
jsonData = await response.json();
}
const asset: IJsonAsset = {
data: jsonData
};
return {
asset,
handle: 0,
metadata,
loadTime: performance.now() - startTime
};
} catch (error) {
if (error instanceof Error) {
throw new AssetLoadError(
`Failed to load JSON: ${error.message}`,
metadata.guid,
AssetType.Json,
error
);
}
throw AssetLoadError.fileNotFound(metadata.guid, path);
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IJsonAsset> {
if (!content.text) {
throw new Error('JSON content is empty');
}
return {
data: JSON.parse(content.text)
};
}
/**
* Fetch with timeout
* 带超时的fetch
*/
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
mode: 'cors',
credentials: 'same-origin'
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Read response with progress
* 带进度读取响应
*/
private async readResponseWithProgress(
response: Response,
total: number,
onProgress: (progress: number) => void
): Promise<unknown> {
const reader = response.body?.getReader();
if (!reader) {
return response.json();
}
const chunks: Uint8Array[] = [];
let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
// 报告进度 / Report progress
onProgress(receivedLength / total);
}
// 合并chunks / Merge chunks
const allChunks = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
// 解码为字符串并解析JSON / Decode to string and parse JSON
const decoder = new TextDecoder('utf-8');
const jsonString = decoder.decode(allChunks);
return JSON.parse(jsonString);
}
/**
* Validate if the loader can handle this asset
* 验证加载器是否可以处理此资产
*/
canLoad(path: string, _metadata: IAssetMetadata): boolean {
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
return this.supportedExtensions.includes(ext);
}
/**
* Estimate memory usage for the asset
* 估算资产的内存使用量
*/
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: IJsonAsset): void {
// JSON资产通常不需要特殊清理 / JSON assets usually don't need special cleanup
// 但可以清空引用以帮助GC / But can clear references to help GC
(asset as any).data = null;
}
}

View File

@@ -3,14 +3,9 @@
* 文本资产加载器
*/
import {
AssetType,
IAssetLoadOptions,
IAssetMetadata,
IAssetLoadResult,
AssetLoadError
} from '../types/AssetTypes';
import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader';
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, ITextAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* Text loader implementation
@@ -19,115 +14,21 @@ import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader';
export class TextLoader implements IAssetLoader<ITextAsset> {
readonly supportedType = AssetType.Text;
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
readonly contentType: AssetContentType = 'text';
/**
* Load text asset
* 加载文本资产
* Parse text from content.
* 从内容解析文本。
*/
async load(
path: string,
metadata: IAssetMetadata,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<ITextAsset>> {
const startTime = performance.now();
try {
const response = await this.fetchWithTimeout(path, options?.timeout);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 获取总大小用于进度回调 / Get total size for progress callback
const contentLength = response.headers.get('content-length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
// 读取响应 / Read response
let content: string;
if (options?.onProgress && total > 0) {
content = await this.readResponseWithProgress(response, total, options.onProgress);
} else {
content = await response.text();
}
// 检测编码 / Detect encoding
const encoding = this.detectEncoding(content);
const asset: ITextAsset = {
content,
encoding
};
return {
asset,
handle: 0,
metadata,
loadTime: performance.now() - startTime
};
} catch (error) {
if (error instanceof Error) {
throw new AssetLoadError(
`Failed to load text: ${error.message}`,
metadata.guid,
AssetType.Text,
error
);
}
throw AssetLoadError.fileNotFound(metadata.guid, path);
}
}
/**
* Fetch with timeout
* 带超时的fetch
*/
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
mode: 'cors',
credentials: 'same-origin'
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Read response with progress
* 带进度读取响应
*/
private async readResponseWithProgress(
response: Response,
total: number,
onProgress: (progress: number) => void
): Promise<string> {
const reader = response.body?.getReader();
if (!reader) {
return response.text();
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<ITextAsset> {
if (!content.text) {
throw new Error('Text content is empty');
}
const decoder = new TextDecoder('utf-8');
let result = '';
let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedLength += value.length;
result += decoder.decode(value, { stream: true });
// 报告进度 / Report progress
onProgress(receivedLength / total);
}
return result;
return {
content: content.text,
encoding: this.detectEncoding(content.text)
};
}
/**
@@ -135,38 +36,20 @@ export class TextLoader implements IAssetLoader<ITextAsset> {
* 检测文本编码
*/
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
// 简单的编码检测 / Simple encoding detection
// 检查是否包含非ASCII字符 / Check for non-ASCII characters
for (let i = 0; i < content.length; i++) {
const charCode = content.charCodeAt(i);
if (charCode > 127) {
// 包含非ASCII字符可能是UTF-8或UTF-16 / Contains non-ASCII, likely UTF-8 or UTF-16
return charCode > 255 ? 'utf16' : 'utf8';
}
}
return 'ascii';
}
/**
* Validate if the loader can handle this asset
* 验证加载器是否可以处理此资产
*/
canLoad(path: string, _metadata: IAssetMetadata): boolean {
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
return this.supportedExtensions.includes(ext);
}
/**
* Estimate memory usage for the asset
* 估算资产的内存使用量
*/
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: ITextAsset): void {
// 清空内容以帮助GC / Clear content to help GC
(asset as any).content = '';
}
}

View File

@@ -3,14 +3,9 @@
* 纹理资产加载器
*/
import {
AssetType,
IAssetLoadOptions,
IAssetMetadata,
IAssetLoadResult,
AssetLoadError
} from '../types/AssetTypes';
import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader';
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, ITextureAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* Texture loader implementation
@@ -19,147 +14,36 @@ import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader';
export class TextureLoader implements IAssetLoader<ITextureAsset> {
readonly supportedType = AssetType.Texture;
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
readonly contentType: AssetContentType = 'image';
private static _nextTextureId = 1;
private readonly _loadedTextures = new Map<string, ITextureAsset>();
/**
* Load texture asset
* 加载纹理资产
* Parse texture from image content.
* 从图片内容解析纹理。
*/
async load(
path: string,
metadata: IAssetMetadata,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<ITextureAsset>> {
const startTime = performance.now();
// 检查缓存 / Check cache
if (!options?.forceReload && this._loadedTextures.has(path)) {
const cached = this._loadedTextures.get(path)!;
return {
asset: cached,
handle: cached.textureId,
metadata,
loadTime: 0
};
async parse(content: IAssetContent, context: IAssetParseContext): Promise<ITextureAsset> {
if (!content.image) {
throw new Error('Texture content is empty');
}
try {
// 创建图像元素 / Create image element
const image = await this.loadImage(path, options);
const image = content.image;
// 创建纹理资产 / Create texture asset
const textureAsset: ITextureAsset = {
textureId: TextureLoader._nextTextureId++,
width: image.width,
height: image.height,
format: 'rgba', // 默认格式 / Default format
hasMipmaps: false,
data: image
};
const textureAsset: ITextureAsset = {
textureId: TextureLoader._nextTextureId++,
width: image.width,
height: image.height,
format: 'rgba',
hasMipmaps: false,
data: image
};
// 缓存纹理 / Cache texture
this._loadedTextures.set(path, textureAsset);
// 触发引擎纹理加载(如果有引擎桥接) / Trigger engine texture loading if bridge exists
if (typeof window !== 'undefined' && (window as any).engineBridge) {
await this.uploadToGPU(textureAsset, path);
}
return {
asset: textureAsset,
handle: textureAsset.textureId,
metadata,
loadTime: performance.now() - startTime
};
} catch (error) {
throw AssetLoadError.fileNotFound(metadata.guid, path);
}
}
/**
* Load image from URL
* 从URL加载图像
*/
private async loadImage(url: string, options?: IAssetLoadOptions): Promise<HTMLImageElement> {
// For Tauri asset URLs, use fetch to load the image
// 对于Tauri资产URL使用fetch加载图像
if (url.startsWith('http://asset.localhost/') || url.startsWith('asset://')) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.statusText}`);
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
// Clean up blob URL after loading
// 加载后清理blob URL
URL.revokeObjectURL(blobUrl);
resolve(image);
};
image.onerror = () => {
URL.revokeObjectURL(blobUrl);
reject(new Error(`Failed to load image from blob: ${url}`));
};
image.src = blobUrl;
});
} catch (error) {
throw new Error(`Failed to load Tauri asset: ${url} - ${error}`);
}
// Upload to GPU if bridge exists.
if (typeof window !== 'undefined' && (window as any).engineBridge) {
await this.uploadToGPU(textureAsset, context.metadata.path);
}
// For regular URLs, use standard Image loading
// 对于常规URL使用标准Image加载
return new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = 'anonymous';
// 超时处理 / Timeout handling
const timeout = options?.timeout || 30000;
const timeoutId = setTimeout(() => {
reject(new Error(`Image load timeout: ${url}`));
}, timeout);
// 进度回调 / Progress callback
if (options?.onProgress) {
// 图像加载没有真正的进度事件,模拟进度 / Images don't have real progress events, simulate
let progress = 0;
const progressInterval = setInterval(() => {
progress = Math.min(progress + 0.1, 0.9);
options.onProgress!(progress);
}, 100);
image.onload = () => {
clearInterval(progressInterval);
clearTimeout(timeoutId);
options.onProgress!(1);
resolve(image);
};
image.onerror = () => {
clearInterval(progressInterval);
clearTimeout(timeoutId);
reject(new Error(`Failed to load image: ${url}`));
};
} else {
image.onload = () => {
clearTimeout(timeoutId);
resolve(image);
};
image.onerror = () => {
clearTimeout(timeoutId);
reject(new Error(`Failed to load image: ${url}`));
};
}
image.src = url;
});
return textureAsset;
}
/**
@@ -173,34 +57,12 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
}
}
/**
* Validate if the loader can handle this asset
* 验证加载器是否可以处理此资产
*/
canLoad(path: string, _metadata: IAssetMetadata): boolean {
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
return this.supportedExtensions.includes(ext);
}
/**
* Estimate memory usage for the asset
* 估算资产的内存使用量
*/
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: ITextureAsset): void {
// 从缓存中移除 / Remove from cache
for (const [path, cached] of this._loadedTextures.entries()) {
if (cached === asset) {
this._loadedTextures.delete(path);
break;
}
}
// 释放GPU资源 / Release GPU resources
// Release GPU resources.
if (typeof window !== 'undefined' && (window as any).engineBridge) {
const bridge = (window as any).engineBridge;
if (bridge.unloadTexture) {
@@ -208,7 +70,7 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
}
}
// 清理图像数据 / Clean up image data
// Clean up image data.
if (asset.data instanceof HTMLImageElement) {
asset.data.src = '';
}